Repository navigation
feat(data-objectstack,app-shell): the Studio changes drawer offers to backfill a new field's literal default on publish (objectui#12929) - #13090
Conversation
… backfill a new field's literal default on publish (objectui#12929)
MetadataClient.publishPackageDrafts takes the protocol's opt-in backfill
(objectstack#24253), picked off @objectstack/client's publishDrafts
signature; without it the body stays '{}'. The review sheet offers an
unchecked choice under each added field of a published object whose
draft declares a literal default (the spec's discriminator), names the
object's record count when the adapter answers one, and passes the
checked fields to its host; Studio's publish sends them and reports the
per-field receipt.
Claude-Session: https://claude.ai/code/session_014MXhk1yjWh37d634hLy3p7
Co-authored-by: Claude <noreply@anthropic.com>
…, and declare its changeset (objectui#12929) Claude-Session: https://claude.ai/code/session_014MXhk1yjWh37d634hLy3p7 Co-authored-by: Claude <noreply@anthropic.com>
…before (objectui#12929) Claude-Session: https://claude.ai/code/session_014MXhk1yjWh37d634hLy3p7 Co-authored-by: Claude <noreply@anthropic.com>
… (objectui#12929) The en pack gains preview.changes.backfillOption and backfillOptionNoCount, so the published TranslationKeys type widens: an additive public-type change. Claude-Session: https://claude.ai/code/session_014MXhk1yjWh37d634hLy3p7 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Face: the claim's ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: Independence: the dispatching seat is also the reviewer, and the implementer was its VERDICT: PASS |
席位核对 · ACCEPT · objectui#12929 · PR #13090 ·
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Fixes #12929
Clause-②: yes
What changes
The Studio 变更 (Changes) drawer now offers a backfill choice under each field that a pending draft adds to an already-published object, when that field declares a literal
defaultValue. The choice reads 「补默认值(N 条已有记录)」 in zh and "Backfill default (N existing records)" in en, and it starts unchecked. Publish all passes the checked fields to the protocol's opt-inbackfill(objectstack-ai/objectstack#24253, carried by objectui's pinf4755a68a797). The publish toast then shows, for each field, the rows that the server'sbackfillAppliedreceipt says it wrote. When nothing is checked, the publish makes the same call it made before, with the body{}.packages/data-objectstack/src/metadata-client.ts:MetadataClient.publishPackageDrafts(packageId, options?).options.backfillgoes into the body asbackfillonly when it is given. Otherwise the body is{}, byte for byte.MetadataPublishPackageDraftsOptions, is picked from@objectstack/client'spackages.publishDraftssignature. The request has no spec schema of its own, so the client signature is where the protocol declares it.MetadataPublishPackageDraftsResult, which is the spec response by reference, and the client does not examine it.packages/app-shell/src/preview/DraftChangesPanel.tsx:backfillCandidatesapplies the batch door's own rule to the two bodies the drill-in already reads.fieldsmap, and the field'sdefaultValueis present and is a LITERAL according to the spec'sdiscriminateDefaultValueShape(the same function the door calls).adapter.find(object, { $top: 0 }), read as the signed-in caller, which is the same caller the door writes as. N is shown only when the answer carries a numerictotal. Otherwise the choice shows no number.backfill.onPublish({ backfill })with the checked fields, oronPublish()with no argument when none is checked.packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx:doPublish(options?)passes the sheet's options to the batch door only when the sheet gave some.updatedcount.error, and the toast becomes a warning. The publish itself still stands.@object-ui/i18npacks:preview.changes.backfillOption, a count family with every CLDR slot the pack's language selects, andpreview.changes.backfillOptionNoCount.engine.studio.backfillWrittenandengine.studio.backfillStopped..changeset/12929-publish-backfill.md: data-objectstack minor, app-shell minor, i18n minor.Published faces this widens (
Clause-②: yes, as the claim corrected it)@object-ui/data-objectstack:MetadataClient.publishPackageDraftsgains an optional second parameter.MetadataPublishPackageDraftsResult.dist/index.d.tsnow has a type-only reference toObjectStackClientfrom@objectstack/client, which is already a dependency.@object-ui/app-shell: no exported name changes type.DraftChangesPanelis not on the package entry, andStudioDesignSurface's props are unchanged. The widening is behaviour in the drawer.@object-ui/i18n: two new key families in every pack.Mechanism hypotheses, measured
StudioDesignSurfaceis the only host that mountsDraftChangesPanelwithonPublish. ItsdoPublishreachesshellClient.publishPackageDrafts.DraftPreviewBarmounts the sheet read-only and publishes throughusePublishAllDrafts, the same as Home.usePublishAllDrafts,PackagesPage,AiChatPageandPendingDraftsBarare unchanged. They call with the package id only, so they send{}.publishPackageDrafts(PKG)stay green:heldPublish-11786,heldWidgetPublish-11910anditemDraftLine-12725.@objectstack/spec(pin build) declaresPublishPackageDraftsResponse.backfillApplied. The client pin parses its receipt fixture against the installedPublishPackageDraftsResponseSchema, so CI re-reads this.1e001c7df, is18.0.0-next.5.git merge-base --is-ancestor 1e001c7dfagainst the@objectstack/spec@18.0.0-next.5tag commit exits 0. Against thenext.3tag commit it exits 1, with the control leg (next.2tag commit insidenext.3) exiting 0.next.4was never tagged, and the npmnextdist-tag reads18.0.0-next.5.discriminateDefaultValueShape.NOW(),current_user) and an expression envelope get no choice. Neither does a field with nodefaultValue(or anullone), nor a draft whosefieldsis not the map the door reads.adapter.find(object, { $top: 0 }).total.engine.countwith the caller's context for a zero-row page, so N is the records the caller can read. The backfill reads and writes as that same caller.total, the choice shows no number. The authoritative count is the receipt'supdated, and the toast reports it.Tests
Run from the repo root with
pnpm exec vitest run. The final union ran at headca83ad0:pnpm --filter @object-ui/data-objectstack testexit 0 (97 files, 1179 tests passed); 26 app-shell files exit 0 (126 tests passed): the 12DraftChangesPanel.*sheet files andDraftPreviewBar, 12 Studio publish-path files (publishCount-11807,publishPosture-12517,publishAdvisoryReachesAuthor,heldPublish-11786,heldWidgetPublish-11910,packageLessFlows-11553,copilotDoors-12894,homeLoop-12513,itemDraftLine-12725,manageSnapshotRefresh,changesCount-11787,publishReadOnlyReason-12303) and the new Studio pin;packages/i18n/exit 0 (84 files, 1319 passed, 13 skipped). The full app-shell suite belongs to CI.packages/data-objectstack/src/metadata-client.publishBackfill-12929.test.ts:{}, and so do{}andundefined;backfillis sent as given and in order;packages/app-shell/src/preview/__tests__/DraftChangesPanel.backfill-12929.test.tsx:= false;null-default fields get no choice, and neither does an unpublished object;onPublish()is called with no arguments; checked, it is called with{ backfill: [{ object, field }] }; checked then unchecked, with no arguments again;backfillCandidateskeeps falsy literals and refuses an array-shapedfields.packages/app-shell/src/views/studio-design/StudioDesignSurface.publishBackfill-12929.test.tsx(the card's Done-when, end to end through the real surface, real sheet and real client):{"backfill":[{"object":"b2r4_ticket","field":"done"}]}and the success toast's description carries the receipt's count, N = 12;{}and the toast carries no backfill line.Ablation
Run at
6348eef, with each fix committed first. Each mutation was made withablation-replace.mjs: the anchor hit once, landed on disk, and the restore was proven as blob == HEAD with an emptygit diff HEAD. The suites import@object-ui/data-objectstackthrough the vitest alias tosrc, so no build sits between the mutation and the run.body: JSON.stringify(...)was replaced withbody: '{}'. Turned red: 3 failed, 3 passed. The red tests were the client'ssends the named fields as the body's backfill, and Studio'scheckedanda field that stopped short.backfillCandidates. The check was deleted. Turned red: 6 failed, 4 passed. A token and an expression default each got a choice, so the five sheet cases that expect exactly one choice found three, andkeeps only a declared literal defaultfailed.void onPublish(). Turned red: 3 failed, 10 passed. The red tests were the sheet'schecked: Publish all passes { backfill: ... }, and Studio'scheckedandstopped short.Gates (local, captured before any pipe)
pnpm --filter @object-ui/data-objectstack lint: exit 0, no errors.type-check: exit 0, and--listFilesOnlyincludes the new test. Both ran at6348eef; data-objectstack has not changed since.pnpm --filter @object-ui/app-shell lint: exit 0, no errors. The diff adds onereact-refresh/only-export-componentswarning, on the exportedbackfillCandidates, the same kind the existingcomputeChangeDetailexport carries.pnpm --filter @object-ui/app-shell type-check: exit 0, both programs.--listFilesOnlyincludes both new tests. Ran atca83ad0.ca83ad0, each exit 0:check:i18n-keys,check:i18n-drift(4 pack keys and 2 table keys added, 0 en values changed),check:i18n-designer-parity;check:new-line-citations(0 new),check:control-bytes,check:spec-symbols;check:metadata-write-doors,check:phantom-deps,check:esm-specifiers;node scripts/check-changeset-{presence,no-major,fixed,claims,overwrite}.mjs;node scripts/check-vi-mock-{inherit,override-shape,specifiers}.mjs;node scripts/check-test-path-roots.mjsandnode scripts/check-type-check-coverage.mjs.check:i18n-dead-keys: exit 0. It is a report, and it lists none of the new keys. The packs have not changed since it ran.node scripts/check-governed-queue-guard.mjs --testover the 18 changed paths: NOT GOVERNED.check:spec-floors: not run, because no floor moves (see Deviations).These belong to CI: the full app-shell suite,
pnpm lintrepo-wide,check:eager-closure(it needs a console build), andcheck:published-dist. This PR adds a runtime import of@objectstack/spec/datato an eager-graph module. That subpath is already a runtime import of@object-ui/core(discriminateDefaultValueShape's siblingisRuntimeDefaultToken), and the file's own header records that the console'svendor-objectstackchunk claims every@objectstack/*module, so no eager-closure movement is expected. That is NOT MEASURED locally;check:eager-closurein CI is the reading.Deviations
@objectstack/specfloor of@object-ui/data-objectstack(^18.0.0-next.1) or of@object-ui/app-shell(^18.0.0-next.3) to R. objectui's AGENTS.md §9 「依赖与锁文件」 says these ranges matter only when objectui publishes, and says not to edit them to align with the pin (「⛔ 别为「对齐 pin」去改它们」).scripts/objectstack-floor.mjscomputes every floor from.objectstack-sha: the earliest spec release whose tag contains the pin.f4755a68a797contains1e001c7df. Thenext.5tag commit is an ancestor of the pin, and the pin is not innext.5. So the computed floor is a release afternext.5, and publishing waits for it (that script's exit 2) until it exists. Either way the floor admits nothing older than R.Acceptance notes
These were observed but not filed. None was reproduced through a public entry point, and none has a carrier.
On the SQL driver, the column DEFAULT fills existing rows at
ALTER TABLE … ADD COLUMN(metadata-protocol:publishPackageDraftstakes 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) objectstack#24253's own premise measurement). There the receipt'supdatedtruthfully reads 0 while the drawer named N existing records. The spec describes that 0 as a complete answer, and the toast shows it as sent. The backfill matters on drivers without a column DEFAULT (Turso remote, memory, MongoDB).ObjectStackAdapter.normalizeQueryResultanswerstotalastotal ?? count ?? records.length, andfindreturns{ data: [], total: 0 }for a resource remembered as missing (404). So a$top: 0count read could produce a confident 0 if a server omittedtotal($count=false, which the console does not send), or for a resource that 404ed earlier in the session. This is a read-only inference about pre-existing adapter code, not reproduced. The drawer shows the number only when it is numeric, which does not guard against this case.Adds no product-specific widget, dialog, hook or string (see AGENTS.md › Product-neutral renderer)
The pull request was created by the os-dev run of PM session
https://claude.ai/code/session_014MXhk1yjWh37d634hLy3p7.Generated by Claude Code