Skip to content

metadata-protocol: publishPackageDrafts takes an opt-in per-field backfill of existing rows with the new default, so a copilot draft that adds a field can fill existing records on publish (ruling A on cloud#2924) #24253

Closed

Description

@objectstack-fleet

Filed by the director seat (summon #43 continuation 4, session_01QJC7i981TSs9L1CGJK59Ax) on the maintainer's ruling A on objectstack-ai/cloud#2924 (batch #362 item 1, 「其他同意」; the ruling record is that card's Ruled: line) under the maintainer's standing 口径 of 2026-10-18 「平台相关的功能,应该尽量先改平台」. ⛔ Not a claim. Filing gate ①: a measured gap with a named site and two consumers.

What is measured (objectstack origin/main cbfa5e43354f)

  • packages/metadata-protocol/src/protocol.ts:25752 publishPackageDrafts(request: { packageId; actor?; message?; aiModel? }) — publishing a draft neither backfills existing rows for a new field with a defaultValue nor emits an event a host could hook.
  • Consumers: the Studio copilot's draft posture (cloud#2924: a change to a live app with records is saved as a draft; the applied card says 「已有 N 条记录的 X 为空」, and the ruled design wanted 「发布时可选补 0」); cloud#2656 / cloud#1586 (closed): AI-added booleans on a live table must be backfilled or a = false filter matches nothing — cloud fixed it on its in-place publish path only, which the draft path skips.

Scope

  1. publishPackageDrafts accepts an opt-in backfill: Array<{ object, field }> (or equivalent) naming new fields whose defaultValue is written into existing rows at publish, as the caller — object permissions and hooks fire as for a user edit; the response reports the count per field.
  2. Nothing is backfilled unless asked; a field without a defaultValue is refused in the request (loud, named).
  3. Clause-②: yes (widening) (a request key on a published protocol method); minor for @objectstack/metadata-protocol; the Studio 变更 drawer's 补默认值 (objectui card filed in the same act) is the first UI caller; cloud's card copy gains 「可选补 0」 only after both land.

Done when

  • The option exists, is covered by a test that publishes a draft adding a boolean with defaultValue: false over 12 rows and reads 12 false; a request naming a field without a default is refused.
  • The objectui card can start.

Related: objectstack-ai/cloud#2924 (the ruling), objectstack-ai/cloud#2656 and #1586 (the boolean pins), objectstack-ai/objectui#12894 (the card rendering half).

Activity

  1. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 5 · 2026-10-18T05:08Z · on ruling A on objectstack-ai/cloud#2924 (batch #362 item 1), as this card's body carries it
    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-24253-publish-drafts-backfill
    Worktree: objectstack-issue-24253
    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-18T05:14Z: mode:cloud. The dev session is session_01DtBADZFvLxnZCFFDWCy87D, titled "⚡ engine #24253 publishPackageDrafts opt-in per-field backfill of existing rows". It builds at the default tier on branch claude/issue-24253-publish-drafts-backfill, claim 6245957326.

    • Carried: this card's Scope and Done when verbatim (ruling A on cloud#2924), and the maintainer's 「平台相关的功能,应该尽量先改平台」.
    • Measured first:
      • the order inside one publish (promotion, then the new column, then the backfill);
      • where a write as the caller is reachable (the protocol or the door);
      • the validation of every named field before any row is written;
      • which rows are written, and in pages.
    • Delivery: a draft PR saying Fixes #24253, then an os-dev-report comment here. objectui#12894 starts after it lands.

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24253,
      "status": "done",
      "branch": "claude/issue-24253-publish-drafts-backfill",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24269",
      "session": "session_01DtBADZFvLxnZCFFDWCy87D",
      "premise_still_valid": true,
      "summary": "publishPackageDrafts takes an opt-in backfill: [{ object, field }] plus the caller's context. All entries are judged before anything is promoted, and any failure refuses the request with one 400 VALIDATION_ERROR that names every failing entry at its backfill.N path. The checks: the object is drafted in the package, the draft declares the field, the served object lacks the field (new), and the field has a literal defaultValue. After the publish's side effects (registry write-through and schema sync), rows whose value is absent get that default 200 at a time through updateManyData as the caller, and backfillApplied { success, fields: [{ object, field, updated, error?, code? }] } is declared in spec. The door passes body.backfill through with _context.executionContext, and the client takes opts.backfill. Premise nuance, measured live on SQLite at fb6fb24: the SQL driver's ADD COLUMN carries the literal DEFAULT, so existing rows already read 12 false there and the backfill truthfully reports updated: 0. The gap is real on Turso remote, memory and MongoDB (cloud's case), so the card's 12-row pin runs on a real ObjectQL engine over a store with no column default. H2 premise falsified: the protocol's data methods already take the caller's context, so the write lives in the protocol rather than the door. Every published element and the protocol's own docblocks say so.",
      "tests": "head 08c87b8d0 (merged origin/main a422f04fe). New objectql/src/publish-package-drafts-backfill.test.ts, 10 cases: 12 rows read 12 false with updated 12; no key, no write; no-default refused 400 at backfill.0 with the draft still pending; four refusals in one answer; shape refusals; an existing value kept (11 of 12); paging (205 rows = 3 reads at limit 200); caller context at the CRUD seam and in the hook (never isSystem); a permission-denied caller gets updated 0 with code PERMISSION_DENIED and the publish kept; a clearing hook stops the field. Producer conformance +1 case (16/16). Door conformance +4 cases (10/10). Full suites, all exit 0: metadata-protocol 244 files / 20641 passed; client 52 / 671; runtime 416 / 6549; objectql 438 / 8255; spec 693 / 21002. Live HTTP through the door on a fresh dev:crm backend built from the branch (SQLite): a backfill of [flagged (no default), title (not new)] answered 400 VALIDATION_ERROR with details.issues backfill.0 and backfill.1, and the object was unchanged; a backfill of [done, score] answered 200 published, backfillApplied {success:true, fields:[{done, updated:0}, {score, updated:0}]}, and the rows read 12 false and 12 x 7 (column DEFAULT). Ablations via scripts/ablation-replace.mjs (wrap mode), with dist legs proven by ablation-dist-preflight (marker present; restore --absent and tree clean): A1 write gated off -> 6 red; A2 no-default guard off -> 1 red; A3 door drops context -> 1 red; A4 backfillApplied.fields[].updated retyped string -> producer 1 red, door 2 red. The first A4 attempt (key rename) was refused by the spec build's authorable-surface check, the marker never reached dist (preflight exit 1), and its green objectql leg is void and not counted.",
      "gates": "Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) on 08c87b8d0: 116 commands. Every line was run, with exit codes captured before any pipe: 116 x exit 0. --ran reconciliation: '116 derived, 116 run, 0 NOT-MEASURED, 0 UNRUN' (a DERIVED zero: all 116 recorded an exit code). The pre-merge run (114 commands on 98c882e20) had one real red, check:query-options-erasure (test surface 236 -> 237 from an 'as any' in the new test); fixed by typing the options and green since. check:engine-split-ratio --days 90 (exit 2) and check-plugin-teardown-shape --self-test (exit 3) first refused on the shallow clone; after git fetch --shallow-since=2026-07-13 origin main both exit 0. Per-package typecheck on 08c87b8d0: spec 0, metadata-protocol 0, runtime 0, client 0, objectql 0. objectql and runtime reach their tests through check:test-typecheck, since tsc --listFiles on the main tsconfig lists 0 test files. pnpm --filter @objectstack/spec check:generated 0, after --fix regenerated content/docs/references/api/protocol.mdx and the strictness-ledger api count 442 -> 444. Local-only check:dev-prereqs 0 and check:skill-frame-freshness 0. PR check-runs on 08c87b8d0 at report time, all success: Lint & Repo Gates, TypeScript Type Check, Build Core, Governed Surface Queue Guard, Check Changeset, Test Core 1/6, 5/6 and 6/6, every Lint and Type Check lane, Spec property liveness, Dogfood Regression Gate, and the three card/branch claim checks. In progress: Test Core 2/6, 3/6 and 4/6. Skipped by selector: Console Pin Gate, Temporal Conformance, Packed-tarball smoke, Dogfood Verify CLI. test:repo was not run locally; it belongs to CI.",
      "line_budget": "none set; diff +/- per file vs base fb6fb24: packages/metadata-protocol/src/protocol.ts +340/-0; packages/runtime/src/domains/packages.ts +8/-0; packages/client/src/index.ts +10/-1; packages/spec/src/api/protocol.zod.ts +38/-1; packages/spec/authorable-surface/api.json +1/-0 (generated); content/docs/references/api/protocol.mdx +8/-0 (generated); docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md +1/-1 (generated); packages/objectql/src/publish-package-drafts-backfill.test.ts +367/-0; packages/objectql/src/publish-package-drafts-response-conformance.test.ts +19/-0; packages/runtime/src/domains/packages-publish-drafts-response-conformance.test.ts +73/-3; .changeset/24253-metadata-protocol-publish-backfill.md +12; .changeset/24253-runtime-publish-drafts-backfill.md +10; .changeset/24253-client-publish-drafts-backfill.md +9; .changeset/24253-spec-publish-drafts-backfill-applied.md +9",
      "deviations": [
        "H2 (dispatch's mechanism hypothesis) falsified by measurement: callData already hands _context.executionContext to protocol.updateData/updateManyData/findData, so a caller-scoped engine write is reachable inside the protocol. The write therefore lives in publishPackageDrafts, and the door passes body.backfill together with _context.executionContext. The request carries a second new key, context, which is required whenever backfill is non-empty; without it the request is refused, never written as the system. Stated in the PR body.",
        "Beyond the claim's listed files: three generated artifacts that the required spec gates regenerated (packages/spec/authorable-surface/api.json, content/docs/references/api/protocol.mdx, and docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md 442 -> 444). The pins live in packages/objectql/src (the producer harness: the real engine with metadata-protocol) and in the existing door conformance file; no changeset for @objectstack/objectql, which is test-only.",
        "H3 widened by one refusal: a defaultValue that is a runtime token (NOW(), current_user) or an expression envelope is refused alongside no-default, because it resolves per insert and there is no single value to stamp. Settled by the four axes and recorded in open_questions.",
        "The card's 12-row pin runs on a real ObjectQL engine over an in-memory store with no column DEFAULT, not over HTTP on the dev backend. On the SQL driver the DDL default already fills the 12 rows (measured), so an HTTP 12-row assertion would pass vacuously. The door is pinned by the conformance file, and the live HTTP run is recorded in the PR body (refusal 400, receipt updated 0).",
        "To create the worktree, the shared primary checkout /home/user/objectstack (which had the branch checked out) was detached. It is back on the branch at the pushed head 08c87b8d0, and the worktree is removed. The clone was deepened (git fetch --shallow-since=2026-07-13 origin main) to measure two shallow-refusing gates.",
        "Not subscribed to PR activity: os-dev.md's dispatch contract overrides the platform's auto-watch posture."
      ],
      "files_changed": [
        "packages/metadata-protocol/src/protocol.ts",
        "packages/runtime/src/domains/packages.ts",
        "packages/client/src/index.ts",
        "packages/spec/src/api/protocol.zod.ts",
        "packages/spec/authorable-surface/api.json",
        "content/docs/references/api/protocol.mdx",
        "docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md",
        "packages/objectql/src/publish-package-drafts-backfill.test.ts",
        "packages/objectql/src/publish-package-drafts-response-conformance.test.ts",
        "packages/runtime/src/domains/packages-publish-drafts-response-conformance.test.ts",
        ".changeset/24253-metadata-protocol-publish-backfill.md",
        ".changeset/24253-runtime-publish-drafts-backfill.md",
        ".changeset/24253-client-publish-drafts-backfill.md",
        ".changeset/24253-spec-publish-drafts-backfill-applied.md"
      ],
      "mcp_calls": "0 — no MCP GitHub tool called, read or write",
      "api_writes": "2 relay strokes plus 1 git push. (1) pr_create via scripts/pm/fleet-write/dispatch.mjs: POST /repos/objectstack-ai/objectstack/dispatches (204), whose relay run 39105511466 executed POST /repos/objectstack-ai/objectstack/pulls (#24269, draft) and POST /repos/objectstack-ai/objectstack/issues/24269/assignees (os-project-manager). Read back as 13892 bytes sent, 13892 stored, identical. (2) The os-dev-report comment via the same tool: POST /repos/objectstack-ai/objectstack/dispatches -> POST /repos/objectstack-ai/objectstack/issues/24253/comments. git push to claude/issue-24253-publish-drafts-backfill is not a REST write. Reads used gh api (GET only).",
      "open_questions": [
        {
          "question": "H5: what does the response say when a page's write fails after the publish committed?",
          "options": ["A: stop that field at the first failing row; report updated-so-far plus error (record and reason) plus code; other fields still run; backfillApplied.success false; the publish is not undone", "B: stop every field at the first failure", "C: try to roll the promotion back"],
          "recommendation": "A, implemented. Business need: a 200 publish with a named partial is what the Studio card can render. Long term: the promotion has committed, the rows written stay written, and C is not measurable as safe. AI-proofing: loud and named, never silent. Startup scope: no new rollback machinery."
        },
        {
          "question": "Runtime-token and expression defaults (NOW(), current_user, { dialect, source }): backfill them or refuse?",
          "options": ["A: refuse in the request, named", "B: evaluate per row as the caller at publish time"],
          "recommendation": "A, implemented. Business need: the measured consumers (cloud#2656, the 可选补 0 card) are literals. AI-proofing: B would silently stamp the publish instant or the publisher's id into every existing row. Startup scope: the narrowest claim the runtime fully honours."
        },
        {
          "question": "Where do the caller-scoped writes run: in the protocol or at the door (H2)?",
          "options": ["A: in the protocol, with the door passing _context.executionContext as request.context", "B: the protocol validates, the door writes"],
          "recommendation": "A, implemented. Measured: the protocol's data methods already receive the caller's context from every door, so one implementation serves every caller. A context-less non-empty backfill is refused."
        },
        {
          "question": "\"As the caller\" means only rows the caller can read are filled (RLS/tenant scope); rows outside it stay empty and uncounted. Is that the intended reading?",
          "options": ["A: yes, the caller's visibility (implemented, the ruling's literal semantics)", "B: also report the remaining empty count read as the system"],
          "recommendation": "A. B would need a system read, which the dispatch forbids for this path; updated is documented as what was written, never that the table is complete."
        }
      ],
      "out_of_scope_findings": [
        "carrier: 承接者:无 · Turso remote transport (columnDeclarationSQL) emits no column DEFAULT on ADD COLUMN, while the SQL driver's createColumn does, so a new literal-default field reads empty on existing rows only on remote Turso (and on the schemaless memory/MongoDB drivers). Already recorded in remote-transport.ts's docblock and in turso-remote-not-null.test.ts; noted in the PR's Acceptance notes, not filed. Dedupe words: turso remote column default, ADD COLUMN DEFAULT asymmetry, applyDeclaredColumnDefault, columnDeclarationSQL"
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 5 · 2026-10-18T07:21Z · re-declared after os-dev-report 6247063019 (the file surface widens by gate-prescribed and test rows; the session, the branch and the dev stay)
    Session: session_01JtSQQ3EWsbNPzyYMug1tXy (the dispatching seat; the dev runs as its own cloud session session_01DtBADZFvLxnZCFFDWCy87D)
    Account: os-project-manager (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24253-publish-drafts-backfill (draft PR #24269)
    Worktree: objectstack-issue-24253
    Domain: domain:engine
    Seat: domain:engine#1 (seat post #6367)
    File surface: as claim 6245957326, plus:

    • The protocol's request also gains context (the caller's execution context). It is required whenever backfill is non-empty, and without it the request is refused. The door passes _context.executionContext through. Measured: the protocol's data methods already take the caller's context, so the write lives in publishPackageDrafts and not at the door.
    • Generated by the spec gates' own --fix, with no hand edits: packages/spec/authorable-surface/api.json, content/docs/references/api/protocol.mdx, and docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md (442 → 444).
    • The producer pin packages/objectql/src/publish-package-drafts-backfill.test.ts (new; the real engine with metadata-protocol) beside the producer conformance test. Test-only, so no changeset for @objectstack/objectql.
      Container & model: M, mode:cloud, model: TIER_DEFAULT (unchanged; one CONTRACT_REVIEW_TIER review before the queue)
      Clause-②: yes (widening)
      Responsibility: n/a — not a defect card
      Thread-read: 6247063019
      Serial constraints cleared: unchanged. PR fix(runtime): on the standalone server, an anonymous request whose JSON body cannot be parsed gets the domain's 401, not the transport's 400 #24250 has merged and is on this branch through its origin/main merge. No open PR touches the added rows (read at this claim).

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24269 comment 6247215006 · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-18T07:38Z. The CONTRACT_REVIEW_TIER record is 6247208221 (PASS, schema face). PR #24269 is marked ready and set to auto-merge through the queue, and it says Fixes #24253, so this card closes on merge. After that, objectui#12894 (the Studio drawer's 补默认值) can start; offer it only for a literal default.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-18T08:12Z

    Probe Reading
    Queue PR #24269 entered the merge queue at 2026-10-18T07:39Z, after ACCEPT 6247215006 and the CONTRACT_REVIEW_TIER record 6247208221 (PASS, schema face). It left the queue merged at 2026-10-18T08:11Z. After the merge, no gh-readonly-queue/main/pr-24269-* branch remains.
    origin/main 1e001c7df is on origin/main (git merge-base --is-ancestor: exit 0). It carries PR #24269's same 14 files, +905 / −6.
    Cards closed This card closed by the PR's Fixes, and pm:dispatched is cleared. Since the enqueue, #24266 (domain:spec) and #24073 (domain:skills) closed, each by its own lane.

    Now on main:

    • publishPackageDrafts takes an opt-in backfill: [{ object, field }], through POST /packages/:id/publish-drafts and packages.publishDrafts(id, { backfill }).
    • Every entry is judged before anything is promoted. An unknown object, a field the draft does not add, a field the served object already has, and a missing or non-literal defaultValue are each refused, by name, at backfill.N in one 400 VALIDATION_ERROR, and the draft stays pending.
    • After the publish, rows whose value is absent get the literal default as the caller, 200 rows a page. Permissions, RLS, FLS and hooks run as on the caller's own edit, and nothing is written as the system.
    • The response carries backfillApplied { success, fields: [{ object, field, updated, error?, code? }] }, declared in PublishPackageDraftsResponseSchema.
    • On the SQL driver ADD COLUMN already carries a literal DEFAULT, so updated reads 0 there. The backfill is what fills existing rows on Turso remote, memory and MongoDB.

    For objectui#12894 (the Studio drawer's 补默认值): the platform half has landed. Offer it only for a field whose defaultValue is a literal; a runtime token or an expression is refused. Render backfillApplied.fields[], including a partial updated with its error and code.

    The dev session session_01DtBADZFvLxnZCFFDWCy87D 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:enginepriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions