Repository navigation
metadata-protocol: the draft save records draftChanges (per-key provenance: which surface, actor and time last changed each key path) and serves it on the strict draft read; the save request declares its surface — the writer of #24298's key #24395
Description
Filing gate ③: a ruled design's writer half, filed at the spec card's landing as triage ordered (#24298, 6249057462). Filed by domain:spec seat 2 (#18549) · zhuangjianguo · session session_019oFY6Hi35J7atvgVFnmoaK. ⛔ Not a claim. Self-labelled domain:engine under the filer exception (packages/metadata-protocol). Grade and area:* follow the spec card; triage may change them.
The ruled pull
- 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: Studio's properties mark a field or view setting the copilot changed with a 草稿 chip and a 来源 line (「由 AI 副驾在 2 分钟前作为草稿加入」).
- Ruling A on [Decision] The copilot's drafts: who sees a copilot change before publish, and where — preview-first (A), auto-publish additive edits (B), sticky author preview (C) or a dev environment (D); the frame #24298 waits on #24324 (
6250704607) keeps that reader and the model: AI drafts, a human publishes. - The spec key landed in PR feat(spec): the draft read declares draftChanges, a pending draft's per-key provenance #24383 →
584b4225e1(metadata-protocol / spec: a draft carries no per-field provenance (which surface changed which key, and when), so Studio cannot mark a copilot's draft change with the ruled 草稿 chip and 来源 line #24298):GetMetaItemResponseSchema.draftChanges,{ path: string[], by: 'copilot' | 'studio' | 'api', actor: string | null, at: datetime }. - Its describe states "No server writes the key yet", and gives the writer's contract (below). This card is that writer.
What the writer must do (the contract in the spec describe on main)
- Record. The draft save (
saveMetaItemmode: 'draft'→SysMetadataRepository.putstate: 'draft', insideresolveWriteHead's transaction) compares the new body with the body it replaces: the existing draft row, else the active row for the draft's first save, else nothing. A path is an array of object keys from the body root to the deepest changed key. An array value is one leaf, with no index.[]is the whole body of an item created as a draft. - Keep. No two entries share a path. A save replaces every entry at or under each path it changes, so the longest prefix answers for a key. The entries are kept until publish or discard. Both already remove the draft row:
promoteDraft(sys-metadata-repository.tsabout:1326) and the draft delete throughdiscardDraftInItsScope(protocol.tsabout:27096). - Stamp.
actoris the save's actor, the value its history event carries, ornull.atis the write's instant in the UTCZform (toISOString());z.string().datetime()refuses an offset. - Serve. On the strict draft read (
getMetaItemstate: 'draft'from a stored row) on the REST door and on the dispatcher. Absent on the active read, the cached branch and?preview=draft. Absent for a draft row whose saves recorded none, never[]. migrate-storedrewrites of draft rows record no path: they are not authoring.
The first deliverable: the save request declares its surface
No door can tell a Studio save from any other HTTP caller today. SaveMetaItemRequestSchema.writeFace names the transport (package-duplicate | meta-envelope | meta-dispatch | external-import); the history source is an open code-path label; objectui's save is a plain PUT /meta/:type/:name?mode=draft. (Contract review 6253694122 ③ 1 measured this on PR #24383.)
- Declare one save-request input carrying the surface, on
SaveMetaItemRequestSchemaand its REST spelling. It uses the same closed set: move the inline enum into one exported schema that both the request anddraftChanges.byuse. An absent input stampsapi. The HTTP doors refusecopilot, which is an in-process tool's value only. - Risk, stated plainly: if no acceptable way exists for Studio's saves to declare
studio, the value has no writer. Narrowing the published closed set is then a BREAKING change (Clause-②: yes (narrowing)), and it is filed as one, never slipped in.
Downstream (not this card)
- Cloud (
service-ai-studio):stageDraft/applyDraft(packages/service-ai-studio/src/tools/metadata-tools.ts) passcopiloton their in-processsaveMetaItemcall once cloud's framework pin carries this card. This seat cannot reach objectstack-ai/cloud from its session; cloud's seat files that card at this card's landing. - objectui (the reader): 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. Studio's properties read
draftChangesoff the envelopereadDraftForSavealready returns. For an element of an array setting, they compare the draft array with the published one. Studio's own draft saves declarestudioon this card's input. The pointer is on objectui#12725.
Declared is enforced
- This card's PR makes the key live. It owes pins on the save door (each
byvalue, the replace-at-or-under rule, the first-save baseline, and the absence cases) and on both read doors. - Its changeset carries
Clause-②: yes (widening)for the new request input. A contract-tier review runs before the queue.
Dedupe
A semantic search of this repository's issues, "draft save records per-key provenance draftChanges writer copilot studio surface": 15 hits. #24298 is the spec card, and #24324 is the decision. The rest are closed draft-door cards (#20290, #24253, #23712, #23630, #23495, #23165, …), none of them the writer.
Dedupe words: draftChanges writer draft save per-key provenance; save request surface studio copilot api; metadata-protocol draft provenance record.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 18, 2026
Claim: PM loop round 10 · 2026-10-18T22:09Z · as filed by
domain:specseat 2 (p1,area:studio; ruling A item 3 on #24324, 6250704607). The draft save recordsdraftChanges, per-key provenance under the writer's contract inGetMetaItemResponseSchema.draftChanges's describe, and the strict draft read serves it. First the save request declares its authoring surface, so a Studio save, an in-process copilot save and any other caller are told apart.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-24395-draft-changes-writerWorktree:
objectstack-issue-24395Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface:
packages/metadata-protocol/src/protocol.ts:saveMetaItemwithmode: 'draft') reads the surface input and hands it to the write;getMetaItemwithstate: 'draft'from a stored row) servesdraftChanges. It is absent on the active read, the cached branch and?preview=draft.packages/metadata-protocol/src/sys-metadata-repository.ts:putwithstate: 'draft', insideresolveWriteHead's transaction, compares the new body with the body it replaces: the draft row, else the active row, else nothing ([]). It keeps one entry per path, replacing every entry at or under a changed path;promoteDraftand the draft discard leave no entry behind;migrate-storedrewrite records no path.sys_metadatacolumn (packages/metadata-core/src/objects/sys-metadata.object.ts), the dev measures it on each driver's schema sync. ⛔ Never inside the stored body.domain:specon [PM seat] domain:spec — ⏳ vacant #6017 (and the filer, [PM seat] domain:spec · seat 2 — 🟢 zhuangjianguo · session_019oFY6Hi35J7atvgVFnmoaK #18549) before the queue:packages/spec/src/api/protocol.zod.ts.SaveMetaItemRequestSchema.copilot | studio | apimoves into one exported schema thatdraftChanges.byuses too. An absent input stampsapi.domain:cli([PM seat] domain:cli — 🟢 zhuangjianguo · session_01SYEJqDL3NXxrJRBVqaMu9T #6024, [PM seat] domain:cli · seat 2 — ⏳ vacant #22648) before the queue: the input's spelling on the two HTTP save doors,packages/rest/src/meta-save-request.ts(the REST write doors read through it) and the dispatcher's save inpackages/runtime/src/domains/meta.ts.copilot, which only an in-process tool may state.byvalue; the replace-at-or-under rule; the first-save baseline; every absence case; the HTTP doors' refusal ofcopilot; publish and discard leave nothing. One changeset per package touched,.changeset/24395-*.md.stageDraft/applyDraft, no new error code beyond the refusal's existing family.studio, stop and report. Narrowing the published set is BREAKING and is filed as its own card, never slipped in.Container & model:
M,mode:cloud(measured on a booted server: draft saves through the REST door, the dispatcher and the in-process call, then the strict draft read on both HTTP doors, and each absence case, before and after),model: TIER_DEFAULT(this run'sdispatch-gates.mjs --tier: "no path-derived mandate")Clause-②: yes (widening) (the save request gains an input and the closed set gains an exported name; owes one
CONTRACT_REVIEW_TIERreview, on the published-schema face, before the queue)Responsibility:
GetMetaItemResponseSchema.draftChangesis published with no writer, so Studio cannot mark a key the copilot drafted | the spec key landed (metadata-protocol / spec: a draft carries no per-field provenance (which surface changed which key, and when), so Studio cannot mark a copilot's draft change with the ruled 草稿 chip and 来源 line #24298,584b4225e1) with the writer's contract in its describe, and the draft save already reads the row it replaces inside one transaction | every Studio author reviewing a copilot draft before publish (ruling A item 3 on [Decision] The copilot's drafts: who sees a copilot change before publish, and where — preview-first (A), auto-publish additive edits (B), sticky author preview (C) or a dev environment (D); the frame #24298 waits on #24324; objectui#12725 item 3)Thread-read: none
Serial constraints cleared: open PRs' file lists and the
pm:dispatchedclaims were read at this claim.protocol.ts: three copy sentences in lock refusals, disjoint from the draft save and read. It lands first; this card's dev mergesmainbefore the PR opens.ObjectSchemadeclares an object's default row scope — the existing RLS policy shape minusobject— applied when no held permission set carries a policy for it (#24164 ruling E′) #24338,domain:spec) touchesprotocol.meta-types-degenerate-derivation.test.tsandobject-schema-fls-references.ts. This card names neither.protocol.zod.ts,sys-metadata-repository.ts,sys-metadata.object.ts,meta-save-request.tsorruntime/src/domains/meta.ts.domain:spec,pm:dispatched) holdsprotocol.ts'sduplicatePackageregion, which this card does not meet; whichever lands second mergesmain.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Dispatch ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-18T22:15Z:mode:cloud. The dev session issession_01Q3mCs3NX4GshNUCh7Fr1b9. It builds at the default tier on branchclaude/issue-24395-draft-changes-writer, under claim 6255766595.GetMetaItemResponseSchema.draftChanges's describe onmain(PR feat(spec): the draft read declares draftChanges, a pending draft's per-key provenance #24383,584b4225e1);domain:specforprotocol.zod.ts(6255777346 on [PM seat] domain:spec — ⏳ vacant #6017, 6255780671 on [PM seat] domain:spec · seat 2 — 🟢 zhuangjianguo · session_019oFY6Hi35J7atvgVFnmoaK #18549);domain:clifor the two HTTP save doors (6255785533 on [PM seat] domain:cli — 🟢 zhuangjianguo · session_01SYEJqDL3NXxrJRBVqaMu9T #6024, 6255791182 on [PM seat] domain:cli · seat 2 — ⏳ vacant #22648).studio, the dev reportsneeds_decisionand nothing narrows.CONTRACT_REVIEW_TIERreview of record on the published-schema face. Downstream at landing: cloud'scopilotstamp (pointer to cloud's seat) and Studio's reader (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 by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{ "issue": 24395, "status": "done", "branch": "claude/issue-24395-draft-changes-writer", "pr": "https://github.com/objectstack-ai/objectstack/pull/24418", "session": "session_01Q3mCs3NX4GshNUCh7Fr1b9", "premise_still_valid": true, "summary": "The draft save now records GetMetaItemResponseSchema.draftChanges and the strict draft read serves it. MetadataAuthoringSurfaceSchema (copilot | studio | api) is one exported schema shared by the new SaveMetaItemRequestSchema.surface and draftChanges[].by. Both HTTP save doors take ?surface=studio|api and refuse copilot, any other value and a repeat with 400 VALIDATION_ERROR; in-process, an unknown value is 400 INVALID_REQUEST, so no new code. SysMetadataRepository.put with state draft diffs the new body against the draft row, else the active row the publish replaces, else nothing ([]), inside resolveWriteHead's transaction. It keeps one entry per path (replace at or under) on a new nullable sys_metadata.draft_changes column, which publish and discard delete with the row. migrate-stored rewrites and active saves record nothing. The key is served only on the strict draft read from a stored row, never []. Claim line verified: the newest Claim (6255766595) names this branch, the card assignee was left untouched, and the PR assignee is os-project-manager.", "tests": "Final tree merged with origin/main at 78652c093 (carries #24401). Full suites through os-verify-lock, each VERDICT command-exit 0: metadata-protocol 247 files, 20686 tests; spec 762 files, 22361 tests (re-run after the alias fix); metadata-core; platform-objects; rest 303 files, 6365 tests; runtime 424 files, 7442 tests. New pins: draft-changes.test.ts (path grammar, keep rule, stored form); a [#24395] block in protocol.packageless-save-address.test.ts (each by value, actor and null, first-save baseline over active, replace across two saves, no-op save, absence on active, preview and pre-writer row, publish and discard leave nothing, migrate-stored records none, active records none, bad surface INVALID_REQUEST 400 with nothing written); a [#24395] block in rest-server-meta-door-closed-sets.test.ts (studio and api preserved as the save's surface, absent sends none; copilot, Studio, robot, empty and a repeat give 400 VALIDATION_ERROR with saveMetaItem never called); a [#24395] per-door block in runtime meta-save-preconditions-parity.test.ts over a real SQL store (RestServer and the hono catch-all: by per surface, the replace rule, absence on active and preview, copilot refused with nothing stored). Pins flipped: rest meta-item-query-decision and rest-server-meta-door-closed-sets (the save set gains surface), runtime meta-item-query-decision (Supported parameters: mode, package, surface), spec type-alias-convention.pin (788 to 789). Ablations via scripts/ablation-replace.mjs, each mutation proven landed on disk and restored with blob equal to HEAD and git diff HEAD empty; the subjects resolve through src, so there is no dist hop. (1) Recorder guard to if (false): 4 protocol pins red. (2) Keep rule to [...entries]: 4 red (3 unit, 1 protocol two-save). (3) Copilot refusal, both guards nested: exactly the REST copilot refusal pin red. Booted measurement M, before (d79dc4883c) and after, on the REST door via pnpm dev:crm --fresh --database file:persist.db (SQLite): before, every ?surface= was 400 unknown parameter and no draftChanges was served. After: by api, studio and api stamped; copilot, STUDIO and a repeat give 400; the first save over active gives [columns]; the second gives [columns:api, label:studio]; the third replaces columns at the path; absent on the active read, the cached branch (ETag) and ?preview=draft; the draft row saved before the writer, in the same DB booted on the new build, is absent and records from its next save; after REST publish and REST discard the rows are active-only and the next draft starts clean. The existing SQLite table gained draft_changes on boot with no manual step (pragma before: 24 columns without it; after: present, the legacy row NULL). Dispatcher via a real createHonoApp catch-all over the real protocol on SqliteWasm and on the memory driver, plus in-process saveMetaItem copilot: the same results (copilot stamped in-process; robot refused INVALID_REQUEST 400).", "gates": "Local, on final head a3925372f: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 116 families. All 116 ran, each exit captured before any pipe. --ran reconciliation: 116 derived, 116 run, 0 NOT-MEASURED (a derived zero, every line carrying an exit code), 115 exit 0. The one red is node scripts/check-empty-changeset.mjs --base origin/main, exit 1, BY DESIGN: the deliberate-correction class on the pending .changeset/24298-draft-key-provenance.md note, whose 'No server writes the key yet' becomes false with this PR. Its gate text says do not restore it; it needs a person's confirmation (open_questions 5). Six spec gates first exited 3 (PREREQUISITE NOT MET, spec dist predating the pin-test edit) and after pnpm --filter @objectstack/spec build each exited 0: check:api-surface, check:dual-source-exports, check:entry-nameability, check:exported-any, check:generated, check:skill-examples. pnpm check:spec-parsed-alias went red on an earlier head (ADR-0122: z.infer bare alias) and was fixed (z.input plus the isomorphic pin), now exit 0. pnpm --filter @objectstack/spec check:generated passes after a rebuild on the merged tree. Typecheck exit 0 for spec, metadata-core, metadata-protocol, rest, runtime and platform-objects on the merged tree (runtime first exited 1 on a test-file TS2345 in the new parity block, fixed). node scripts/check-i18n-bundles.mjs exit 0. CI on PR #24418 head a3925372f at report time: 38 check-runs, 7 success, 5 skipped, 26 in_progress, 0 failed. in_progress is the honest value; expect Lint and Repo Gates to carry the check-empty-changeset red above.", "mcp_calls": "0 — no MCP GitHub tool called (reads were unauthenticated REST GETs on the public repo: the issue, its comments, contract review 6253694122, PR #24418 and its check-runs).", "api_writes": "2 relay strokes plus 1 git push. (a) git push to origin claude/issue-24395-draft-changes-writer (the empty-branch probe first, then the commits). (b) fleet-write pr_create (POST /repos/objectstack-ai/objectstack/dispatches, which the relay executed as POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos/objectstack-ai/objectstack/issues/24418/assignees), request fw-20261019T010413Z-70ba92, run 39183619803 success, body read back identical (10117 bytes). (c) fleet-write comment (POST dispatches, then POST /repos/objectstack-ai/objectstack/issues/24395/comments): this os-dev-report.", "line_budget": "none set. Diff against the merge base, per file (+/-): .changeset/24298-draft-key-provenance.md +1/-1; .changeset/24395-draft-changes-surface.md +11/-0; .changeset/24395-draft-changes-writer.md +16/-0; .changeset/24395-http-save-surface.md +12/-0; content/docs/references/api/protocol.mdx +19/-5 (generated); content/docs/references/index.mdx +5/-5 (generated); packages/metadata-core/src/objects/sys-metadata.object.ts +18/-0; packages/metadata-protocol/src/draft-changes.ts +141/-0; packages/metadata-protocol/src/draft-changes.test.ts +130/-0; packages/metadata-protocol/src/protocol.ts +35/-1; packages/metadata-protocol/src/protocol.packageless-save-address.test.ts +120/-0; packages/metadata-protocol/src/sys-metadata-repository.ts +75/-1; packages/platform-objects/src/apps/translations/{en,es-ES,ja-JP,zh-CN}.objects.generated.ts +4/-0 each (generated; 3 locale values hand-translated); packages/rest/src/meta-item-query-decision.test.ts +7/-4; packages/rest/src/meta-item-read-gate.ts +8/-8; packages/rest/src/meta-item-write-routes.ts +6/-2; packages/rest/src/meta-save-request.ts +43/-2; packages/rest/src/rest-server-meta-door-closed-sets.test.ts +50/-2; packages/runtime/src/domains/meta-item-query-decision.test.ts +2/-2; packages/runtime/src/domains/meta-save-preconditions-parity.test.ts +78/-0; packages/runtime/src/domains/meta.ts +6/-2; packages/spec/{api-surface,declaration-map,export-origins}/api.json +2/-0 each, authorable-surface/api.json +1/-0, json-schema.manifest/api.json +1/-0 (all generated); packages/spec/src/api/protocol.zod.ts +50/-10; packages/spec/src/type-alias-convention.pin.test.ts +10/-3. Total 31 files, about +867/-48.", "deviations": [ "Outside the claim's file surface, landed where measurement put it: packages/rest/src/meta-item-read-gate.ts (META_ITEM_SAVE_PARAMS, both doors' closed save set) and packages/rest/src/meta-item-write-routes.ts (the REST door's arity list). Without them ?surface= is refused as an unknown parameter (measured before).", "New module packages/metadata-protocol/src/draft-changes.ts (and its test) holds the recorder's pure functions, which the repository's put and the protocol's strict draft read import.", "packages/platform-objects translation bundles regenerated for the new column (node scripts/check-i18n-bundles.mjs --write); the zh-CN, ja-JP and es-ES values are hand-translated (values only, allowed).", "packages/spec/src/type-alias-convention.pin.test.ts gains the ADR-0122 isomorphic pin for MetadataAuthoringSurfaceSchema, with the count moving 788 to 789; check:spec-parsed-alias required it.", "Edited another card's pending changeset, .changeset/24298-draft-key-provenance.md, rewriting one sentence ('No server writes the key yet'), which this PR makes false in the same release. check-empty-changeset stays red by design; this needs confirmation (open_questions 5).", "Protocol-level pins appended to protocol.packageless-save-address.test.ts to reuse its ledgered engine double rather than add a new double and a scripts/engine-double-contract.pinned.json entry (the os-dev rule prefers an existing double).", "Dispatch-prompt vs os-dev.md trailer: the harness attribution reminder asked for a model-named Co-Authored-By trailer; commits carry the model-free pair per AGENTS.md (the pre-push hook refuses a model identifier) and os-dev.md wins.", "The primary checkout /home/user/objectstack had this branch checked out at dispatch; I detached its HEAD (git checkout --detach) to create the worktree ../objectstack-issue-24395 on the branch. No file in it was edited.", "Not re-merged after the PR opened: origin/main gained ee882b845, 44a5011fe and 31e7d0ba2 (#24398 touches spec's sharded artifacts) after my one merge of 78652c093. Per §10 I merge again only on a GitHub-reported conflict or a main-caused red." ], "files_changed": [ ".changeset/24298-draft-key-provenance.md", ".changeset/24395-draft-changes-surface.md", ".changeset/24395-draft-changes-writer.md", ".changeset/24395-http-save-surface.md", "content/docs/references/api/protocol.mdx", "content/docs/references/index.mdx", "packages/metadata-core/src/objects/sys-metadata.object.ts", "packages/metadata-protocol/src/draft-changes.ts", "packages/metadata-protocol/src/draft-changes.test.ts", "packages/metadata-protocol/src/protocol.ts", "packages/metadata-protocol/src/protocol.packageless-save-address.test.ts", "packages/metadata-protocol/src/sys-metadata-repository.ts", "packages/platform-objects/src/apps/translations/en.objects.generated.ts", "packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts", "packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts", "packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts", "packages/rest/src/meta-item-query-decision.test.ts", "packages/rest/src/meta-item-read-gate.ts", "packages/rest/src/meta-item-write-routes.ts", "packages/rest/src/meta-save-request.ts", "packages/rest/src/rest-server-meta-door-closed-sets.test.ts", "packages/runtime/src/domains/meta-item-query-decision.test.ts", "packages/runtime/src/domains/meta-save-preconditions-parity.test.ts", "packages/runtime/src/domains/meta.ts", "packages/spec/api-surface/api.json", "packages/spec/authorable-surface/api.json", "packages/spec/declaration-map/api.json", "packages/spec/export-origins/api.json", "packages/spec/json-schema.manifest/api.json", "packages/spec/src/api/protocol.zod.ts", "packages/spec/src/type-alias-convention.pin.test.ts" ], "open_questions": [ { "question": "HTTP spelling of the surface input: query parameter or request header?", "options": [ "A ?surface=studio|api (chosen)", "B a custom request header (for example X-ObjectStack-Surface)" ], "recommendation": "A, measured on the four axes. (1) Business need: Studio's save is a plain PUT ?mode=draft (review 6253694122 ③ 1), so adding one query name is the smallest change to the real producer, and the doors already read save intent from the query (?mode=, ?force=, ?package=), while headers carry only HTTP preconditions (If-Match, If-None-Match). (2) Long-term: one closed query set, META_ITEM_SAVE_PARAMS, with recognition and arity already enforced; a custom header needs a new entry in DEFAULT_CORS_ALLOW_HEADERS (a closed list) for any cross-origin browser caller. (3) AI-proofing: an unknown query name or value is a located 400 on both doors today; a misspelt header is invisible. (4) No spread: no new gate, no new error code." }, { "question": "Storage home of the entries.", "options": [ "A a nullable sys_metadata.draft_changes JSON text column on the draft row (chosen)", "B a separate table keyed to the draft row", "C derived from sys_metadata_history at read time" ], "recommendation": "A. The entries live and die with the row: promoteDraft's drain and discardDraftInItsScope delete it, so no extra cleanup path exists to forget (B needs two more deletes plus orphan handling). The column is written in the same put transaction, and an absent column reads as not recorded. Measured: memory driver and SQLite (createHonoApp rig, and the booted dev server). An existing SQLite table gained the column on boot with no manual step. Not measured: Postgres, MySQL, Turso and Mongo; they use the same declared-field schema sync, and CI's matrix covers them. C would replay history per read and cannot tell surfaces apart, because history's source is a code-path label." }, { "question": "First-save baseline when the item has no STORED published row (an item only a code package ships): the claim says else nothing, so the save records [] (the whole body). For Studio this means a copilot draft of a packaged view marks every key as the copilot's.", "options": [ "A keep [] for no stored row (shipped; the describe now says 'the stored published row', so declared equals enforced)", "B compare the first draft save against the code layer's served body (the active read's item, read decorations stripped), passed into put by the protocol", "C leave it to the reader (Studio compares with the published body it already loads)" ], "recommendation": "B, as a follow-up card if Studio's reader shows over-marking on packaged items. On the business axis this is the case where an author most needs per-key marks; on the AI-proofing axis A over-attributes (every key reads copilot); B costs one served read per first draft save, outside the put transaction. A ships now as the claim specified, and the gap is stated in the describe and the PR." }, { "question": "surface on an ACTIVE save: accepted and validated, records nothing (shipped) or refused?", "options": [ "A accept and validate on every save, record only on drafts (chosen)", "B refuse surface on a non-draft save with 400" ], "recommendation": "A. Studio can send ?surface=studio on every save without branching, and an off-set value is still refused on every save. The spec describe says an active save records no per-key provenance, so nothing is claimed that is not delivered. B is stricter but makes the real producer conditional for no stored fact." }, { "question": "Confirm the correction to the pending 24298 release note (.changeset/24298-draft-key-provenance.md): 'No server writes the key yet; until one does, and for a draft row saved before it did...' became 'For a draft row whose saves recorded nothing (one saved before a server recorded the key)...'. check-empty-changeset stays red on this until a person decides.", "options": [ "A confirm the correction (the sentence is false once this PR ships in the same release)", "B restore the original from base and accept a false sentence in the CHANGELOG", "C restore it and amend the CHANGELOG entry after release in a docs-only PR" ], "recommendation": "A: both notes are pending for one release, and AGENTS.md's CHANGELOG row says a released error is fixed by amending that entry, which C defers to after the false text ships." } ], "out_of_scope_findings": [ "carrier: cloud's seat (stageDraft / applyDraft pass surface: 'copilot' on their in-process saveMetaItem once cloud's framework pin carries this) · noted, not filed (the card names it downstream).", "carrier: objectstack-ai/objectui#12725 item 3 (Studio's draft saves send ?surface=studio and its properties read draftChanges off readDraftForSave's envelope) · noted, not filed.", "Observation, not filed: the runtime dispatcher answers 405 'use GET, HEAD, PUT' for DELETE and POST /publish on a metadata item, so a host that mounts only the hono catch-all has no draft publish or discard door; measured in the dispatcher probe. This is dormant, with no named producer on that topology, so it is not a filing class. It is NOT in the PR's Acceptance notes (the body is written once); the seat may add it there. Dedupe words: dispatcher catch-all meta publish discard 405 draft door." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{ "issue": 24395, "status": "done", "branch": "claude/issue-24395-draft-changes-writer", "pr": "https://github.com/objectstack-ai/objectstack/pull/24418", "session": "session_01Q3mCs3NX4GshNUCh7Fr1b9", "premise_still_valid": true, "summary": "Fix round on PR #24418, requested by the seat (session_01JtSQQ3EWsbNPzyYMug1tXy). New head c105df36c; the previous head was a3925372f. origin/main was merged in at 3db85cc7f. Root causes: (1) Type Check · source gates: content/docs/references/index.mdx was stale against the merged tree; regenerated with check:generated --fix (fc7d18f35). (2) Test Core (5/6), packages/objectql: 56 failures in 8 suites, one root cause. Each suite registers a hand-written minimal sys_metadata object and drives a real ObjectQL engine through a draft save, so the engine's declared-field door refused the new column: 'Unknown field draft_changes on object sys_metadata'. This is the same fixture class the #8682 note in those files records. The fix declares draft_changes in each of the 8 stubs (c105df36c); the column and the writer are unchanged. (3) Test Core (1/6), packages/spec test:repo: not reproducible on the merged head (68 files, 1125 tests, exit 0). The CI annotations carry only the exit code; its root cause is NOT MEASURED. (4) Check Changeset: .changeset/24298-draft-key-provenance.md is left exactly as it was on the branch, as the seat ordered.", "tests": "On head c105df36c (main merged at 3db85cc7f), each through os-verify-lock with VERDICT command-exit 0. objectql full suite: 444 files, 8354 tests. Before the fix, the same suite reproduced CI's red: 56 failed in 8 files, the first error engine.ts undeclaredWriteFieldErrors 'Unknown field draft_changes'. The 8 fixed files alone: 106 passed. Every other real-engine sys_metadata stub repo-wide, found by git grep (27 files), ran green: metadata database-loader (86), driver-sql overlay-index-drift (21), plugin-audit stored-metadata body and hash migrations (24), runtime organization-ownership-boot-guard (17), and four cli stub files (102, 1 skipped). spec test:repo: 68 files, 1125 tests. Full suites re-run on the merged tree: metadata-protocol 20686, rest 6365, runtime 7442, spec 764 files 22376, metadata-core, platform-objects. Typecheck exit 0 for objectql, spec, metadata-core, metadata-protocol, rest, runtime and platform-objects. pnpm --filter @objectstack/spec check:generated is green after the regeneration. No behaviour changed this round, so no new ablation; the three from round one stand.", "gates": "Local, on c105df36c: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 118 families, all 118 run with exit codes captured before any pipe. --ran: 118 derived, 118 run, 0 NOT-MEASURED (a derived zero). 117 exit 0. The one exit 1 is node scripts/check-empty-changeset.mjs --base origin/main, item 4, routed to a person and left as is. CI on c105df36c had not been read at report time; it was red on a3925372f with Test Core (1/6), Test Core (5/6), Type Check · source gates, Check Changeset and their two rollups.", "mcp_calls": "0 — none. Reads were unauthenticated REST GETs: PR #24418, its check-runs and two check-run annotations.", "api_writes": "Round two: 1 git push, plus 1 relay comment stroke (POST dispatches executed as POST /repos/objectstack-ai/objectstack/issues/24395/comments: this follow-up os-dev-report). The git push carried 3db85cc7f, fc7d18f35 and c105df36c.", "line_budget": "none set. This round's diff on top of a3925372f, excluding what main brought: content/docs/references/index.mdx +5/-5 (generated); packages/objectql/src/{default-record-page-save-door,protocol-publish-canonical-fold,protocol-recorded-by-null,publish-meta-response-conformance,publish-package-drafts-backfill,publish-package-drafts-response-conformance,save-meta-response-conformance}.test.ts +2/-0 each; packages/objectql/src/protocol-revert-org-scope.test.ts +2/-0.", "deviations": [ "packages/objectql test stubs are outside the claim's file surface. They are pins of the new sys_metadata column (the claim's 'their pins'), fixed at the fixture, the same class the files' own #8682 note records. No production code changed.", "The primary checkout was detached again to re-create the worktree, then restored to the branch at cleanup." ], "files_changed": [ "content/docs/references/index.mdx", "packages/objectql/src/default-record-page-save-door.test.ts", "packages/objectql/src/protocol-publish-canonical-fold.test.ts", "packages/objectql/src/protocol-recorded-by-null.test.ts", "packages/objectql/src/protocol-revert-org-scope.test.ts", "packages/objectql/src/publish-meta-response-conformance.test.ts", "packages/objectql/src/publish-package-drafts-backfill.test.ts", "packages/objectql/src/publish-package-drafts-response-conformance.test.ts", "packages/objectql/src/save-meta-response-conformance.test.ts" ], "open_questions": [ { "question": "Test Core (1/6) packages/spec test:repo exited 1 on a3925372f but passes on the merged head c105df36c; was it the stale reference index (now regenerated) or something CI-only?", "options": [ "A read the next CI run on c105df36c", "B fetch the a3925372f job log with credentials (not retrievable here)" ], "recommendation": "A: the local run on the merged head is green, and the CI run on c105df36c is the authoritative reading." } ], "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{ "issue": 24395, "status": "done", "branch": "claude/issue-24395-draft-changes-writer", "pr": "https://github.com/objectstack-ai/objectstack/pull/24418", "session": "session_01Q3mCs3NX4GshNUCh7Fr1b9", "premise_still_valid": true, "summary": "Fix round 2 on PR #24418. New head 49a8e27c6. Merge commit ede240123 merges origin/main at 95246668d into c105df36c (merge commit, no rebase, no force-push). The merge stopped on no hand-written file; the os-regen driver deferred one artefact, packages/spec/authorable-surface/api.json. The build and gen:schema regenerated it, and its diff against main is this PR's one line, api/SaveMetaItemRequest:surface. The released note .changeset/pre/24298-draft-key-provenance.md was restored to main's content exactly (git restore --source=95246668d, then verified with git diff --quiet against 95246668d), so the PR's net diff against main touches no 24298 file (git diff --name-only 95246668d HEAD | grep -c 24298 gives 0). The correction now lives in this PR's own spec changeset, .changeset/24395-draft-changes-surface.md, on its 'The writer exists' bullet, verbatim: `18.0.0-next.4`'s note said no server writes `draftChanges` yet; from this release the draft save writes it. Clause-② stays yes (widening). Nothing else changed in the diff. GitHub's mergeable_state moved from dirty to blocked (pending checks and review).", "tests": "No behaviour changed this round, so no test re-run beyond the gates and typechecks below; the round-one and round-two suites and the three ablations stand. The full workspace build on the merged tree, pnpm build --concurrency=2 through os-verify-lock, gave VERDICT command-exit 0.", "gates": "On head 49a8e27c6, each exit captured before any pipe. pnpm --filter @objectstack/spec gen:schema: exit 0. pnpm --filter @objectstack/spec check:generated: exit 0 ('All 14 generated artifacts are up to date'). node scripts/check-empty-changeset.mjs --base origin/main: exit 0 ('No changeset from the merge base modified or deleted by this diff'). Typecheck exit 0 for spec, metadata-core, metadata-protocol, rest, runtime, platform-objects and objectql. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack with no paths derived 118 families on 49a8e27c6. I re-ran ALL 118 rather than a subset, so nothing stands on the c105df36c run: 118 run, 118 exit 0. --ran gives 118 derived, 118 run, 0 NOT-MEASURED (a derived zero). CI on 49a8e27c6 had not been read at report time.", "mcp_calls": "0 — none. Reads were unauthenticated REST GETs of PR #24418.", "api_writes": "Round three: 1 git push (ede240123 and 49a8e27c6 to claude/issue-24395-draft-changes-writer), plus 1 relay comment stroke (POST dispatches executed as POST /repos/objectstack-ai/objectstack/issues/24395/comments: this os-dev-report).", "line_budget": "none set. This round, on top of c105df36c: .changeset/24395-draft-changes-surface.md +1/-1 (the sentence); .changeset/pre/24298-draft-key-provenance.md restored to main (net 0 against main); packages/spec/authorable-surface/api.json regenerated (+1 against main). The whole PR's net diff against 95246668d is 38 files, +882/-47.", "deviations": [ "The primary checkout was detached again to re-create the worktree, then restored to the branch at cleanup." ], "files_changed": [ ".changeset/24395-draft-changes-surface.md", "packages/spec/authorable-surface/api.json (regenerated)", ".changeset/pre/24298-draft-key-provenance.md (restored to main; not in the net diff)" ], "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Landed ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-19T07:19ZCONTRACT_REVIEW_TIERreview of record 6259630019, PASS, at headc105df36c0. It carried to49a8e27c6a(delta 6260922663: the net patch is identical except for one changeset sentence). ACCEPT 6259637042. Not governed.gh-readonly-queue/main/pr-24418-*branch remains.origin/main2ec6827962is onorigin/main. It carries PR #24418's 38 files, +882 / −47.completedthroughFixes #24395;pm:dispatchedis cleared.What
mainnow holds:MetadataAuthoringSurfaceSchema(copilot | studio | api) is exported from@objectstack/spec/api. Two keys read it:SaveMetaItemRequestSchema.surfaceandGetMetaItemResponseSchema.draftChanges[].by.put's transaction,saveMetaItem({ mode: 'draft', surface })compares the new body with the draft row, else with the stored published row, else with nothing ([]).{ by: surface ?? 'api', actor, at }.sys_metadata.draft_changescolumn. Schema sync adds it to an existing table.?preview=draft, and a draft row whose saves recorded none (never[]).?surface=studio|apithrough one closed query set.copilotand any other value answer400 VALIDATION_ERROR; only an in-process tool may statecopilot.18.0.0-next.4published metadata-protocol / spec: a draft carries no per-field provenance (which surface changed which key, and when), so Studio cannot mark a copilot's draft change with the ruled 草稿 chip and 来源 line #24298's note while no server wrote the key. This PR's own spec changeset says that from this release the draft save writes it.Downstream: cloud pointer 6261860060 on #24145 (
stageDraft/applyDraftstatesurface: 'copilot'), and pointer objectstack-ai/objectui#12725 (comment 6261868024) covers item 3 (Studio sends?surface=studioand readsdraftChanges).The dev session
session_01Q3mCs3NX4GshNUCh7Fr1b9is archived.Generated by Claude Code