fix(data-objectstack): declare the batch write-warning's unattributed-strip placeholder instead of a bare empty string - #7176

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7160-batch-warning-empty-object
Sep 1, 2026
Merged

fix(data-objectstack): declare the batch write-warning's unattributed-strip placeholder instead of a bare empty string#7176
os-warren merged 2 commits into
mainfrom
claude/issue-7160-batch-warning-empty-object

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7160

Union of gates below re-run on the final commit f05427d91.

The defect

ObjectStackAdapter.notifyBatchDroppedFields resolves the object a cross-object write-strip is about from the wire entry's own object, else from the operation its index addresses, else ''. That last arm is a value satisfying the spec's required object: string while naming no object at all, and since PR objectui#7159 unified the two expressions it lands on both the notice's object and the event's resource.

The gating measurement — driven, not reasoned

Driven end to end through the real chain: ObjectStackAdapter.batchTransaction (stubbed client.data) into a real onWriteWarning subscriber, into app-shell's emitWriteWarning with the realt (a createI18n instance under I18nextProvider) and the realfieldLabel (useSafeFieldLabel, taken out of a rendered probe). Two-op batch: account create + invoice update.

caseeventgetObjectSchema calledwhat the user reads
index: 99, no wire object{"operation":"create","resource":"","droppedFields":[{"fields":["tax_rate"],"reason":"readonly_when","object":""}]}[]Saved — but some fields did not take effect / Not editable in this record's current state, so it did not take effect: tax_rate
no index at allidentical to the above[]identical
CONTROLindex: 1{"operation":"update","resource":"invoice","id":"inv1",...,"object":"invoice"}[["invoice"]]... did not take effect: Localized Tax rate

The control is alive and disagrees on every axis on the same instrument, so the empty call list is a reading and not a broken harness. index: -1, 1.5, 0.5 and NaN all behave as index: 99.

So the empty string is load-bearing, not inert.writeWarningToast gates label resolution on adapter && ev.resource; the empty resource is falsy, so the schema lookup is skipped and fields are named by their api key — the documented truthful fallback. The card recorded that clause as reasoned rather than executed; it is now executed, and it holds.

Reachability from a real response

Not reachable from a spec-conformant one. The spec's CrossObjectBatchDroppedFieldsSchema (@objectstack/spec@17.2.0) declares bothobject: z.string() and index: z.number() REQUIRED, and CrossObjectBatchResponseSchema documents results as index-aligned with the request's operations. Reaching the placeholder needs one entry that omits (or non-strings) objectand carries an index naming no operation — off-spec twice over.

Unlike objectui#6889's exotic case this is not structurally impossible: the payload arrives as parsed JSON and a non-conformant server can send it. Whether a deployed backend does is not answerable from this repo, and is reported as NOT MEASURED.

Which side of the refuse/tolerate line — and why it is neither

objectui#6889's repair is deliberately asymmetric with PR objectui#6884's reason arm:

  • a reason from the future is the producer running ahead of us: expected version skew, so it is tolerated on an explicit arm carrying the wire value verbatim;
  • a non-string fields element is off-spec input that would reach a consumer typed as a field name: refused here, fixed at the producer.

An unattributable strip is a third case, and the reason it is third is that there is no producer value to keep or drop. The response supplied nothing; the only question is what we write. So the answer is declare, not refuse and not widen:

Refuse was measured and rejected. Dropping the entry would replace a truthful, useful, user-visible warning — the save acknowledgement, the field list and the reason sentence all survive today — with silence for a strip the server really did report. That is objectui#3484's failure, and it is the stated reason neither object nor reason is gated on in the shared boundary.

Widen was rejected as the banned lenient fallback. Letting the notice say "no object" means making object optional on DroppedFieldsNotice, whose canonical arm is the spec's DroppedFieldsEvent (objectui#3160). That writes "servers may omit object" into our published client type in order to accommodate a producer violating two REQUIRED spec fields — precisely the consumer-side tolerance AGENTS.md #0.1 bans, fossilising the producer's bug into a second de-facto contract. The contract-first repair for an off-spec response is at the producer.

What this PR does

  • Replaces the bare '' with a module-private, documented UNATTRIBUTED_STRIP_OBJECT, carrying the reachability argument, the refuse/widen rejections, and the reason the value must stay falsy: a namespaced sentinel in the style of UNRECOGNIZED_DROP_REASON would be truthful but truthy, and would send the sole consumer to getObjectSchema('objectui:...'). It is deliberately not exported — no consumer should branch on its identity; the falsiness check is the whole correct handling.
  • Adds packages/data-objectstack/src/droppedFieldsUnattributed.boundary.test.ts, third sibling to the reason (objectui#4934) and shape (objectui#6889) boundary suites, pinning that the warning still reaches the subscriber, that the placeholder is falsy on both the notice and the event, that a wire object still wins, and that the single-record path cannot reach the placeholder at all. Every zero carries a live control.
  • Corrects a comment that contradicted the code it sat on.

The shared predicate was not extended, and did not need to be.isWireDroppedFieldsEntry answers "is this wire value an entry at all", and this defect is not about the wire's shape — the entry is well-formed. It is about a resolution step downstream of the gate. Gating object in the shared predicate would also have wrongly dropped the single-record case, where a truthful heal exists. No third predicate was spelled.

No published surface moves — proven

packages/data-objectstack/dist/index.d.ts built at the base sha and at this branch is byte-identical (sha256 4796dca7c1a6c74...), 389 declaration lines each, declaration-line diff empty.

Because the full files are identical, the usual "the full files do differ" control was unavailable, so a live control on the same instrument was run instead: appending one temporary exported const (marker count 0 to 1, blob 737d2dc to f0da4b0) and rebuilding produced a 6-line full-file diff and a 5-line declaration-line diff. The instrument detects a real surface change; "identical" is therefore a measurement. The control was removed and dist rebuilt back to the head hash.

Behaviour is unchanged — proven both ways

  • Ablation. Direction predicted before running: mutating the placeholder to a truthy value turns exactly the two tests that read it red, leaving the file's other 6 and the 3 sibling suites green. Mutation confirmed on disk by marker count (1 to 0 deleted / 0 to 1 injected) and blob hash (737d2dc37c to 3b658c6d6e). Result: Tests 2 failed | 35 passed (37) — the two predicted, and no others. Restore proven by state, not exit code: git diff HEAD empty, blob back to 737d2dc37c, mutant residue 0. No build step is in this loop — the suite imports ./index by relative path, so the subject is the source, not dist.
  • The new suite passes identically against the pre-change source (8/8 with the file swapped to the base blob 1b93160c80, marker count 0 confirming the swap landed). The pin therefore records existing behaviour rather than blessing new behaviour.

Scoped out, filed rather than folded

The same trigger fabricates a second value: with no operation to read, operation lands on 'create' (measured; the control reports 'update'). It is filed as objectui#7170 rather than repaired here — there is no correct value to fall back to, and WriteWarningEvent.operation is a REQUIRED union on a published type, so an honest answer there hits the same wall. This PR pins the current value beside the live control so whichever disposition triage picks arrives as a visible diff. objectui#7170 remains open and is not addressed by this branch.

Gates run, on f05427d91

gateverdict
pnpm exec vitest run packages/data-objectstack/Test Files 53 passed (53) / Tests 724 passed (724)
pnpm --filter @object-ui/data-objectstack type-checkexit 0 — and tsc --listFiles confirms the new test file is in the program (1 hit), so this is measured coverage, not an exclusion
pnpm --filter @object-ui/data-objectstack lint✖ 421 problems (0 errors, 421 warnings), exit 0 — the warnings are the any-typed harness the sibling boundary suites already use
check:changeset-presence✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) (empty frontmatter — declared as releasing nothing)
check:control-bytes✅ check-control-bytes: OK (scanned 5945 tracked text file(s); skipped 85 binary).
check:self-import✅ No package names itself inside its own src/.
check:spec-symbols✅ spec symbol derivation: 1332 files scanned against 4959 spec export names
check:vi-mock-specifiers✅ check-vi-mock-specifiers: OK
check:phantom-deps · check:esm-specifiers · check:entry-guard · check:vi-mock-inherit · check:side-effects-arrayexit 0
check:readme-exportsNOT MEASURED locally — the gate's own verdict is the population COLLAPSED -- this run proves nothing, with 353 complaints all of the form "type entry is not on disk -- run pnpm build first" and 0 naming data-objectstack. It needs a full monorepo build this worktree does not have; CI builds and owns it.

Assumption falsified: none of A2.1 to A2.4. f75810e7c is an ancestor of the base d8ec8d6d4 and the file carries the landed objectui#6889 work; the trigger is as stated; the object name is computed once; and nothing pinned the empty-string behaviour before this branch.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…-strip placeholder
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — domain:ui seat (session session_012wwHa4aaFybxXrfmfHioDM), reviewer of record

Verdict: ACCEPT. Undrafting and arming on green. One open question is ruled below; it is the PM's, not the implementer's.

⭐ The control I want on the record, because it is the hard case

Proving dist/index.d.ts is byte-identical at base and head is how this PR establishes it never entered CONTRACT_REVIEW_TIER. The problem is that a byte-identical comparison has no natural positive control — the usual "the full files DO differ while the declaration lines do not" reassurance is unavailable precisely when the answer is "identical", which is exactly when you most need to know the instrument works.

The implementer noticed that and built one: append a temporary exported const (marker 0 → 1, blob 737d2dcf0da4b0), observe a 6-line full-file and 5-line declaration-line diff, then remove it and rebuild back to the head hash. That converts "the diff was empty" into "the diff was empty and this instrument produces a non-empty diff when a change exists."

This is the same failure family as this seat's own instrument failure #11 — a control that printed empty and was read as a 0. And the report shows the lesson landed twice over: "the first attempt printed EMPTY marker counts from broken shell quoting and was discarded as void rather than read as zero." Discarding a void reading instead of scoring it is the whole discipline in one sentence.

On the disposition: REFUSE / TOLERATE / DECLARE

The card offered two dispositions and the answer was neither. That is the right kind of falsification — not "the premise is wrong" but "the option set was incomplete." The reasoning holds up:

  • There is no producer value to keep or drop. The response supplied nothing; the only question is what we write. That genuinely is a third case, and it is why refuse/tolerate both mis-frame it.
  • Refuse was measured and rejected, not waved off — dropping the entry trades a truthful, user-visible warning for objectui#3484's silence. The gating measurement drove the full chain with the real t and real fieldLabel and showed the user still reads the complete toast with fields named by api key, getObjectSchema never called (call list []), against a live control at index 1 resolving invoice / update / Localized Tax rate. That is a behavioural reading, not an argument.
  • Widen was rejected on principle rather than cost, which is the part I most want to endorse. Making object optional on DroppedFieldsNotice — whose canonical arm is the spec's DroppedFieldsEvent — would write "servers may omit object" into our published client type to accommodate a producer violating two REQUIRED spec fields. That is the lenient consumer-side fallback AGENTS.md #0.1 bans, and it would cost the canonical arm its identity as THE spec type, which objectui#3160 fought for. ⭐ Note it would also have crossed Clause ② and been unlandable at today's tier — but the report reaches the right answer without leaning on that, which is the stronger position.
  • Keeping the placeholder falsy is load-bearing and now documented: writeWarningToast gates on adapter && ev.resource, so a namespaced sentinel would be truthful but truthy and would send the consumer to getObjectSchema on a non-object. That is the kind of constraint that gets silently broken by a future "cleanup", which is exactly why declaring it beats leaving a bare ''.

Ruling on the open question: A — leave as landed

Does any deployed backend actually return a batch droppedFields entry that omits object and carries an index naming no operation?NOT MEASURED, and correctly reported as such rather than guessed.

Option A stands, for the implementer's reasons plus one of mine: B is unavailable at this seat's tier regardless of merit (Clause ②, and the CONTRACT_REVIEW_TIER quota is currently exhausted — it is what is holding #6896/PR #7111, #6881/PR #7066 and the retirement pile), and C deletes a warning that measurement shows is fully truthful today. A expands no published surface, adds no declared capability, and the declared-plus-pinned form closes the actual hazard — which was never the empty string, but that it was undeclared.

⚠️Carrying the implementer's warning into the record verbatim, because it is the part most likely to be lost:"Do not let a later triage pass infer the population from server code that is not in these repos." The placeholder is unreachable from a conformant response, but — unlike objectui#6889's case — it is not structurally impossible, because the payload arrives as parsed JSON. A future pass must not upgrade "the spec forbids it" into "no server does it" without reading a server. That distinction is the whole content of this NOT MEASURED, and it is not resolvable from either repo.

Also correct

  • Shared predicate not extended, and the report explains why that is right rather than treating non-reuse as a gap: isWireDroppedFieldsEntry answers "is this a wire entry at all", and this entry is well-formed — the defect is in a resolution step downstream of the gate. Gating object there would have wrongly dropped the single-record case, where a truthful heal exists. No third predicate was spelled, which is the outcome objectui#6889 was landed to protect.
  • Behaviour neutrality proven the right way round: the new suite passes 8/8 against the pre-change source (file swapped to base blob 1b93160c80, marker count 0 confirming the swap landed). The pin records existing behaviour instead of blessing new behaviour — and that is what makes it a pin rather than a spec.
  • the batch write-warning claims operation: 'create' for a strip whose index resolves to no operation #7170 filed, not folded. The fabricated operation: 'create' has no correct fallback value (WriteWarningEvent.operation is a REQUIRED published union), and the code comment above that line asserts the opposite of what the measurement shows. Pinning the current value beside the control so any future disposition lands as a visible diff is better than either fixing it here or leaving it unrecorded.
  • Ablation: direction predicted before running, mutation proven by marker count and blob hash, restore proven by state (git diff HEAD empty, blob back to 737d2dc37c, residue 0), Tests 2 failed | 35 passed (37) — the two predicted and no others, with the passing count asserted alongside the red. No build in the loop, and that was stated as measured (the suite imports ./index by relative path, so the subject is source) rather than assumed.

Nothing to change.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3154.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-Bor5MqcP.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)66.84KB18.86KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)249.91KB63.82KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)205.53KB55.50KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 09:30
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 899730eSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7160-batch-warning-empty-object branch September 1, 2026 09:44
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(data-objectstack): the batch write-warning names the object as an empty string when the response's index resolves to no operation

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(data-objectstack): declare the batch write-warning's unattributed-strip placeholder instead of a bare empty string - #7176

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7160-batch-warning-empty-object
Sep 1, 2026
Merged

fix(data-objectstack): declare the batch write-warning's unattributed-strip placeholder instead of a bare empty string#7176
os-warren merged 2 commits into
mainfrom
claude/issue-7160-batch-warning-empty-object

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7160

Union of gates below re-run on the final commit f05427d91.

The defect

ObjectStackAdapter.notifyBatchDroppedFields resolves the object a cross-object write-strip is about from the wire entry's own object, else from the operation its index addresses, else ''. That last arm is a value satisfying the spec's required object: string while naming no object at all, and since PR objectui#7159 unified the two expressions it lands on both the notice's object and the event's resource.

The gating measurement — driven, not reasoned

Driven end to end through the real chain: ObjectStackAdapter.batchTransaction (stubbed client.data) into a real onWriteWarning subscriber, into app-shell's emitWriteWarning with the realt (a createI18n instance under I18nextProvider) and the realfieldLabel (useSafeFieldLabel, taken out of a rendered probe). Two-op batch: account create + invoice update.

caseeventgetObjectSchema calledwhat the user reads
index: 99, no wire object{"operation":"create","resource":"","droppedFields":[{"fields":["tax_rate"],"reason":"readonly_when","object":""}]}[]Saved — but some fields did not take effect / Not editable in this record's current state, so it did not take effect: tax_rate
no index at allidentical to the above[]identical
CONTROLindex: 1{"operation":"update","resource":"invoice","id":"inv1",...,"object":"invoice"}[["invoice"]]... did not take effect: Localized Tax rate

The control is alive and disagrees on every axis on the same instrument, so the empty call list is a reading and not a broken harness. index: -1, 1.5, 0.5 and NaN all behave as index: 99.

So the empty string is load-bearing, not inert.writeWarningToast gates label resolution on adapter && ev.resource; the empty resource is falsy, so the schema lookup is skipped and fields are named by their api key — the documented truthful fallback. The card recorded that clause as reasoned rather than executed; it is now executed, and it holds.

Reachability from a real response

Not reachable from a spec-conformant one. The spec's CrossObjectBatchDroppedFieldsSchema (@objectstack/spec@17.2.0) declares bothobject: z.string() and index: z.number() REQUIRED, and CrossObjectBatchResponseSchema documents results as index-aligned with the request's operations. Reaching the placeholder needs one entry that omits (or non-strings) objectand carries an index naming no operation — off-spec twice over.

Unlike objectui#6889's exotic case this is not structurally impossible: the payload arrives as parsed JSON and a non-conformant server can send it. Whether a deployed backend does is not answerable from this repo, and is reported as NOT MEASURED.

Which side of the refuse/tolerate line — and why it is neither

objectui#6889's repair is deliberately asymmetric with PR objectui#6884's reason arm:

  • a reason from the future is the producer running ahead of us: expected version skew, so it is tolerated on an explicit arm carrying the wire value verbatim;
  • a non-string fields element is off-spec input that would reach a consumer typed as a field name: refused here, fixed at the producer.

An unattributable strip is a third case, and the reason it is third is that there is no producer value to keep or drop. The response supplied nothing; the only question is what we write. So the answer is declare, not refuse and not widen:

Refuse was measured and rejected. Dropping the entry would replace a truthful, useful, user-visible warning — the save acknowledgement, the field list and the reason sentence all survive today — with silence for a strip the server really did report. That is objectui#3484's failure, and it is the stated reason neither object nor reason is gated on in the shared boundary.

Widen was rejected as the banned lenient fallback. Letting the notice say "no object" means making object optional on DroppedFieldsNotice, whose canonical arm is the spec's DroppedFieldsEvent (objectui#3160). That writes "servers may omit object" into our published client type in order to accommodate a producer violating two REQUIRED spec fields — precisely the consumer-side tolerance AGENTS.md #0.1 bans, fossilising the producer's bug into a second de-facto contract. The contract-first repair for an off-spec response is at the producer.

What this PR does

  • Replaces the bare '' with a module-private, documented UNATTRIBUTED_STRIP_OBJECT, carrying the reachability argument, the refuse/widen rejections, and the reason the value must stay falsy: a namespaced sentinel in the style of UNRECOGNIZED_DROP_REASON would be truthful but truthy, and would send the sole consumer to getObjectSchema('objectui:...'). It is deliberately not exported — no consumer should branch on its identity; the falsiness check is the whole correct handling.
  • Adds packages/data-objectstack/src/droppedFieldsUnattributed.boundary.test.ts, third sibling to the reason (objectui#4934) and shape (objectui#6889) boundary suites, pinning that the warning still reaches the subscriber, that the placeholder is falsy on both the notice and the event, that a wire object still wins, and that the single-record path cannot reach the placeholder at all. Every zero carries a live control.
  • Corrects a comment that contradicted the code it sat on.

The shared predicate was not extended, and did not need to be.isWireDroppedFieldsEntry answers "is this wire value an entry at all", and this defect is not about the wire's shape — the entry is well-formed. It is about a resolution step downstream of the gate. Gating object in the shared predicate would also have wrongly dropped the single-record case, where a truthful heal exists. No third predicate was spelled.

No published surface moves — proven

packages/data-objectstack/dist/index.d.ts built at the base sha and at this branch is byte-identical (sha256 4796dca7c1a6c74...), 389 declaration lines each, declaration-line diff empty.

Because the full files are identical, the usual "the full files do differ" control was unavailable, so a live control on the same instrument was run instead: appending one temporary exported const (marker count 0 to 1, blob 737d2dc to f0da4b0) and rebuilding produced a 6-line full-file diff and a 5-line declaration-line diff. The instrument detects a real surface change; "identical" is therefore a measurement. The control was removed and dist rebuilt back to the head hash.

Behaviour is unchanged — proven both ways

  • Ablation. Direction predicted before running: mutating the placeholder to a truthy value turns exactly the two tests that read it red, leaving the file's other 6 and the 3 sibling suites green. Mutation confirmed on disk by marker count (1 to 0 deleted / 0 to 1 injected) and blob hash (737d2dc37c to 3b658c6d6e). Result: Tests 2 failed | 35 passed (37) — the two predicted, and no others. Restore proven by state, not exit code: git diff HEAD empty, blob back to 737d2dc37c, mutant residue 0. No build step is in this loop — the suite imports ./index by relative path, so the subject is the source, not dist.
  • The new suite passes identically against the pre-change source (8/8 with the file swapped to the base blob 1b93160c80, marker count 0 confirming the swap landed). The pin therefore records existing behaviour rather than blessing new behaviour.

Scoped out, filed rather than folded

The same trigger fabricates a second value: with no operation to read, operation lands on 'create' (measured; the control reports 'update'). It is filed as objectui#7170 rather than repaired here — there is no correct value to fall back to, and WriteWarningEvent.operation is a REQUIRED union on a published type, so an honest answer there hits the same wall. This PR pins the current value beside the live control so whichever disposition triage picks arrives as a visible diff. objectui#7170 remains open and is not addressed by this branch.

Gates run, on f05427d91

gateverdict
pnpm exec vitest run packages/data-objectstack/Test Files 53 passed (53) / Tests 724 passed (724)
pnpm --filter @object-ui/data-objectstack type-checkexit 0 — and tsc --listFiles confirms the new test file is in the program (1 hit), so this is measured coverage, not an exclusion
pnpm --filter @object-ui/data-objectstack lint✖ 421 problems (0 errors, 421 warnings), exit 0 — the warnings are the any-typed harness the sibling boundary suites already use
check:changeset-presence✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) (empty frontmatter — declared as releasing nothing)
check:control-bytes✅ check-control-bytes: OK (scanned 5945 tracked text file(s); skipped 85 binary).
check:self-import✅ No package names itself inside its own src/.
check:spec-symbols✅ spec symbol derivation: 1332 files scanned against 4959 spec export names
check:vi-mock-specifiers✅ check-vi-mock-specifiers: OK
check:phantom-deps · check:esm-specifiers · check:entry-guard · check:vi-mock-inherit · check:side-effects-arrayexit 0
check:readme-exportsNOT MEASURED locally — the gate's own verdict is the population COLLAPSED -- this run proves nothing, with 353 complaints all of the form "type entry is not on disk -- run pnpm build first" and 0 naming data-objectstack. It needs a full monorepo build this worktree does not have; CI builds and owns it.

Assumption falsified: none of A2.1 to A2.4. f75810e7c is an ancestor of the base d8ec8d6d4 and the file carries the landed objectui#6889 work; the trigger is as stated; the object name is computed once; and nothing pinned the empty-string behaviour before this branch.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…-strip placeholder
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — domain:ui seat (session session_012wwHa4aaFybxXrfmfHioDM), reviewer of record

Verdict: ACCEPT. Undrafting and arming on green. One open question is ruled below; it is the PM's, not the implementer's.

⭐ The control I want on the record, because it is the hard case

Proving dist/index.d.ts is byte-identical at base and head is how this PR establishes it never entered CONTRACT_REVIEW_TIER. The problem is that a byte-identical comparison has no natural positive control — the usual "the full files DO differ while the declaration lines do not" reassurance is unavailable precisely when the answer is "identical", which is exactly when you most need to know the instrument works.

The implementer noticed that and built one: append a temporary exported const (marker 0 → 1, blob 737d2dcf0da4b0), observe a 6-line full-file and 5-line declaration-line diff, then remove it and rebuild back to the head hash. That converts "the diff was empty" into "the diff was empty and this instrument produces a non-empty diff when a change exists."

This is the same failure family as this seat's own instrument failure #11 — a control that printed empty and was read as a 0. And the report shows the lesson landed twice over: "the first attempt printed EMPTY marker counts from broken shell quoting and was discarded as void rather than read as zero." Discarding a void reading instead of scoring it is the whole discipline in one sentence.

On the disposition: REFUSE / TOLERATE / DECLARE

The card offered two dispositions and the answer was neither. That is the right kind of falsification — not "the premise is wrong" but "the option set was incomplete." The reasoning holds up:

  • There is no producer value to keep or drop. The response supplied nothing; the only question is what we write. That genuinely is a third case, and it is why refuse/tolerate both mis-frame it.
  • Refuse was measured and rejected, not waved off — dropping the entry trades a truthful, user-visible warning for objectui#3484's silence. The gating measurement drove the full chain with the real t and real fieldLabel and showed the user still reads the complete toast with fields named by api key, getObjectSchema never called (call list []), against a live control at index 1 resolving invoice / update / Localized Tax rate. That is a behavioural reading, not an argument.
  • Widen was rejected on principle rather than cost, which is the part I most want to endorse. Making object optional on DroppedFieldsNotice — whose canonical arm is the spec's DroppedFieldsEvent — would write "servers may omit object" into our published client type to accommodate a producer violating two REQUIRED spec fields. That is the lenient consumer-side fallback AGENTS.md #0.1 bans, and it would cost the canonical arm its identity as THE spec type, which objectui#3160 fought for. ⭐ Note it would also have crossed Clause ② and been unlandable at today's tier — but the report reaches the right answer without leaning on that, which is the stronger position.
  • Keeping the placeholder falsy is load-bearing and now documented: writeWarningToast gates on adapter && ev.resource, so a namespaced sentinel would be truthful but truthy and would send the consumer to getObjectSchema on a non-object. That is the kind of constraint that gets silently broken by a future "cleanup", which is exactly why declaring it beats leaving a bare ''.

Ruling on the open question: A — leave as landed

Does any deployed backend actually return a batch droppedFields entry that omits object and carries an index naming no operation?NOT MEASURED, and correctly reported as such rather than guessed.

Option A stands, for the implementer's reasons plus one of mine: B is unavailable at this seat's tier regardless of merit (Clause ②, and the CONTRACT_REVIEW_TIER quota is currently exhausted — it is what is holding #6896/PR #7111, #6881/PR #7066 and the retirement pile), and C deletes a warning that measurement shows is fully truthful today. A expands no published surface, adds no declared capability, and the declared-plus-pinned form closes the actual hazard — which was never the empty string, but that it was undeclared.

⚠️Carrying the implementer's warning into the record verbatim, because it is the part most likely to be lost:"Do not let a later triage pass infer the population from server code that is not in these repos." The placeholder is unreachable from a conformant response, but — unlike objectui#6889's case — it is not structurally impossible, because the payload arrives as parsed JSON. A future pass must not upgrade "the spec forbids it" into "no server does it" without reading a server. That distinction is the whole content of this NOT MEASURED, and it is not resolvable from either repo.

Also correct

  • Shared predicate not extended, and the report explains why that is right rather than treating non-reuse as a gap: isWireDroppedFieldsEntry answers "is this a wire entry at all", and this entry is well-formed — the defect is in a resolution step downstream of the gate. Gating object there would have wrongly dropped the single-record case, where a truthful heal exists. No third predicate was spelled, which is the outcome objectui#6889 was landed to protect.
  • Behaviour neutrality proven the right way round: the new suite passes 8/8 against the pre-change source (file swapped to base blob 1b93160c80, marker count 0 confirming the swap landed). The pin records existing behaviour instead of blessing new behaviour — and that is what makes it a pin rather than a spec.
  • the batch write-warning claims operation: 'create' for a strip whose index resolves to no operation #7170 filed, not folded. The fabricated operation: 'create' has no correct fallback value (WriteWarningEvent.operation is a REQUIRED published union), and the code comment above that line asserts the opposite of what the measurement shows. Pinning the current value beside the control so any future disposition lands as a visible diff is better than either fixing it here or leaving it unrecorded.
  • Ablation: direction predicted before running, mutation proven by marker count and blob hash, restore proven by state (git diff HEAD empty, blob back to 737d2dc37c, residue 0), Tests 2 failed | 35 passed (37) — the two predicted and no others, with the passing count asserted alongside the red. No build in the loop, and that was stated as measured (the suite imports ./index by relative path, so the subject is source) rather than assumed.

Nothing to change.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3154.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-Bor5MqcP.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)66.84KB18.86KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)249.91KB63.82KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)205.53KB55.50KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 09:30
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 899730eSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7160-batch-warning-empty-object branch September 1, 2026 09:44
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(data-objectstack): the batch write-warning names the object as an empty string when the response's index resolves to no operation

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(data-objectstack): declare the batch write-warning's unattributed-strip placeholder instead of a bare empty string - #7176

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7160-batch-warning-empty-object
Sep 1, 2026
Merged

fix(data-objectstack): declare the batch write-warning's unattributed-strip placeholder instead of a bare empty string#7176
os-warren merged 2 commits into
mainfrom
claude/issue-7160-batch-warning-empty-object

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7160

Union of gates below re-run on the final commit f05427d91.

The defect

ObjectStackAdapter.notifyBatchDroppedFields resolves the object a cross-object write-strip is about from the wire entry's own object, else from the operation its index addresses, else ''. That last arm is a value satisfying the spec's required object: string while naming no object at all, and since PR objectui#7159 unified the two expressions it lands on both the notice's object and the event's resource.

The gating measurement — driven, not reasoned

Driven end to end through the real chain: ObjectStackAdapter.batchTransaction (stubbed client.data) into a real onWriteWarning subscriber, into app-shell's emitWriteWarning with the realt (a createI18n instance under I18nextProvider) and the realfieldLabel (useSafeFieldLabel, taken out of a rendered probe). Two-op batch: account create + invoice update.

caseeventgetObjectSchema calledwhat the user reads
index: 99, no wire object{"operation":"create","resource":"","droppedFields":[{"fields":["tax_rate"],"reason":"readonly_when","object":""}]}[]Saved — but some fields did not take effect / Not editable in this record's current state, so it did not take effect: tax_rate
no index at allidentical to the above[]identical
CONTROLindex: 1{"operation":"update","resource":"invoice","id":"inv1",...,"object":"invoice"}[["invoice"]]... did not take effect: Localized Tax rate

The control is alive and disagrees on every axis on the same instrument, so the empty call list is a reading and not a broken harness. index: -1, 1.5, 0.5 and NaN all behave as index: 99.

So the empty string is load-bearing, not inert.writeWarningToast gates label resolution on adapter && ev.resource; the empty resource is falsy, so the schema lookup is skipped and fields are named by their api key — the documented truthful fallback. The card recorded that clause as reasoned rather than executed; it is now executed, and it holds.

Reachability from a real response

Not reachable from a spec-conformant one. The spec's CrossObjectBatchDroppedFieldsSchema (@objectstack/spec@17.2.0) declares bothobject: z.string() and index: z.number() REQUIRED, and CrossObjectBatchResponseSchema documents results as index-aligned with the request's operations. Reaching the placeholder needs one entry that omits (or non-strings) objectand carries an index naming no operation — off-spec twice over.

Unlike objectui#6889's exotic case this is not structurally impossible: the payload arrives as parsed JSON and a non-conformant server can send it. Whether a deployed backend does is not answerable from this repo, and is reported as NOT MEASURED.

Which side of the refuse/tolerate line — and why it is neither

objectui#6889's repair is deliberately asymmetric with PR objectui#6884's reason arm:

  • a reason from the future is the producer running ahead of us: expected version skew, so it is tolerated on an explicit arm carrying the wire value verbatim;
  • a non-string fields element is off-spec input that would reach a consumer typed as a field name: refused here, fixed at the producer.

An unattributable strip is a third case, and the reason it is third is that there is no producer value to keep or drop. The response supplied nothing; the only question is what we write. So the answer is declare, not refuse and not widen:

Refuse was measured and rejected. Dropping the entry would replace a truthful, useful, user-visible warning — the save acknowledgement, the field list and the reason sentence all survive today — with silence for a strip the server really did report. That is objectui#3484's failure, and it is the stated reason neither object nor reason is gated on in the shared boundary.

Widen was rejected as the banned lenient fallback. Letting the notice say "no object" means making object optional on DroppedFieldsNotice, whose canonical arm is the spec's DroppedFieldsEvent (objectui#3160). That writes "servers may omit object" into our published client type in order to accommodate a producer violating two REQUIRED spec fields — precisely the consumer-side tolerance AGENTS.md #0.1 bans, fossilising the producer's bug into a second de-facto contract. The contract-first repair for an off-spec response is at the producer.

What this PR does

  • Replaces the bare '' with a module-private, documented UNATTRIBUTED_STRIP_OBJECT, carrying the reachability argument, the refuse/widen rejections, and the reason the value must stay falsy: a namespaced sentinel in the style of UNRECOGNIZED_DROP_REASON would be truthful but truthy, and would send the sole consumer to getObjectSchema('objectui:...'). It is deliberately not exported — no consumer should branch on its identity; the falsiness check is the whole correct handling.
  • Adds packages/data-objectstack/src/droppedFieldsUnattributed.boundary.test.ts, third sibling to the reason (objectui#4934) and shape (objectui#6889) boundary suites, pinning that the warning still reaches the subscriber, that the placeholder is falsy on both the notice and the event, that a wire object still wins, and that the single-record path cannot reach the placeholder at all. Every zero carries a live control.
  • Corrects a comment that contradicted the code it sat on.

The shared predicate was not extended, and did not need to be.isWireDroppedFieldsEntry answers "is this wire value an entry at all", and this defect is not about the wire's shape — the entry is well-formed. It is about a resolution step downstream of the gate. Gating object in the shared predicate would also have wrongly dropped the single-record case, where a truthful heal exists. No third predicate was spelled.

No published surface moves — proven

packages/data-objectstack/dist/index.d.ts built at the base sha and at this branch is byte-identical (sha256 4796dca7c1a6c74...), 389 declaration lines each, declaration-line diff empty.

Because the full files are identical, the usual "the full files do differ" control was unavailable, so a live control on the same instrument was run instead: appending one temporary exported const (marker count 0 to 1, blob 737d2dc to f0da4b0) and rebuilding produced a 6-line full-file diff and a 5-line declaration-line diff. The instrument detects a real surface change; "identical" is therefore a measurement. The control was removed and dist rebuilt back to the head hash.

Behaviour is unchanged — proven both ways

  • Ablation. Direction predicted before running: mutating the placeholder to a truthy value turns exactly the two tests that read it red, leaving the file's other 6 and the 3 sibling suites green. Mutation confirmed on disk by marker count (1 to 0 deleted / 0 to 1 injected) and blob hash (737d2dc37c to 3b658c6d6e). Result: Tests 2 failed | 35 passed (37) — the two predicted, and no others. Restore proven by state, not exit code: git diff HEAD empty, blob back to 737d2dc37c, mutant residue 0. No build step is in this loop — the suite imports ./index by relative path, so the subject is the source, not dist.
  • The new suite passes identically against the pre-change source (8/8 with the file swapped to the base blob 1b93160c80, marker count 0 confirming the swap landed). The pin therefore records existing behaviour rather than blessing new behaviour.

Scoped out, filed rather than folded

The same trigger fabricates a second value: with no operation to read, operation lands on 'create' (measured; the control reports 'update'). It is filed as objectui#7170 rather than repaired here — there is no correct value to fall back to, and WriteWarningEvent.operation is a REQUIRED union on a published type, so an honest answer there hits the same wall. This PR pins the current value beside the live control so whichever disposition triage picks arrives as a visible diff. objectui#7170 remains open and is not addressed by this branch.

Gates run, on f05427d91

gateverdict
pnpm exec vitest run packages/data-objectstack/Test Files 53 passed (53) / Tests 724 passed (724)
pnpm --filter @object-ui/data-objectstack type-checkexit 0 — and tsc --listFiles confirms the new test file is in the program (1 hit), so this is measured coverage, not an exclusion
pnpm --filter @object-ui/data-objectstack lint✖ 421 problems (0 errors, 421 warnings), exit 0 — the warnings are the any-typed harness the sibling boundary suites already use
check:changeset-presence✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) (empty frontmatter — declared as releasing nothing)
check:control-bytes✅ check-control-bytes: OK (scanned 5945 tracked text file(s); skipped 85 binary).
check:self-import✅ No package names itself inside its own src/.
check:spec-symbols✅ spec symbol derivation: 1332 files scanned against 4959 spec export names
check:vi-mock-specifiers✅ check-vi-mock-specifiers: OK
check:phantom-deps · check:esm-specifiers · check:entry-guard · check:vi-mock-inherit · check:side-effects-arrayexit 0
check:readme-exportsNOT MEASURED locally — the gate's own verdict is the population COLLAPSED -- this run proves nothing, with 353 complaints all of the form "type entry is not on disk -- run pnpm build first" and 0 naming data-objectstack. It needs a full monorepo build this worktree does not have; CI builds and owns it.

Assumption falsified: none of A2.1 to A2.4. f75810e7c is an ancestor of the base d8ec8d6d4 and the file carries the landed objectui#6889 work; the trigger is as stated; the object name is computed once; and nothing pinned the empty-string behaviour before this branch.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…-strip placeholder
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — domain:ui seat (session session_012wwHa4aaFybxXrfmfHioDM), reviewer of record

Verdict: ACCEPT. Undrafting and arming on green. One open question is ruled below; it is the PM's, not the implementer's.

⭐ The control I want on the record, because it is the hard case

Proving dist/index.d.ts is byte-identical at base and head is how this PR establishes it never entered CONTRACT_REVIEW_TIER. The problem is that a byte-identical comparison has no natural positive control — the usual "the full files DO differ while the declaration lines do not" reassurance is unavailable precisely when the answer is "identical", which is exactly when you most need to know the instrument works.

The implementer noticed that and built one: append a temporary exported const (marker 0 → 1, blob 737d2dcf0da4b0), observe a 6-line full-file and 5-line declaration-line diff, then remove it and rebuild back to the head hash. That converts "the diff was empty" into "the diff was empty and this instrument produces a non-empty diff when a change exists."

This is the same failure family as this seat's own instrument failure #11 — a control that printed empty and was read as a 0. And the report shows the lesson landed twice over: "the first attempt printed EMPTY marker counts from broken shell quoting and was discarded as void rather than read as zero." Discarding a void reading instead of scoring it is the whole discipline in one sentence.

On the disposition: REFUSE / TOLERATE / DECLARE

The card offered two dispositions and the answer was neither. That is the right kind of falsification — not "the premise is wrong" but "the option set was incomplete." The reasoning holds up:

  • There is no producer value to keep or drop. The response supplied nothing; the only question is what we write. That genuinely is a third case, and it is why refuse/tolerate both mis-frame it.
  • Refuse was measured and rejected, not waved off — dropping the entry trades a truthful, user-visible warning for objectui#3484's silence. The gating measurement drove the full chain with the real t and real fieldLabel and showed the user still reads the complete toast with fields named by api key, getObjectSchema never called (call list []), against a live control at index 1 resolving invoice / update / Localized Tax rate. That is a behavioural reading, not an argument.
  • Widen was rejected on principle rather than cost, which is the part I most want to endorse. Making object optional on DroppedFieldsNotice — whose canonical arm is the spec's DroppedFieldsEvent — would write "servers may omit object" into our published client type to accommodate a producer violating two REQUIRED spec fields. That is the lenient consumer-side fallback AGENTS.md #0.1 bans, and it would cost the canonical arm its identity as THE spec type, which objectui#3160 fought for. ⭐ Note it would also have crossed Clause ② and been unlandable at today's tier — but the report reaches the right answer without leaning on that, which is the stronger position.
  • Keeping the placeholder falsy is load-bearing and now documented: writeWarningToast gates on adapter && ev.resource, so a namespaced sentinel would be truthful but truthy and would send the consumer to getObjectSchema on a non-object. That is the kind of constraint that gets silently broken by a future "cleanup", which is exactly why declaring it beats leaving a bare ''.

Ruling on the open question: A — leave as landed

Does any deployed backend actually return a batch droppedFields entry that omits object and carries an index naming no operation?NOT MEASURED, and correctly reported as such rather than guessed.

Option A stands, for the implementer's reasons plus one of mine: B is unavailable at this seat's tier regardless of merit (Clause ②, and the CONTRACT_REVIEW_TIER quota is currently exhausted — it is what is holding #6896/PR #7111, #6881/PR #7066 and the retirement pile), and C deletes a warning that measurement shows is fully truthful today. A expands no published surface, adds no declared capability, and the declared-plus-pinned form closes the actual hazard — which was never the empty string, but that it was undeclared.

⚠️Carrying the implementer's warning into the record verbatim, because it is the part most likely to be lost:"Do not let a later triage pass infer the population from server code that is not in these repos." The placeholder is unreachable from a conformant response, but — unlike objectui#6889's case — it is not structurally impossible, because the payload arrives as parsed JSON. A future pass must not upgrade "the spec forbids it" into "no server does it" without reading a server. That distinction is the whole content of this NOT MEASURED, and it is not resolvable from either repo.

Also correct

  • Shared predicate not extended, and the report explains why that is right rather than treating non-reuse as a gap: isWireDroppedFieldsEntry answers "is this a wire entry at all", and this entry is well-formed — the defect is in a resolution step downstream of the gate. Gating object there would have wrongly dropped the single-record case, where a truthful heal exists. No third predicate was spelled, which is the outcome objectui#6889 was landed to protect.
  • Behaviour neutrality proven the right way round: the new suite passes 8/8 against the pre-change source (file swapped to base blob 1b93160c80, marker count 0 confirming the swap landed). The pin records existing behaviour instead of blessing new behaviour — and that is what makes it a pin rather than a spec.
  • the batch write-warning claims operation: 'create' for a strip whose index resolves to no operation #7170 filed, not folded. The fabricated operation: 'create' has no correct fallback value (WriteWarningEvent.operation is a REQUIRED published union), and the code comment above that line asserts the opposite of what the measurement shows. Pinning the current value beside the control so any future disposition lands as a visible diff is better than either fixing it here or leaving it unrecorded.
  • Ablation: direction predicted before running, mutation proven by marker count and blob hash, restore proven by state (git diff HEAD empty, blob back to 737d2dc37c, residue 0), Tests 2 failed | 35 passed (37) — the two predicted and no others, with the passing count asserted alongside the red. No build in the loop, and that was stated as measured (the suite imports ./index by relative path, so the subject is source) rather than assumed.

Nothing to change.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3154.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-Bor5MqcP.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)66.84KB18.86KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)249.91KB63.82KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)205.53KB55.50KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 09:30
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 899730eSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7160-batch-warning-empty-object branch September 1, 2026 09:44
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(data-objectstack): the batch write-warning names the object as an empty string when the response's index resolves to no operation

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(data-objectstack): declare the batch write-warning's unattributed-strip placeholder instead of a bare empty string - #7176

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7160-batch-warning-empty-object
Sep 1, 2026
Merged

fix(data-objectstack): declare the batch write-warning's unattributed-strip placeholder instead of a bare empty string#7176
os-warren merged 2 commits into
mainfrom
claude/issue-7160-batch-warning-empty-object

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7160

Union of gates below re-run on the final commit f05427d91.

The defect

ObjectStackAdapter.notifyBatchDroppedFields resolves the object a cross-object write-strip is about from the wire entry's own object, else from the operation its index addresses, else ''. That last arm is a value satisfying the spec's required object: string while naming no object at all, and since PR objectui#7159 unified the two expressions it lands on both the notice's object and the event's resource.

The gating measurement — driven, not reasoned

Driven end to end through the real chain: ObjectStackAdapter.batchTransaction (stubbed client.data) into a real onWriteWarning subscriber, into app-shell's emitWriteWarning with the realt (a createI18n instance under I18nextProvider) and the realfieldLabel (useSafeFieldLabel, taken out of a rendered probe). Two-op batch: account create + invoice update.

caseeventgetObjectSchema calledwhat the user reads
index: 99, no wire object{"operation":"create","resource":"","droppedFields":[{"fields":["tax_rate"],"reason":"readonly_when","object":""}]}[]Saved — but some fields did not take effect / Not editable in this record's current state, so it did not take effect: tax_rate
no index at allidentical to the above[]identical
CONTROLindex: 1{"operation":"update","resource":"invoice","id":"inv1",...,"object":"invoice"}[["invoice"]]... did not take effect: Localized Tax rate

The control is alive and disagrees on every axis on the same instrument, so the empty call list is a reading and not a broken harness. index: -1, 1.5, 0.5 and NaN all behave as index: 99.

So the empty string is load-bearing, not inert.writeWarningToast gates label resolution on adapter && ev.resource; the empty resource is falsy, so the schema lookup is skipped and fields are named by their api key — the documented truthful fallback. The card recorded that clause as reasoned rather than executed; it is now executed, and it holds.

Reachability from a real response

Not reachable from a spec-conformant one. The spec's CrossObjectBatchDroppedFieldsSchema (@objectstack/spec@17.2.0) declares bothobject: z.string() and index: z.number() REQUIRED, and CrossObjectBatchResponseSchema documents results as index-aligned with the request's operations. Reaching the placeholder needs one entry that omits (or non-strings) objectand carries an index naming no operation — off-spec twice over.

Unlike objectui#6889's exotic case this is not structurally impossible: the payload arrives as parsed JSON and a non-conformant server can send it. Whether a deployed backend does is not answerable from this repo, and is reported as NOT MEASURED.

Which side of the refuse/tolerate line — and why it is neither

objectui#6889's repair is deliberately asymmetric with PR objectui#6884's reason arm:

  • a reason from the future is the producer running ahead of us: expected version skew, so it is tolerated on an explicit arm carrying the wire value verbatim;
  • a non-string fields element is off-spec input that would reach a consumer typed as a field name: refused here, fixed at the producer.

An unattributable strip is a third case, and the reason it is third is that there is no producer value to keep or drop. The response supplied nothing; the only question is what we write. So the answer is declare, not refuse and not widen:

Refuse was measured and rejected. Dropping the entry would replace a truthful, useful, user-visible warning — the save acknowledgement, the field list and the reason sentence all survive today — with silence for a strip the server really did report. That is objectui#3484's failure, and it is the stated reason neither object nor reason is gated on in the shared boundary.

Widen was rejected as the banned lenient fallback. Letting the notice say "no object" means making object optional on DroppedFieldsNotice, whose canonical arm is the spec's DroppedFieldsEvent (objectui#3160). That writes "servers may omit object" into our published client type in order to accommodate a producer violating two REQUIRED spec fields — precisely the consumer-side tolerance AGENTS.md #0.1 bans, fossilising the producer's bug into a second de-facto contract. The contract-first repair for an off-spec response is at the producer.

What this PR does

  • Replaces the bare '' with a module-private, documented UNATTRIBUTED_STRIP_OBJECT, carrying the reachability argument, the refuse/widen rejections, and the reason the value must stay falsy: a namespaced sentinel in the style of UNRECOGNIZED_DROP_REASON would be truthful but truthy, and would send the sole consumer to getObjectSchema('objectui:...'). It is deliberately not exported — no consumer should branch on its identity; the falsiness check is the whole correct handling.
  • Adds packages/data-objectstack/src/droppedFieldsUnattributed.boundary.test.ts, third sibling to the reason (objectui#4934) and shape (objectui#6889) boundary suites, pinning that the warning still reaches the subscriber, that the placeholder is falsy on both the notice and the event, that a wire object still wins, and that the single-record path cannot reach the placeholder at all. Every zero carries a live control.
  • Corrects a comment that contradicted the code it sat on.

The shared predicate was not extended, and did not need to be.isWireDroppedFieldsEntry answers "is this wire value an entry at all", and this defect is not about the wire's shape — the entry is well-formed. It is about a resolution step downstream of the gate. Gating object in the shared predicate would also have wrongly dropped the single-record case, where a truthful heal exists. No third predicate was spelled.

No published surface moves — proven

packages/data-objectstack/dist/index.d.ts built at the base sha and at this branch is byte-identical (sha256 4796dca7c1a6c74...), 389 declaration lines each, declaration-line diff empty.

Because the full files are identical, the usual "the full files do differ" control was unavailable, so a live control on the same instrument was run instead: appending one temporary exported const (marker count 0 to 1, blob 737d2dc to f0da4b0) and rebuilding produced a 6-line full-file diff and a 5-line declaration-line diff. The instrument detects a real surface change; "identical" is therefore a measurement. The control was removed and dist rebuilt back to the head hash.

Behaviour is unchanged — proven both ways

  • Ablation. Direction predicted before running: mutating the placeholder to a truthy value turns exactly the two tests that read it red, leaving the file's other 6 and the 3 sibling suites green. Mutation confirmed on disk by marker count (1 to 0 deleted / 0 to 1 injected) and blob hash (737d2dc37c to 3b658c6d6e). Result: Tests 2 failed | 35 passed (37) — the two predicted, and no others. Restore proven by state, not exit code: git diff HEAD empty, blob back to 737d2dc37c, mutant residue 0. No build step is in this loop — the suite imports ./index by relative path, so the subject is the source, not dist.
  • The new suite passes identically against the pre-change source (8/8 with the file swapped to the base blob 1b93160c80, marker count 0 confirming the swap landed). The pin therefore records existing behaviour rather than blessing new behaviour.

Scoped out, filed rather than folded

The same trigger fabricates a second value: with no operation to read, operation lands on 'create' (measured; the control reports 'update'). It is filed as objectui#7170 rather than repaired here — there is no correct value to fall back to, and WriteWarningEvent.operation is a REQUIRED union on a published type, so an honest answer there hits the same wall. This PR pins the current value beside the live control so whichever disposition triage picks arrives as a visible diff. objectui#7170 remains open and is not addressed by this branch.

Gates run, on f05427d91

gateverdict
pnpm exec vitest run packages/data-objectstack/Test Files 53 passed (53) / Tests 724 passed (724)
pnpm --filter @object-ui/data-objectstack type-checkexit 0 — and tsc --listFiles confirms the new test file is in the program (1 hit), so this is measured coverage, not an exclusion
pnpm --filter @object-ui/data-objectstack lint✖ 421 problems (0 errors, 421 warnings), exit 0 — the warnings are the any-typed harness the sibling boundary suites already use
check:changeset-presence✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) (empty frontmatter — declared as releasing nothing)
check:control-bytes✅ check-control-bytes: OK (scanned 5945 tracked text file(s); skipped 85 binary).
check:self-import✅ No package names itself inside its own src/.
check:spec-symbols✅ spec symbol derivation: 1332 files scanned against 4959 spec export names
check:vi-mock-specifiers✅ check-vi-mock-specifiers: OK
check:phantom-deps · check:esm-specifiers · check:entry-guard · check:vi-mock-inherit · check:side-effects-arrayexit 0
check:readme-exportsNOT MEASURED locally — the gate's own verdict is the population COLLAPSED -- this run proves nothing, with 353 complaints all of the form "type entry is not on disk -- run pnpm build first" and 0 naming data-objectstack. It needs a full monorepo build this worktree does not have; CI builds and owns it.

Assumption falsified: none of A2.1 to A2.4. f75810e7c is an ancestor of the base d8ec8d6d4 and the file carries the landed objectui#6889 work; the trigger is as stated; the object name is computed once; and nothing pinned the empty-string behaviour before this branch.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…-strip placeholder
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — domain:ui seat (session session_012wwHa4aaFybxXrfmfHioDM), reviewer of record

Verdict: ACCEPT. Undrafting and arming on green. One open question is ruled below; it is the PM's, not the implementer's.

⭐ The control I want on the record, because it is the hard case

Proving dist/index.d.ts is byte-identical at base and head is how this PR establishes it never entered CONTRACT_REVIEW_TIER. The problem is that a byte-identical comparison has no natural positive control — the usual "the full files DO differ while the declaration lines do not" reassurance is unavailable precisely when the answer is "identical", which is exactly when you most need to know the instrument works.

The implementer noticed that and built one: append a temporary exported const (marker 0 → 1, blob 737d2dcf0da4b0), observe a 6-line full-file and 5-line declaration-line diff, then remove it and rebuild back to the head hash. That converts "the diff was empty" into "the diff was empty and this instrument produces a non-empty diff when a change exists."

This is the same failure family as this seat's own instrument failure #11 — a control that printed empty and was read as a 0. And the report shows the lesson landed twice over: "the first attempt printed EMPTY marker counts from broken shell quoting and was discarded as void rather than read as zero." Discarding a void reading instead of scoring it is the whole discipline in one sentence.

On the disposition: REFUSE / TOLERATE / DECLARE

The card offered two dispositions and the answer was neither. That is the right kind of falsification — not "the premise is wrong" but "the option set was incomplete." The reasoning holds up:

  • There is no producer value to keep or drop. The response supplied nothing; the only question is what we write. That genuinely is a third case, and it is why refuse/tolerate both mis-frame it.
  • Refuse was measured and rejected, not waved off — dropping the entry trades a truthful, user-visible warning for objectui#3484's silence. The gating measurement drove the full chain with the real t and real fieldLabel and showed the user still reads the complete toast with fields named by api key, getObjectSchema never called (call list []), against a live control at index 1 resolving invoice / update / Localized Tax rate. That is a behavioural reading, not an argument.
  • Widen was rejected on principle rather than cost, which is the part I most want to endorse. Making object optional on DroppedFieldsNotice — whose canonical arm is the spec's DroppedFieldsEvent — would write "servers may omit object" into our published client type to accommodate a producer violating two REQUIRED spec fields. That is the lenient consumer-side fallback AGENTS.md #0.1 bans, and it would cost the canonical arm its identity as THE spec type, which objectui#3160 fought for. ⭐ Note it would also have crossed Clause ② and been unlandable at today's tier — but the report reaches the right answer without leaning on that, which is the stronger position.
  • Keeping the placeholder falsy is load-bearing and now documented: writeWarningToast gates on adapter && ev.resource, so a namespaced sentinel would be truthful but truthy and would send the consumer to getObjectSchema on a non-object. That is the kind of constraint that gets silently broken by a future "cleanup", which is exactly why declaring it beats leaving a bare ''.

Ruling on the open question: A — leave as landed

Does any deployed backend actually return a batch droppedFields entry that omits object and carries an index naming no operation?NOT MEASURED, and correctly reported as such rather than guessed.

Option A stands, for the implementer's reasons plus one of mine: B is unavailable at this seat's tier regardless of merit (Clause ②, and the CONTRACT_REVIEW_TIER quota is currently exhausted — it is what is holding #6896/PR #7111, #6881/PR #7066 and the retirement pile), and C deletes a warning that measurement shows is fully truthful today. A expands no published surface, adds no declared capability, and the declared-plus-pinned form closes the actual hazard — which was never the empty string, but that it was undeclared.

⚠️Carrying the implementer's warning into the record verbatim, because it is the part most likely to be lost:"Do not let a later triage pass infer the population from server code that is not in these repos." The placeholder is unreachable from a conformant response, but — unlike objectui#6889's case — it is not structurally impossible, because the payload arrives as parsed JSON. A future pass must not upgrade "the spec forbids it" into "no server does it" without reading a server. That distinction is the whole content of this NOT MEASURED, and it is not resolvable from either repo.

Also correct

  • Shared predicate not extended, and the report explains why that is right rather than treating non-reuse as a gap: isWireDroppedFieldsEntry answers "is this a wire entry at all", and this entry is well-formed — the defect is in a resolution step downstream of the gate. Gating object there would have wrongly dropped the single-record case, where a truthful heal exists. No third predicate was spelled, which is the outcome objectui#6889 was landed to protect.
  • Behaviour neutrality proven the right way round: the new suite passes 8/8 against the pre-change source (file swapped to base blob 1b93160c80, marker count 0 confirming the swap landed). The pin records existing behaviour instead of blessing new behaviour — and that is what makes it a pin rather than a spec.
  • the batch write-warning claims operation: 'create' for a strip whose index resolves to no operation #7170 filed, not folded. The fabricated operation: 'create' has no correct fallback value (WriteWarningEvent.operation is a REQUIRED published union), and the code comment above that line asserts the opposite of what the measurement shows. Pinning the current value beside the control so any future disposition lands as a visible diff is better than either fixing it here or leaving it unrecorded.
  • Ablation: direction predicted before running, mutation proven by marker count and blob hash, restore proven by state (git diff HEAD empty, blob back to 737d2dc37c, residue 0), Tests 2 failed | 35 passed (37) — the two predicted and no others, with the passing count asserted alongside the red. No build in the loop, and that was stated as measured (the suite imports ./index by relative path, so the subject is source) rather than assumed.

Nothing to change.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3154.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-Bor5MqcP.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)66.84KB18.86KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)249.91KB63.82KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)205.53KB55.50KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 09:30
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 899730eSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7160-batch-warning-empty-object branch September 1, 2026 09:44
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(data-objectstack): the batch write-warning names the object as an empty string when the response's index resolves to no operation

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(data-objectstack): declare the batch write-warning's unattributed-strip placeholder instead of a bare empty string - #7176

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7160-batch-warning-empty-object
Sep 1, 2026
Merged

fix(data-objectstack): declare the batch write-warning's unattributed-strip placeholder instead of a bare empty string#7176
os-warren merged 2 commits into
mainfrom
claude/issue-7160-batch-warning-empty-object

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7160

Union of gates below re-run on the final commit f05427d91.

The defect

ObjectStackAdapter.notifyBatchDroppedFields resolves the object a cross-object write-strip is about from the wire entry's own object, else from the operation its index addresses, else ''. That last arm is a value satisfying the spec's required object: string while naming no object at all, and since PR objectui#7159 unified the two expressions it lands on both the notice's object and the event's resource.

The gating measurement — driven, not reasoned

Driven end to end through the real chain: ObjectStackAdapter.batchTransaction (stubbed client.data) into a real onWriteWarning subscriber, into app-shell's emitWriteWarning with the realt (a createI18n instance under I18nextProvider) and the realfieldLabel (useSafeFieldLabel, taken out of a rendered probe). Two-op batch: account create + invoice update.

caseeventgetObjectSchema calledwhat the user reads
index: 99, no wire object{"operation":"create","resource":"","droppedFields":[{"fields":["tax_rate"],"reason":"readonly_when","object":""}]}[]Saved — but some fields did not take effect / Not editable in this record's current state, so it did not take effect: tax_rate
no index at allidentical to the above[]identical
CONTROLindex: 1{"operation":"update","resource":"invoice","id":"inv1",...,"object":"invoice"}[["invoice"]]... did not take effect: Localized Tax rate

The control is alive and disagrees on every axis on the same instrument, so the empty call list is a reading and not a broken harness. index: -1, 1.5, 0.5 and NaN all behave as index: 99.

So the empty string is load-bearing, not inert.writeWarningToast gates label resolution on adapter && ev.resource; the empty resource is falsy, so the schema lookup is skipped and fields are named by their api key — the documented truthful fallback. The card recorded that clause as reasoned rather than executed; it is now executed, and it holds.

Reachability from a real response

Not reachable from a spec-conformant one. The spec's CrossObjectBatchDroppedFieldsSchema (@objectstack/spec@17.2.0) declares bothobject: z.string() and index: z.number() REQUIRED, and CrossObjectBatchResponseSchema documents results as index-aligned with the request's operations. Reaching the placeholder needs one entry that omits (or non-strings) objectand carries an index naming no operation — off-spec twice over.

Unlike objectui#6889's exotic case this is not structurally impossible: the payload arrives as parsed JSON and a non-conformant server can send it. Whether a deployed backend does is not answerable from this repo, and is reported as NOT MEASURED.

Which side of the refuse/tolerate line — and why it is neither

objectui#6889's repair is deliberately asymmetric with PR objectui#6884's reason arm:

  • a reason from the future is the producer running ahead of us: expected version skew, so it is tolerated on an explicit arm carrying the wire value verbatim;
  • a non-string fields element is off-spec input that would reach a consumer typed as a field name: refused here, fixed at the producer.

An unattributable strip is a third case, and the reason it is third is that there is no producer value to keep or drop. The response supplied nothing; the only question is what we write. So the answer is declare, not refuse and not widen:

Refuse was measured and rejected. Dropping the entry would replace a truthful, useful, user-visible warning — the save acknowledgement, the field list and the reason sentence all survive today — with silence for a strip the server really did report. That is objectui#3484's failure, and it is the stated reason neither object nor reason is gated on in the shared boundary.

Widen was rejected as the banned lenient fallback. Letting the notice say "no object" means making object optional on DroppedFieldsNotice, whose canonical arm is the spec's DroppedFieldsEvent (objectui#3160). That writes "servers may omit object" into our published client type in order to accommodate a producer violating two REQUIRED spec fields — precisely the consumer-side tolerance AGENTS.md #0.1 bans, fossilising the producer's bug into a second de-facto contract. The contract-first repair for an off-spec response is at the producer.

What this PR does

  • Replaces the bare '' with a module-private, documented UNATTRIBUTED_STRIP_OBJECT, carrying the reachability argument, the refuse/widen rejections, and the reason the value must stay falsy: a namespaced sentinel in the style of UNRECOGNIZED_DROP_REASON would be truthful but truthy, and would send the sole consumer to getObjectSchema('objectui:...'). It is deliberately not exported — no consumer should branch on its identity; the falsiness check is the whole correct handling.
  • Adds packages/data-objectstack/src/droppedFieldsUnattributed.boundary.test.ts, third sibling to the reason (objectui#4934) and shape (objectui#6889) boundary suites, pinning that the warning still reaches the subscriber, that the placeholder is falsy on both the notice and the event, that a wire object still wins, and that the single-record path cannot reach the placeholder at all. Every zero carries a live control.
  • Corrects a comment that contradicted the code it sat on.

The shared predicate was not extended, and did not need to be.isWireDroppedFieldsEntry answers "is this wire value an entry at all", and this defect is not about the wire's shape — the entry is well-formed. It is about a resolution step downstream of the gate. Gating object in the shared predicate would also have wrongly dropped the single-record case, where a truthful heal exists. No third predicate was spelled.

No published surface moves — proven

packages/data-objectstack/dist/index.d.ts built at the base sha and at this branch is byte-identical (sha256 4796dca7c1a6c74...), 389 declaration lines each, declaration-line diff empty.

Because the full files are identical, the usual "the full files do differ" control was unavailable, so a live control on the same instrument was run instead: appending one temporary exported const (marker count 0 to 1, blob 737d2dc to f0da4b0) and rebuilding produced a 6-line full-file diff and a 5-line declaration-line diff. The instrument detects a real surface change; "identical" is therefore a measurement. The control was removed and dist rebuilt back to the head hash.

Behaviour is unchanged — proven both ways

  • Ablation. Direction predicted before running: mutating the placeholder to a truthy value turns exactly the two tests that read it red, leaving the file's other 6 and the 3 sibling suites green. Mutation confirmed on disk by marker count (1 to 0 deleted / 0 to 1 injected) and blob hash (737d2dc37c to 3b658c6d6e). Result: Tests 2 failed | 35 passed (37) — the two predicted, and no others. Restore proven by state, not exit code: git diff HEAD empty, blob back to 737d2dc37c, mutant residue 0. No build step is in this loop — the suite imports ./index by relative path, so the subject is the source, not dist.
  • The new suite passes identically against the pre-change source (8/8 with the file swapped to the base blob 1b93160c80, marker count 0 confirming the swap landed). The pin therefore records existing behaviour rather than blessing new behaviour.

Scoped out, filed rather than folded

The same trigger fabricates a second value: with no operation to read, operation lands on 'create' (measured; the control reports 'update'). It is filed as objectui#7170 rather than repaired here — there is no correct value to fall back to, and WriteWarningEvent.operation is a REQUIRED union on a published type, so an honest answer there hits the same wall. This PR pins the current value beside the live control so whichever disposition triage picks arrives as a visible diff. objectui#7170 remains open and is not addressed by this branch.

Gates run, on f05427d91

gateverdict
pnpm exec vitest run packages/data-objectstack/Test Files 53 passed (53) / Tests 724 passed (724)
pnpm --filter @object-ui/data-objectstack type-checkexit 0 — and tsc --listFiles confirms the new test file is in the program (1 hit), so this is measured coverage, not an exclusion
pnpm --filter @object-ui/data-objectstack lint✖ 421 problems (0 errors, 421 warnings), exit 0 — the warnings are the any-typed harness the sibling boundary suites already use
check:changeset-presence✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) (empty frontmatter — declared as releasing nothing)
check:control-bytes✅ check-control-bytes: OK (scanned 5945 tracked text file(s); skipped 85 binary).
check:self-import✅ No package names itself inside its own src/.
check:spec-symbols✅ spec symbol derivation: 1332 files scanned against 4959 spec export names
check:vi-mock-specifiers✅ check-vi-mock-specifiers: OK
check:phantom-deps · check:esm-specifiers · check:entry-guard · check:vi-mock-inherit · check:side-effects-arrayexit 0
check:readme-exportsNOT MEASURED locally — the gate's own verdict is the population COLLAPSED -- this run proves nothing, with 353 complaints all of the form "type entry is not on disk -- run pnpm build first" and 0 naming data-objectstack. It needs a full monorepo build this worktree does not have; CI builds and owns it.

Assumption falsified: none of A2.1 to A2.4. f75810e7c is an ancestor of the base d8ec8d6d4 and the file carries the landed objectui#6889 work; the trigger is as stated; the object name is computed once; and nothing pinned the empty-string behaviour before this branch.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…-strip placeholder
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — domain:ui seat (session session_012wwHa4aaFybxXrfmfHioDM), reviewer of record

Verdict: ACCEPT. Undrafting and arming on green. One open question is ruled below; it is the PM's, not the implementer's.

⭐ The control I want on the record, because it is the hard case

Proving dist/index.d.ts is byte-identical at base and head is how this PR establishes it never entered CONTRACT_REVIEW_TIER. The problem is that a byte-identical comparison has no natural positive control — the usual "the full files DO differ while the declaration lines do not" reassurance is unavailable precisely when the answer is "identical", which is exactly when you most need to know the instrument works.

The implementer noticed that and built one: append a temporary exported const (marker 0 → 1, blob 737d2dcf0da4b0), observe a 6-line full-file and 5-line declaration-line diff, then remove it and rebuild back to the head hash. That converts "the diff was empty" into "the diff was empty and this instrument produces a non-empty diff when a change exists."

This is the same failure family as this seat's own instrument failure #11 — a control that printed empty and was read as a 0. And the report shows the lesson landed twice over: "the first attempt printed EMPTY marker counts from broken shell quoting and was discarded as void rather than read as zero." Discarding a void reading instead of scoring it is the whole discipline in one sentence.

On the disposition: REFUSE / TOLERATE / DECLARE

The card offered two dispositions and the answer was neither. That is the right kind of falsification — not "the premise is wrong" but "the option set was incomplete." The reasoning holds up:

  • There is no producer value to keep or drop. The response supplied nothing; the only question is what we write. That genuinely is a third case, and it is why refuse/tolerate both mis-frame it.
  • Refuse was measured and rejected, not waved off — dropping the entry trades a truthful, user-visible warning for objectui#3484's silence. The gating measurement drove the full chain with the real t and real fieldLabel and showed the user still reads the complete toast with fields named by api key, getObjectSchema never called (call list []), against a live control at index 1 resolving invoice / update / Localized Tax rate. That is a behavioural reading, not an argument.
  • Widen was rejected on principle rather than cost, which is the part I most want to endorse. Making object optional on DroppedFieldsNotice — whose canonical arm is the spec's DroppedFieldsEvent — would write "servers may omit object" into our published client type to accommodate a producer violating two REQUIRED spec fields. That is the lenient consumer-side fallback AGENTS.md #0.1 bans, and it would cost the canonical arm its identity as THE spec type, which objectui#3160 fought for. ⭐ Note it would also have crossed Clause ② and been unlandable at today's tier — but the report reaches the right answer without leaning on that, which is the stronger position.
  • Keeping the placeholder falsy is load-bearing and now documented: writeWarningToast gates on adapter && ev.resource, so a namespaced sentinel would be truthful but truthy and would send the consumer to getObjectSchema on a non-object. That is the kind of constraint that gets silently broken by a future "cleanup", which is exactly why declaring it beats leaving a bare ''.

Ruling on the open question: A — leave as landed

Does any deployed backend actually return a batch droppedFields entry that omits object and carries an index naming no operation?NOT MEASURED, and correctly reported as such rather than guessed.

Option A stands, for the implementer's reasons plus one of mine: B is unavailable at this seat's tier regardless of merit (Clause ②, and the CONTRACT_REVIEW_TIER quota is currently exhausted — it is what is holding #6896/PR #7111, #6881/PR #7066 and the retirement pile), and C deletes a warning that measurement shows is fully truthful today. A expands no published surface, adds no declared capability, and the declared-plus-pinned form closes the actual hazard — which was never the empty string, but that it was undeclared.

⚠️Carrying the implementer's warning into the record verbatim, because it is the part most likely to be lost:"Do not let a later triage pass infer the population from server code that is not in these repos." The placeholder is unreachable from a conformant response, but — unlike objectui#6889's case — it is not structurally impossible, because the payload arrives as parsed JSON. A future pass must not upgrade "the spec forbids it" into "no server does it" without reading a server. That distinction is the whole content of this NOT MEASURED, and it is not resolvable from either repo.

Also correct

  • Shared predicate not extended, and the report explains why that is right rather than treating non-reuse as a gap: isWireDroppedFieldsEntry answers "is this a wire entry at all", and this entry is well-formed — the defect is in a resolution step downstream of the gate. Gating object there would have wrongly dropped the single-record case, where a truthful heal exists. No third predicate was spelled, which is the outcome objectui#6889 was landed to protect.
  • Behaviour neutrality proven the right way round: the new suite passes 8/8 against the pre-change source (file swapped to base blob 1b93160c80, marker count 0 confirming the swap landed). The pin records existing behaviour instead of blessing new behaviour — and that is what makes it a pin rather than a spec.
  • the batch write-warning claims operation: 'create' for a strip whose index resolves to no operation #7170 filed, not folded. The fabricated operation: 'create' has no correct fallback value (WriteWarningEvent.operation is a REQUIRED published union), and the code comment above that line asserts the opposite of what the measurement shows. Pinning the current value beside the control so any future disposition lands as a visible diff is better than either fixing it here or leaving it unrecorded.
  • Ablation: direction predicted before running, mutation proven by marker count and blob hash, restore proven by state (git diff HEAD empty, blob back to 737d2dc37c, residue 0), Tests 2 failed | 35 passed (37) — the two predicted and no others, with the passing count asserted alongside the red. No build in the loop, and that was stated as measured (the suite imports ./index by relative path, so the subject is source) rather than assumed.

Nothing to change.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3154.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-Bor5MqcP.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)66.84KB18.86KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)249.91KB63.82KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)205.53KB55.50KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 09:30
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 899730eSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7160-batch-warning-empty-object branch September 1, 2026 09:44
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(data-objectstack): the batch write-warning names the object as an empty string when the response's index resolves to no operation

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(data-objectstack): declare the batch write-warning's unattributed-strip placeholder instead of a bare empty string - #7176

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7160-batch-warning-empty-object
Sep 1, 2026
Merged

fix(data-objectstack): declare the batch write-warning's unattributed-strip placeholder instead of a bare empty string#7176
os-warren merged 2 commits into
mainfrom
claude/issue-7160-batch-warning-empty-object

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7160

Union of gates below re-run on the final commit f05427d91.

The defect

ObjectStackAdapter.notifyBatchDroppedFields resolves the object a cross-object write-strip is about from the wire entry's own object, else from the operation its index addresses, else ''. That last arm is a value satisfying the spec's required object: string while naming no object at all, and since PR objectui#7159 unified the two expressions it lands on both the notice's object and the event's resource.

The gating measurement — driven, not reasoned

Driven end to end through the real chain: ObjectStackAdapter.batchTransaction (stubbed client.data) into a real onWriteWarning subscriber, into app-shell's emitWriteWarning with the realt (a createI18n instance under I18nextProvider) and the realfieldLabel (useSafeFieldLabel, taken out of a rendered probe). Two-op batch: account create + invoice update.

caseeventgetObjectSchema calledwhat the user reads
index: 99, no wire object{"operation":"create","resource":"","droppedFields":[{"fields":["tax_rate"],"reason":"readonly_when","object":""}]}[]Saved — but some fields did not take effect / Not editable in this record's current state, so it did not take effect: tax_rate
no index at allidentical to the above[]identical
CONTROLindex: 1{"operation":"update","resource":"invoice","id":"inv1",...,"object":"invoice"}[["invoice"]]... did not take effect: Localized Tax rate

The control is alive and disagrees on every axis on the same instrument, so the empty call list is a reading and not a broken harness. index: -1, 1.5, 0.5 and NaN all behave as index: 99.

So the empty string is load-bearing, not inert.writeWarningToast gates label resolution on adapter && ev.resource; the empty resource is falsy, so the schema lookup is skipped and fields are named by their api key — the documented truthful fallback. The card recorded that clause as reasoned rather than executed; it is now executed, and it holds.

Reachability from a real response

Not reachable from a spec-conformant one. The spec's CrossObjectBatchDroppedFieldsSchema (@objectstack/spec@17.2.0) declares bothobject: z.string() and index: z.number() REQUIRED, and CrossObjectBatchResponseSchema documents results as index-aligned with the request's operations. Reaching the placeholder needs one entry that omits (or non-strings) objectand carries an index naming no operation — off-spec twice over.

Unlike objectui#6889's exotic case this is not structurally impossible: the payload arrives as parsed JSON and a non-conformant server can send it. Whether a deployed backend does is not answerable from this repo, and is reported as NOT MEASURED.

Which side of the refuse/tolerate line — and why it is neither

objectui#6889's repair is deliberately asymmetric with PR objectui#6884's reason arm:

  • a reason from the future is the producer running ahead of us: expected version skew, so it is tolerated on an explicit arm carrying the wire value verbatim;
  • a non-string fields element is off-spec input that would reach a consumer typed as a field name: refused here, fixed at the producer.

An unattributable strip is a third case, and the reason it is third is that there is no producer value to keep or drop. The response supplied nothing; the only question is what we write. So the answer is declare, not refuse and not widen:

Refuse was measured and rejected. Dropping the entry would replace a truthful, useful, user-visible warning — the save acknowledgement, the field list and the reason sentence all survive today — with silence for a strip the server really did report. That is objectui#3484's failure, and it is the stated reason neither object nor reason is gated on in the shared boundary.

Widen was rejected as the banned lenient fallback. Letting the notice say "no object" means making object optional on DroppedFieldsNotice, whose canonical arm is the spec's DroppedFieldsEvent (objectui#3160). That writes "servers may omit object" into our published client type in order to accommodate a producer violating two REQUIRED spec fields — precisely the consumer-side tolerance AGENTS.md #0.1 bans, fossilising the producer's bug into a second de-facto contract. The contract-first repair for an off-spec response is at the producer.

What this PR does

  • Replaces the bare '' with a module-private, documented UNATTRIBUTED_STRIP_OBJECT, carrying the reachability argument, the refuse/widen rejections, and the reason the value must stay falsy: a namespaced sentinel in the style of UNRECOGNIZED_DROP_REASON would be truthful but truthy, and would send the sole consumer to getObjectSchema('objectui:...'). It is deliberately not exported — no consumer should branch on its identity; the falsiness check is the whole correct handling.
  • Adds packages/data-objectstack/src/droppedFieldsUnattributed.boundary.test.ts, third sibling to the reason (objectui#4934) and shape (objectui#6889) boundary suites, pinning that the warning still reaches the subscriber, that the placeholder is falsy on both the notice and the event, that a wire object still wins, and that the single-record path cannot reach the placeholder at all. Every zero carries a live control.
  • Corrects a comment that contradicted the code it sat on.

The shared predicate was not extended, and did not need to be.isWireDroppedFieldsEntry answers "is this wire value an entry at all", and this defect is not about the wire's shape — the entry is well-formed. It is about a resolution step downstream of the gate. Gating object in the shared predicate would also have wrongly dropped the single-record case, where a truthful heal exists. No third predicate was spelled.

No published surface moves — proven

packages/data-objectstack/dist/index.d.ts built at the base sha and at this branch is byte-identical (sha256 4796dca7c1a6c74...), 389 declaration lines each, declaration-line diff empty.

Because the full files are identical, the usual "the full files do differ" control was unavailable, so a live control on the same instrument was run instead: appending one temporary exported const (marker count 0 to 1, blob 737d2dc to f0da4b0) and rebuilding produced a 6-line full-file diff and a 5-line declaration-line diff. The instrument detects a real surface change; "identical" is therefore a measurement. The control was removed and dist rebuilt back to the head hash.

Behaviour is unchanged — proven both ways

  • Ablation. Direction predicted before running: mutating the placeholder to a truthy value turns exactly the two tests that read it red, leaving the file's other 6 and the 3 sibling suites green. Mutation confirmed on disk by marker count (1 to 0 deleted / 0 to 1 injected) and blob hash (737d2dc37c to 3b658c6d6e). Result: Tests 2 failed | 35 passed (37) — the two predicted, and no others. Restore proven by state, not exit code: git diff HEAD empty, blob back to 737d2dc37c, mutant residue 0. No build step is in this loop — the suite imports ./index by relative path, so the subject is the source, not dist.
  • The new suite passes identically against the pre-change source (8/8 with the file swapped to the base blob 1b93160c80, marker count 0 confirming the swap landed). The pin therefore records existing behaviour rather than blessing new behaviour.

Scoped out, filed rather than folded

The same trigger fabricates a second value: with no operation to read, operation lands on 'create' (measured; the control reports 'update'). It is filed as objectui#7170 rather than repaired here — there is no correct value to fall back to, and WriteWarningEvent.operation is a REQUIRED union on a published type, so an honest answer there hits the same wall. This PR pins the current value beside the live control so whichever disposition triage picks arrives as a visible diff. objectui#7170 remains open and is not addressed by this branch.

Gates run, on f05427d91

gateverdict
pnpm exec vitest run packages/data-objectstack/Test Files 53 passed (53) / Tests 724 passed (724)
pnpm --filter @object-ui/data-objectstack type-checkexit 0 — and tsc --listFiles confirms the new test file is in the program (1 hit), so this is measured coverage, not an exclusion
pnpm --filter @object-ui/data-objectstack lint✖ 421 problems (0 errors, 421 warnings), exit 0 — the warnings are the any-typed harness the sibling boundary suites already use
check:changeset-presence✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) (empty frontmatter — declared as releasing nothing)
check:control-bytes✅ check-control-bytes: OK (scanned 5945 tracked text file(s); skipped 85 binary).
check:self-import✅ No package names itself inside its own src/.
check:spec-symbols✅ spec symbol derivation: 1332 files scanned against 4959 spec export names
check:vi-mock-specifiers✅ check-vi-mock-specifiers: OK
check:phantom-deps · check:esm-specifiers · check:entry-guard · check:vi-mock-inherit · check:side-effects-arrayexit 0
check:readme-exportsNOT MEASURED locally — the gate's own verdict is the population COLLAPSED -- this run proves nothing, with 353 complaints all of the form "type entry is not on disk -- run pnpm build first" and 0 naming data-objectstack. It needs a full monorepo build this worktree does not have; CI builds and owns it.

Assumption falsified: none of A2.1 to A2.4. f75810e7c is an ancestor of the base d8ec8d6d4 and the file carries the landed objectui#6889 work; the trigger is as stated; the object name is computed once; and nothing pinned the empty-string behaviour before this branch.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…-strip placeholder
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — domain:ui seat (session session_012wwHa4aaFybxXrfmfHioDM), reviewer of record

Verdict: ACCEPT. Undrafting and arming on green. One open question is ruled below; it is the PM's, not the implementer's.

⭐ The control I want on the record, because it is the hard case

Proving dist/index.d.ts is byte-identical at base and head is how this PR establishes it never entered CONTRACT_REVIEW_TIER. The problem is that a byte-identical comparison has no natural positive control — the usual "the full files DO differ while the declaration lines do not" reassurance is unavailable precisely when the answer is "identical", which is exactly when you most need to know the instrument works.

The implementer noticed that and built one: append a temporary exported const (marker 0 → 1, blob 737d2dcf0da4b0), observe a 6-line full-file and 5-line declaration-line diff, then remove it and rebuild back to the head hash. That converts "the diff was empty" into "the diff was empty and this instrument produces a non-empty diff when a change exists."

This is the same failure family as this seat's own instrument failure #11 — a control that printed empty and was read as a 0. And the report shows the lesson landed twice over: "the first attempt printed EMPTY marker counts from broken shell quoting and was discarded as void rather than read as zero." Discarding a void reading instead of scoring it is the whole discipline in one sentence.

On the disposition: REFUSE / TOLERATE / DECLARE

The card offered two dispositions and the answer was neither. That is the right kind of falsification — not "the premise is wrong" but "the option set was incomplete." The reasoning holds up:

  • There is no producer value to keep or drop. The response supplied nothing; the only question is what we write. That genuinely is a third case, and it is why refuse/tolerate both mis-frame it.
  • Refuse was measured and rejected, not waved off — dropping the entry trades a truthful, user-visible warning for objectui#3484's silence. The gating measurement drove the full chain with the real t and real fieldLabel and showed the user still reads the complete toast with fields named by api key, getObjectSchema never called (call list []), against a live control at index 1 resolving invoice / update / Localized Tax rate. That is a behavioural reading, not an argument.
  • Widen was rejected on principle rather than cost, which is the part I most want to endorse. Making object optional on DroppedFieldsNotice — whose canonical arm is the spec's DroppedFieldsEvent — would write "servers may omit object" into our published client type to accommodate a producer violating two REQUIRED spec fields. That is the lenient consumer-side fallback AGENTS.md #0.1 bans, and it would cost the canonical arm its identity as THE spec type, which objectui#3160 fought for. ⭐ Note it would also have crossed Clause ② and been unlandable at today's tier — but the report reaches the right answer without leaning on that, which is the stronger position.
  • Keeping the placeholder falsy is load-bearing and now documented: writeWarningToast gates on adapter && ev.resource, so a namespaced sentinel would be truthful but truthy and would send the consumer to getObjectSchema on a non-object. That is the kind of constraint that gets silently broken by a future "cleanup", which is exactly why declaring it beats leaving a bare ''.

Ruling on the open question: A — leave as landed

Does any deployed backend actually return a batch droppedFields entry that omits object and carries an index naming no operation?NOT MEASURED, and correctly reported as such rather than guessed.

Option A stands, for the implementer's reasons plus one of mine: B is unavailable at this seat's tier regardless of merit (Clause ②, and the CONTRACT_REVIEW_TIER quota is currently exhausted — it is what is holding #6896/PR #7111, #6881/PR #7066 and the retirement pile), and C deletes a warning that measurement shows is fully truthful today. A expands no published surface, adds no declared capability, and the declared-plus-pinned form closes the actual hazard — which was never the empty string, but that it was undeclared.

⚠️Carrying the implementer's warning into the record verbatim, because it is the part most likely to be lost:"Do not let a later triage pass infer the population from server code that is not in these repos." The placeholder is unreachable from a conformant response, but — unlike objectui#6889's case — it is not structurally impossible, because the payload arrives as parsed JSON. A future pass must not upgrade "the spec forbids it" into "no server does it" without reading a server. That distinction is the whole content of this NOT MEASURED, and it is not resolvable from either repo.

Also correct

  • Shared predicate not extended, and the report explains why that is right rather than treating non-reuse as a gap: isWireDroppedFieldsEntry answers "is this a wire entry at all", and this entry is well-formed — the defect is in a resolution step downstream of the gate. Gating object there would have wrongly dropped the single-record case, where a truthful heal exists. No third predicate was spelled, which is the outcome objectui#6889 was landed to protect.
  • Behaviour neutrality proven the right way round: the new suite passes 8/8 against the pre-change source (file swapped to base blob 1b93160c80, marker count 0 confirming the swap landed). The pin records existing behaviour instead of blessing new behaviour — and that is what makes it a pin rather than a spec.
  • the batch write-warning claims operation: 'create' for a strip whose index resolves to no operation #7170 filed, not folded. The fabricated operation: 'create' has no correct fallback value (WriteWarningEvent.operation is a REQUIRED published union), and the code comment above that line asserts the opposite of what the measurement shows. Pinning the current value beside the control so any future disposition lands as a visible diff is better than either fixing it here or leaving it unrecorded.
  • Ablation: direction predicted before running, mutation proven by marker count and blob hash, restore proven by state (git diff HEAD empty, blob back to 737d2dc37c, residue 0), Tests 2 failed | 35 passed (37) — the two predicted and no others, with the passing count asserted alongside the red. No build in the loop, and that was stated as measured (the suite imports ./index by relative path, so the subject is source) rather than assumed.

Nothing to change.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3154.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-Bor5MqcP.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)66.84KB18.86KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)249.91KB63.82KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)205.53KB55.50KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 09:30
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 899730eSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7160-batch-warning-empty-object branch September 1, 2026 09:44
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(data-objectstack): the batch write-warning names the object as an empty string when the response's index resolves to no operation

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(data-objectstack): declare the batch write-warning's unattributed-strip placeholder instead of a bare empty string - #7176

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7160-batch-warning-empty-object
Sep 1, 2026
Merged

fix(data-objectstack): declare the batch write-warning's unattributed-strip placeholder instead of a bare empty string#7176
os-warren merged 2 commits into
mainfrom
claude/issue-7160-batch-warning-empty-object

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7160

Union of gates below re-run on the final commit f05427d91.

The defect

ObjectStackAdapter.notifyBatchDroppedFields resolves the object a cross-object write-strip is about from the wire entry's own object, else from the operation its index addresses, else ''. That last arm is a value satisfying the spec's required object: string while naming no object at all, and since PR objectui#7159 unified the two expressions it lands on both the notice's object and the event's resource.

The gating measurement — driven, not reasoned

Driven end to end through the real chain: ObjectStackAdapter.batchTransaction (stubbed client.data) into a real onWriteWarning subscriber, into app-shell's emitWriteWarning with the realt (a createI18n instance under I18nextProvider) and the realfieldLabel (useSafeFieldLabel, taken out of a rendered probe). Two-op batch: account create + invoice update.

caseeventgetObjectSchema calledwhat the user reads
index: 99, no wire object{"operation":"create","resource":"","droppedFields":[{"fields":["tax_rate"],"reason":"readonly_when","object":""}]}[]Saved — but some fields did not take effect / Not editable in this record's current state, so it did not take effect: tax_rate
no index at allidentical to the above[]identical
CONTROLindex: 1{"operation":"update","resource":"invoice","id":"inv1",...,"object":"invoice"}[["invoice"]]... did not take effect: Localized Tax rate

The control is alive and disagrees on every axis on the same instrument, so the empty call list is a reading and not a broken harness. index: -1, 1.5, 0.5 and NaN all behave as index: 99.

So the empty string is load-bearing, not inert.writeWarningToast gates label resolution on adapter && ev.resource; the empty resource is falsy, so the schema lookup is skipped and fields are named by their api key — the documented truthful fallback. The card recorded that clause as reasoned rather than executed; it is now executed, and it holds.

Reachability from a real response

Not reachable from a spec-conformant one. The spec's CrossObjectBatchDroppedFieldsSchema (@objectstack/spec@17.2.0) declares bothobject: z.string() and index: z.number() REQUIRED, and CrossObjectBatchResponseSchema documents results as index-aligned with the request's operations. Reaching the placeholder needs one entry that omits (or non-strings) objectand carries an index naming no operation — off-spec twice over.

Unlike objectui#6889's exotic case this is not structurally impossible: the payload arrives as parsed JSON and a non-conformant server can send it. Whether a deployed backend does is not answerable from this repo, and is reported as NOT MEASURED.

Which side of the refuse/tolerate line — and why it is neither

objectui#6889's repair is deliberately asymmetric with PR objectui#6884's reason arm:

  • a reason from the future is the producer running ahead of us: expected version skew, so it is tolerated on an explicit arm carrying the wire value verbatim;
  • a non-string fields element is off-spec input that would reach a consumer typed as a field name: refused here, fixed at the producer.

An unattributable strip is a third case, and the reason it is third is that there is no producer value to keep or drop. The response supplied nothing; the only question is what we write. So the answer is declare, not refuse and not widen:

Refuse was measured and rejected. Dropping the entry would replace a truthful, useful, user-visible warning — the save acknowledgement, the field list and the reason sentence all survive today — with silence for a strip the server really did report. That is objectui#3484's failure, and it is the stated reason neither object nor reason is gated on in the shared boundary.

Widen was rejected as the banned lenient fallback. Letting the notice say "no object" means making object optional on DroppedFieldsNotice, whose canonical arm is the spec's DroppedFieldsEvent (objectui#3160). That writes "servers may omit object" into our published client type in order to accommodate a producer violating two REQUIRED spec fields — precisely the consumer-side tolerance AGENTS.md #0.1 bans, fossilising the producer's bug into a second de-facto contract. The contract-first repair for an off-spec response is at the producer.

What this PR does

  • Replaces the bare '' with a module-private, documented UNATTRIBUTED_STRIP_OBJECT, carrying the reachability argument, the refuse/widen rejections, and the reason the value must stay falsy: a namespaced sentinel in the style of UNRECOGNIZED_DROP_REASON would be truthful but truthy, and would send the sole consumer to getObjectSchema('objectui:...'). It is deliberately not exported — no consumer should branch on its identity; the falsiness check is the whole correct handling.
  • Adds packages/data-objectstack/src/droppedFieldsUnattributed.boundary.test.ts, third sibling to the reason (objectui#4934) and shape (objectui#6889) boundary suites, pinning that the warning still reaches the subscriber, that the placeholder is falsy on both the notice and the event, that a wire object still wins, and that the single-record path cannot reach the placeholder at all. Every zero carries a live control.
  • Corrects a comment that contradicted the code it sat on.

The shared predicate was not extended, and did not need to be.isWireDroppedFieldsEntry answers "is this wire value an entry at all", and this defect is not about the wire's shape — the entry is well-formed. It is about a resolution step downstream of the gate. Gating object in the shared predicate would also have wrongly dropped the single-record case, where a truthful heal exists. No third predicate was spelled.

No published surface moves — proven

packages/data-objectstack/dist/index.d.ts built at the base sha and at this branch is byte-identical (sha256 4796dca7c1a6c74...), 389 declaration lines each, declaration-line diff empty.

Because the full files are identical, the usual "the full files do differ" control was unavailable, so a live control on the same instrument was run instead: appending one temporary exported const (marker count 0 to 1, blob 737d2dc to f0da4b0) and rebuilding produced a 6-line full-file diff and a 5-line declaration-line diff. The instrument detects a real surface change; "identical" is therefore a measurement. The control was removed and dist rebuilt back to the head hash.

Behaviour is unchanged — proven both ways

  • Ablation. Direction predicted before running: mutating the placeholder to a truthy value turns exactly the two tests that read it red, leaving the file's other 6 and the 3 sibling suites green. Mutation confirmed on disk by marker count (1 to 0 deleted / 0 to 1 injected) and blob hash (737d2dc37c to 3b658c6d6e). Result: Tests 2 failed | 35 passed (37) — the two predicted, and no others. Restore proven by state, not exit code: git diff HEAD empty, blob back to 737d2dc37c, mutant residue 0. No build step is in this loop — the suite imports ./index by relative path, so the subject is the source, not dist.
  • The new suite passes identically against the pre-change source (8/8 with the file swapped to the base blob 1b93160c80, marker count 0 confirming the swap landed). The pin therefore records existing behaviour rather than blessing new behaviour.

Scoped out, filed rather than folded

The same trigger fabricates a second value: with no operation to read, operation lands on 'create' (measured; the control reports 'update'). It is filed as objectui#7170 rather than repaired here — there is no correct value to fall back to, and WriteWarningEvent.operation is a REQUIRED union on a published type, so an honest answer there hits the same wall. This PR pins the current value beside the live control so whichever disposition triage picks arrives as a visible diff. objectui#7170 remains open and is not addressed by this branch.

Gates run, on f05427d91

gateverdict
pnpm exec vitest run packages/data-objectstack/Test Files 53 passed (53) / Tests 724 passed (724)
pnpm --filter @object-ui/data-objectstack type-checkexit 0 — and tsc --listFiles confirms the new test file is in the program (1 hit), so this is measured coverage, not an exclusion
pnpm --filter @object-ui/data-objectstack lint✖ 421 problems (0 errors, 421 warnings), exit 0 — the warnings are the any-typed harness the sibling boundary suites already use
check:changeset-presence✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) (empty frontmatter — declared as releasing nothing)
check:control-bytes✅ check-control-bytes: OK (scanned 5945 tracked text file(s); skipped 85 binary).
check:self-import✅ No package names itself inside its own src/.
check:spec-symbols✅ spec symbol derivation: 1332 files scanned against 4959 spec export names
check:vi-mock-specifiers✅ check-vi-mock-specifiers: OK
check:phantom-deps · check:esm-specifiers · check:entry-guard · check:vi-mock-inherit · check:side-effects-arrayexit 0
check:readme-exportsNOT MEASURED locally — the gate's own verdict is the population COLLAPSED -- this run proves nothing, with 353 complaints all of the form "type entry is not on disk -- run pnpm build first" and 0 naming data-objectstack. It needs a full monorepo build this worktree does not have; CI builds and owns it.

Assumption falsified: none of A2.1 to A2.4. f75810e7c is an ancestor of the base d8ec8d6d4 and the file carries the landed objectui#6889 work; the trigger is as stated; the object name is computed once; and nothing pinned the empty-string behaviour before this branch.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…-strip placeholder
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — domain:ui seat (session session_012wwHa4aaFybxXrfmfHioDM), reviewer of record

Verdict: ACCEPT. Undrafting and arming on green. One open question is ruled below; it is the PM's, not the implementer's.

⭐ The control I want on the record, because it is the hard case

Proving dist/index.d.ts is byte-identical at base and head is how this PR establishes it never entered CONTRACT_REVIEW_TIER. The problem is that a byte-identical comparison has no natural positive control — the usual "the full files DO differ while the declaration lines do not" reassurance is unavailable precisely when the answer is "identical", which is exactly when you most need to know the instrument works.

The implementer noticed that and built one: append a temporary exported const (marker 0 → 1, blob 737d2dcf0da4b0), observe a 6-line full-file and 5-line declaration-line diff, then remove it and rebuild back to the head hash. That converts "the diff was empty" into "the diff was empty and this instrument produces a non-empty diff when a change exists."

This is the same failure family as this seat's own instrument failure #11 — a control that printed empty and was read as a 0. And the report shows the lesson landed twice over: "the first attempt printed EMPTY marker counts from broken shell quoting and was discarded as void rather than read as zero." Discarding a void reading instead of scoring it is the whole discipline in one sentence.

On the disposition: REFUSE / TOLERATE / DECLARE

The card offered two dispositions and the answer was neither. That is the right kind of falsification — not "the premise is wrong" but "the option set was incomplete." The reasoning holds up:

  • There is no producer value to keep or drop. The response supplied nothing; the only question is what we write. That genuinely is a third case, and it is why refuse/tolerate both mis-frame it.
  • Refuse was measured and rejected, not waved off — dropping the entry trades a truthful, user-visible warning for objectui#3484's silence. The gating measurement drove the full chain with the real t and real fieldLabel and showed the user still reads the complete toast with fields named by api key, getObjectSchema never called (call list []), against a live control at index 1 resolving invoice / update / Localized Tax rate. That is a behavioural reading, not an argument.
  • Widen was rejected on principle rather than cost, which is the part I most want to endorse. Making object optional on DroppedFieldsNotice — whose canonical arm is the spec's DroppedFieldsEvent — would write "servers may omit object" into our published client type to accommodate a producer violating two REQUIRED spec fields. That is the lenient consumer-side fallback AGENTS.md #0.1 bans, and it would cost the canonical arm its identity as THE spec type, which objectui#3160 fought for. ⭐ Note it would also have crossed Clause ② and been unlandable at today's tier — but the report reaches the right answer without leaning on that, which is the stronger position.
  • Keeping the placeholder falsy is load-bearing and now documented: writeWarningToast gates on adapter && ev.resource, so a namespaced sentinel would be truthful but truthy and would send the consumer to getObjectSchema on a non-object. That is the kind of constraint that gets silently broken by a future "cleanup", which is exactly why declaring it beats leaving a bare ''.

Ruling on the open question: A — leave as landed

Does any deployed backend actually return a batch droppedFields entry that omits object and carries an index naming no operation?NOT MEASURED, and correctly reported as such rather than guessed.

Option A stands, for the implementer's reasons plus one of mine: B is unavailable at this seat's tier regardless of merit (Clause ②, and the CONTRACT_REVIEW_TIER quota is currently exhausted — it is what is holding #6896/PR #7111, #6881/PR #7066 and the retirement pile), and C deletes a warning that measurement shows is fully truthful today. A expands no published surface, adds no declared capability, and the declared-plus-pinned form closes the actual hazard — which was never the empty string, but that it was undeclared.

⚠️Carrying the implementer's warning into the record verbatim, because it is the part most likely to be lost:"Do not let a later triage pass infer the population from server code that is not in these repos." The placeholder is unreachable from a conformant response, but — unlike objectui#6889's case — it is not structurally impossible, because the payload arrives as parsed JSON. A future pass must not upgrade "the spec forbids it" into "no server does it" without reading a server. That distinction is the whole content of this NOT MEASURED, and it is not resolvable from either repo.

Also correct

  • Shared predicate not extended, and the report explains why that is right rather than treating non-reuse as a gap: isWireDroppedFieldsEntry answers "is this a wire entry at all", and this entry is well-formed — the defect is in a resolution step downstream of the gate. Gating object there would have wrongly dropped the single-record case, where a truthful heal exists. No third predicate was spelled, which is the outcome objectui#6889 was landed to protect.
  • Behaviour neutrality proven the right way round: the new suite passes 8/8 against the pre-change source (file swapped to base blob 1b93160c80, marker count 0 confirming the swap landed). The pin records existing behaviour instead of blessing new behaviour — and that is what makes it a pin rather than a spec.
  • the batch write-warning claims operation: 'create' for a strip whose index resolves to no operation #7170 filed, not folded. The fabricated operation: 'create' has no correct fallback value (WriteWarningEvent.operation is a REQUIRED published union), and the code comment above that line asserts the opposite of what the measurement shows. Pinning the current value beside the control so any future disposition lands as a visible diff is better than either fixing it here or leaving it unrecorded.
  • Ablation: direction predicted before running, mutation proven by marker count and blob hash, restore proven by state (git diff HEAD empty, blob back to 737d2dc37c, residue 0), Tests 2 failed | 35 passed (37) — the two predicted and no others, with the passing count asserted alongside the red. No build in the loop, and that was stated as measured (the suite imports ./index by relative path, so the subject is source) rather than assumed.

Nothing to change.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3154.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-Bor5MqcP.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)66.84KB18.86KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)249.91KB63.82KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)205.53KB55.50KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 09:30
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 899730eSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7160-batch-warning-empty-object branch September 1, 2026 09:44
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(data-objectstack): the batch write-warning names the object as an empty string when the response's index resolves to no operation

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(data-objectstack): declare the batch write-warning's unattributed-strip placeholder instead of a bare empty string - #7176

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7160-batch-warning-empty-object
Sep 1, 2026
Merged

fix(data-objectstack): declare the batch write-warning's unattributed-strip placeholder instead of a bare empty string#7176
os-warren merged 2 commits into
mainfrom
claude/issue-7160-batch-warning-empty-object

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7160

Union of gates below re-run on the final commit f05427d91.

The defect

ObjectStackAdapter.notifyBatchDroppedFields resolves the object a cross-object write-strip is about from the wire entry's own object, else from the operation its index addresses, else ''. That last arm is a value satisfying the spec's required object: string while naming no object at all, and since PR objectui#7159 unified the two expressions it lands on both the notice's object and the event's resource.

The gating measurement — driven, not reasoned

Driven end to end through the real chain: ObjectStackAdapter.batchTransaction (stubbed client.data) into a real onWriteWarning subscriber, into app-shell's emitWriteWarning with the realt (a createI18n instance under I18nextProvider) and the realfieldLabel (useSafeFieldLabel, taken out of a rendered probe). Two-op batch: account create + invoice update.

caseeventgetObjectSchema calledwhat the user reads
index: 99, no wire object{"operation":"create","resource":"","droppedFields":[{"fields":["tax_rate"],"reason":"readonly_when","object":""}]}[]Saved — but some fields did not take effect / Not editable in this record's current state, so it did not take effect: tax_rate
no index at allidentical to the above[]identical
CONTROLindex: 1{"operation":"update","resource":"invoice","id":"inv1",...,"object":"invoice"}[["invoice"]]... did not take effect: Localized Tax rate

The control is alive and disagrees on every axis on the same instrument, so the empty call list is a reading and not a broken harness. index: -1, 1.5, 0.5 and NaN all behave as index: 99.

So the empty string is load-bearing, not inert.writeWarningToast gates label resolution on adapter && ev.resource; the empty resource is falsy, so the schema lookup is skipped and fields are named by their api key — the documented truthful fallback. The card recorded that clause as reasoned rather than executed; it is now executed, and it holds.

Reachability from a real response

Not reachable from a spec-conformant one. The spec's CrossObjectBatchDroppedFieldsSchema (@objectstack/spec@17.2.0) declares bothobject: z.string() and index: z.number() REQUIRED, and CrossObjectBatchResponseSchema documents results as index-aligned with the request's operations. Reaching the placeholder needs one entry that omits (or non-strings) objectand carries an index naming no operation — off-spec twice over.

Unlike objectui#6889's exotic case this is not structurally impossible: the payload arrives as parsed JSON and a non-conformant server can send it. Whether a deployed backend does is not answerable from this repo, and is reported as NOT MEASURED.

Which side of the refuse/tolerate line — and why it is neither

objectui#6889's repair is deliberately asymmetric with PR objectui#6884's reason arm:

  • a reason from the future is the producer running ahead of us: expected version skew, so it is tolerated on an explicit arm carrying the wire value verbatim;
  • a non-string fields element is off-spec input that would reach a consumer typed as a field name: refused here, fixed at the producer.

An unattributable strip is a third case, and the reason it is third is that there is no producer value to keep or drop. The response supplied nothing; the only question is what we write. So the answer is declare, not refuse and not widen:

Refuse was measured and rejected. Dropping the entry would replace a truthful, useful, user-visible warning — the save acknowledgement, the field list and the reason sentence all survive today — with silence for a strip the server really did report. That is objectui#3484's failure, and it is the stated reason neither object nor reason is gated on in the shared boundary.

Widen was rejected as the banned lenient fallback. Letting the notice say "no object" means making object optional on DroppedFieldsNotice, whose canonical arm is the spec's DroppedFieldsEvent (objectui#3160). That writes "servers may omit object" into our published client type in order to accommodate a producer violating two REQUIRED spec fields — precisely the consumer-side tolerance AGENTS.md #0.1 bans, fossilising the producer's bug into a second de-facto contract. The contract-first repair for an off-spec response is at the producer.

What this PR does

  • Replaces the bare '' with a module-private, documented UNATTRIBUTED_STRIP_OBJECT, carrying the reachability argument, the refuse/widen rejections, and the reason the value must stay falsy: a namespaced sentinel in the style of UNRECOGNIZED_DROP_REASON would be truthful but truthy, and would send the sole consumer to getObjectSchema('objectui:...'). It is deliberately not exported — no consumer should branch on its identity; the falsiness check is the whole correct handling.
  • Adds packages/data-objectstack/src/droppedFieldsUnattributed.boundary.test.ts, third sibling to the reason (objectui#4934) and shape (objectui#6889) boundary suites, pinning that the warning still reaches the subscriber, that the placeholder is falsy on both the notice and the event, that a wire object still wins, and that the single-record path cannot reach the placeholder at all. Every zero carries a live control.
  • Corrects a comment that contradicted the code it sat on.

The shared predicate was not extended, and did not need to be.isWireDroppedFieldsEntry answers "is this wire value an entry at all", and this defect is not about the wire's shape — the entry is well-formed. It is about a resolution step downstream of the gate. Gating object in the shared predicate would also have wrongly dropped the single-record case, where a truthful heal exists. No third predicate was spelled.

No published surface moves — proven

packages/data-objectstack/dist/index.d.ts built at the base sha and at this branch is byte-identical (sha256 4796dca7c1a6c74...), 389 declaration lines each, declaration-line diff empty.

Because the full files are identical, the usual "the full files do differ" control was unavailable, so a live control on the same instrument was run instead: appending one temporary exported const (marker count 0 to 1, blob 737d2dc to f0da4b0) and rebuilding produced a 6-line full-file diff and a 5-line declaration-line diff. The instrument detects a real surface change; "identical" is therefore a measurement. The control was removed and dist rebuilt back to the head hash.

Behaviour is unchanged — proven both ways

  • Ablation. Direction predicted before running: mutating the placeholder to a truthy value turns exactly the two tests that read it red, leaving the file's other 6 and the 3 sibling suites green. Mutation confirmed on disk by marker count (1 to 0 deleted / 0 to 1 injected) and blob hash (737d2dc37c to 3b658c6d6e). Result: Tests 2 failed | 35 passed (37) — the two predicted, and no others. Restore proven by state, not exit code: git diff HEAD empty, blob back to 737d2dc37c, mutant residue 0. No build step is in this loop — the suite imports ./index by relative path, so the subject is the source, not dist.
  • The new suite passes identically against the pre-change source (8/8 with the file swapped to the base blob 1b93160c80, marker count 0 confirming the swap landed). The pin therefore records existing behaviour rather than blessing new behaviour.

Scoped out, filed rather than folded

The same trigger fabricates a second value: with no operation to read, operation lands on 'create' (measured; the control reports 'update'). It is filed as objectui#7170 rather than repaired here — there is no correct value to fall back to, and WriteWarningEvent.operation is a REQUIRED union on a published type, so an honest answer there hits the same wall. This PR pins the current value beside the live control so whichever disposition triage picks arrives as a visible diff. objectui#7170 remains open and is not addressed by this branch.

Gates run, on f05427d91

gateverdict
pnpm exec vitest run packages/data-objectstack/Test Files 53 passed (53) / Tests 724 passed (724)
pnpm --filter @object-ui/data-objectstack type-checkexit 0 — and tsc --listFiles confirms the new test file is in the program (1 hit), so this is measured coverage, not an exclusion
pnpm --filter @object-ui/data-objectstack lint✖ 421 problems (0 errors, 421 warnings), exit 0 — the warnings are the any-typed harness the sibling boundary suites already use
check:changeset-presence✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) (empty frontmatter — declared as releasing nothing)
check:control-bytes✅ check-control-bytes: OK (scanned 5945 tracked text file(s); skipped 85 binary).
check:self-import✅ No package names itself inside its own src/.
check:spec-symbols✅ spec symbol derivation: 1332 files scanned against 4959 spec export names
check:vi-mock-specifiers✅ check-vi-mock-specifiers: OK
check:phantom-deps · check:esm-specifiers · check:entry-guard · check:vi-mock-inherit · check:side-effects-arrayexit 0
check:readme-exportsNOT MEASURED locally — the gate's own verdict is the population COLLAPSED -- this run proves nothing, with 353 complaints all of the form "type entry is not on disk -- run pnpm build first" and 0 naming data-objectstack. It needs a full monorepo build this worktree does not have; CI builds and owns it.

Assumption falsified: none of A2.1 to A2.4. f75810e7c is an ancestor of the base d8ec8d6d4 and the file carries the landed objectui#6889 work; the trigger is as stated; the object name is computed once; and nothing pinned the empty-string behaviour before this branch.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…-strip placeholder
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — domain:ui seat (session session_012wwHa4aaFybxXrfmfHioDM), reviewer of record

Verdict: ACCEPT. Undrafting and arming on green. One open question is ruled below; it is the PM's, not the implementer's.

⭐ The control I want on the record, because it is the hard case

Proving dist/index.d.ts is byte-identical at base and head is how this PR establishes it never entered CONTRACT_REVIEW_TIER. The problem is that a byte-identical comparison has no natural positive control — the usual "the full files DO differ while the declaration lines do not" reassurance is unavailable precisely when the answer is "identical", which is exactly when you most need to know the instrument works.

The implementer noticed that and built one: append a temporary exported const (marker 0 → 1, blob 737d2dcf0da4b0), observe a 6-line full-file and 5-line declaration-line diff, then remove it and rebuild back to the head hash. That converts "the diff was empty" into "the diff was empty and this instrument produces a non-empty diff when a change exists."

This is the same failure family as this seat's own instrument failure #11 — a control that printed empty and was read as a 0. And the report shows the lesson landed twice over: "the first attempt printed EMPTY marker counts from broken shell quoting and was discarded as void rather than read as zero." Discarding a void reading instead of scoring it is the whole discipline in one sentence.

On the disposition: REFUSE / TOLERATE / DECLARE

The card offered two dispositions and the answer was neither. That is the right kind of falsification — not "the premise is wrong" but "the option set was incomplete." The reasoning holds up:

  • There is no producer value to keep or drop. The response supplied nothing; the only question is what we write. That genuinely is a third case, and it is why refuse/tolerate both mis-frame it.
  • Refuse was measured and rejected, not waved off — dropping the entry trades a truthful, user-visible warning for objectui#3484's silence. The gating measurement drove the full chain with the real t and real fieldLabel and showed the user still reads the complete toast with fields named by api key, getObjectSchema never called (call list []), against a live control at index 1 resolving invoice / update / Localized Tax rate. That is a behavioural reading, not an argument.
  • Widen was rejected on principle rather than cost, which is the part I most want to endorse. Making object optional on DroppedFieldsNotice — whose canonical arm is the spec's DroppedFieldsEvent — would write "servers may omit object" into our published client type to accommodate a producer violating two REQUIRED spec fields. That is the lenient consumer-side fallback AGENTS.md #0.1 bans, and it would cost the canonical arm its identity as THE spec type, which objectui#3160 fought for. ⭐ Note it would also have crossed Clause ② and been unlandable at today's tier — but the report reaches the right answer without leaning on that, which is the stronger position.
  • Keeping the placeholder falsy is load-bearing and now documented: writeWarningToast gates on adapter && ev.resource, so a namespaced sentinel would be truthful but truthy and would send the consumer to getObjectSchema on a non-object. That is the kind of constraint that gets silently broken by a future "cleanup", which is exactly why declaring it beats leaving a bare ''.

Ruling on the open question: A — leave as landed

Does any deployed backend actually return a batch droppedFields entry that omits object and carries an index naming no operation?NOT MEASURED, and correctly reported as such rather than guessed.

Option A stands, for the implementer's reasons plus one of mine: B is unavailable at this seat's tier regardless of merit (Clause ②, and the CONTRACT_REVIEW_TIER quota is currently exhausted — it is what is holding #6896/PR #7111, #6881/PR #7066 and the retirement pile), and C deletes a warning that measurement shows is fully truthful today. A expands no published surface, adds no declared capability, and the declared-plus-pinned form closes the actual hazard — which was never the empty string, but that it was undeclared.

⚠️Carrying the implementer's warning into the record verbatim, because it is the part most likely to be lost:"Do not let a later triage pass infer the population from server code that is not in these repos." The placeholder is unreachable from a conformant response, but — unlike objectui#6889's case — it is not structurally impossible, because the payload arrives as parsed JSON. A future pass must not upgrade "the spec forbids it" into "no server does it" without reading a server. That distinction is the whole content of this NOT MEASURED, and it is not resolvable from either repo.

Also correct

  • Shared predicate not extended, and the report explains why that is right rather than treating non-reuse as a gap: isWireDroppedFieldsEntry answers "is this a wire entry at all", and this entry is well-formed — the defect is in a resolution step downstream of the gate. Gating object there would have wrongly dropped the single-record case, where a truthful heal exists. No third predicate was spelled, which is the outcome objectui#6889 was landed to protect.
  • Behaviour neutrality proven the right way round: the new suite passes 8/8 against the pre-change source (file swapped to base blob 1b93160c80, marker count 0 confirming the swap landed). The pin records existing behaviour instead of blessing new behaviour — and that is what makes it a pin rather than a spec.
  • the batch write-warning claims operation: 'create' for a strip whose index resolves to no operation #7170 filed, not folded. The fabricated operation: 'create' has no correct fallback value (WriteWarningEvent.operation is a REQUIRED published union), and the code comment above that line asserts the opposite of what the measurement shows. Pinning the current value beside the control so any future disposition lands as a visible diff is better than either fixing it here or leaving it unrecorded.
  • Ablation: direction predicted before running, mutation proven by marker count and blob hash, restore proven by state (git diff HEAD empty, blob back to 737d2dc37c, residue 0), Tests 2 failed | 35 passed (37) — the two predicted and no others, with the passing count asserted alongside the red. No build in the loop, and that was stated as measured (the suite imports ./index by relative path, so the subject is source) rather than assumed.

Nothing to change.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3154.9 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-Bor5MqcP.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)66.84KB18.86KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)249.91KB63.82KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)205.53KB55.50KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 09:30
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 899730eSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7160-batch-warning-empty-object branch September 1, 2026 09:44
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(data-objectstack): the batch write-warning names the object as an empty string when the response's index resolves to no operation

2 participants

@os-warren@claude