Skip to content

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

Closed

Description

@objectstack-fleet

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

What the writer must do (the contract in the spec describe on main)

  • Record. The draft save (saveMetaItem mode: 'draft' → SysMetadataRepository.put state: 'draft', inside resolveWriteHead'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.ts about :1326) and the draft delete through discardDraftInItsScope (protocol.ts about :27096).
  • Stamp. actor is the save's actor, the value its history event carries, or null. at is the write's instant in the UTC Z form (toISOString()); z.string().datetime() refuses an offset.
  • Serve. On the strict draft read (getMetaItem state: '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-stored rewrites 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 SaveMetaItemRequestSchema and its REST spelling. It uses the same closed set: move the inline enum into one exported schema that both the request and draftChanges.by use. An absent input stamps api. The HTTP doors refuse copilot, 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)

Declared is enforced

  • This card's PR makes the key live. It owes pins on the save door (each by value, 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

  1. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 10 · 2026-10-18T22:09Z · as filed by domain:spec seat 2 (p1, area:studio; ruling A item 3 on #24324, 6250704607). The draft save records draftChanges, per-key provenance under the writer's contract in GetMetaItemResponseSchema.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 as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24395-draft-changes-writer
    Worktree: objectstack-issue-24395
    Domain: domain:engine
    Seat: domain:engine#1 (seat post #6367)
    File surface:


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatch · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-18T22:15Z: mode:cloud. The dev session is session_01Q3mCs3NX4GshNUCh7Fr1b9. It builds at the default tier on branch claude/issue-24395-draft-changes-writer, under claim 6255766595.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  4. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  5. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  6. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-19T07:19Z

    Probe Reading
    Review CONTRACT_REVIEW_TIER review of record 6259630019, PASS, at head c105df36c0. It carried to 49a8e27c6a (delta 6260922663: the net patch is identical except for one changeset sentence). ACCEPT 6259637042. Not governed.
    Queue It entered the merge queue at 2026-10-19T06:46Z and left it merged at 2026-10-19T07:16Z. No gh-readonly-queue/main/pr-24418-* branch remains.
    origin/main 2ec6827962 is on origin/main. It carries PR #24418's 38 files, +882 / −47.
    Card Closed completed through Fixes #24395; pm:dispatched is cleared.

    What main now holds:

    • One closed set. MetadataAuthoringSurfaceSchema (copilot | studio | api) is exported from @objectstack/spec/api. Two keys read it: SaveMetaItemRequestSchema.surface and GetMetaItemResponseSchema.draftChanges[].by.
    • The draft save records per-key provenance. Inside put's transaction, saveMetaItem({ mode: 'draft', surface }) compares the new body with the draft row, else with the stored published row, else with nothing ([]).
      • It keeps one entry per path, and a save replaces every entry at or under each path it changes. An array is one leaf.
      • Each entry is stamped { by: surface ?? 'api', actor, at }.
      • The entries live on a nullable sys_metadata.draft_changes column. Schema sync adds it to an existing table.
      • Publish and discard delete the row, and the entries go with it.
    • Served on the strict draft read only. The entries are absent on the active read, the cached branch, ?preview=draft, and a draft row whose saves recorded none (never []).
    • Both HTTP doors. REST and the runtime dispatcher take ?surface=studio|api through one closed query set. copilot and any other value answer 400 VALIDATION_ERROR; only an in-process tool may state copilot.
    • The release notes. 18.0.0-next.4 published 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 / applyDraft state surface: 'copilot'), and pointer objectstack-ai/objectui#12725 (comment 6261868024) covers item 3 (Studio sends ?surface=studio and reads draftChanges).

    The dev session session_01Q3mCs3NX4GshNUCh7Fr1b9 is archived.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:studioChanging a running app without code — authoring, publish, docs and the portaldomain:engineenhancementNew feature or requestpriority:p1High: required for production / M2target:v18

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions