Skip to content

fix(fields): a file URL whose last segment holds a bare % renders, named by its raw segment (objectui#10614) - #10653

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-10614-file-name-bare-percent
Sep 25, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-10614-file-name-bare-percent

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #10614

Clause-②: no

What changed

nameFromUrl in packages/fields/src/widgets/file-value.ts names a file value that carries no name after its URL's last path segment, decoded with decodeURIComponent. A segment that is not a valid percent-encoding made the decode throw URIError: URI malformed, and the throw left readFileValue during render. The decode is now guarded once, in that helper. On URIError the name is the raw segment; any other error is rethrown. No caller changed, and no caller has its own try/catch (triage ruling on the card, comment 5833345168). A valid escape decodes exactly as before.

Files: packages/fields/src/widgets/file-value.ts (the guard and its doc comment), packages/fields/src/widgets/file-value.test.ts (helper pins), packages/fields/src/widgets/file-value.bare-percent-10614.test.tsx (rendered-face pins, new), .changeset/10614-file-name-bare-percent.md (patch, @object-ui/fields).

Reach, measured first at the base eca2760

A scratch harness (not committed) mounted every face that reaches the helper. Values: https://cdn.example.com/100%.png (string) and { url: 'https://cdn.example.com/a%zz.pdf' } (object).

Face At the base After the fix
file cell (FileCellRenderer), string and array throws URIError: URI malformed link named 100%.png / a%zz.pdf
image cell (ImageCellRenderer), string and object throws URIError: URI malformed renders
signature cell (via getCellRenderer) throws URIError: URI malformed renders
FileField edit and readonly throws URIError: URI malformed renders, name shown
FileCell throws URIError: URI malformed renders, name shown
ImageField edit and readonly throws URIError: URI malformed renders
object-grid through SchemaRenderer, one bad row plus one valid row the data-table node's error boundary replaces the WHOLE table with its panel Component "data-table" failed to render / URI malformed / Retry. Neither row is visible. both rows visible, no alert
object-gallery through SchemaRenderer, one bad cover plus one valid the gallery's boundary replaces the whole gallery with Component "object-gallery" failed to render / URI malformed / Retry both cards visible, no alert
control: file cell with report%20q3.pdf report q3.pdf report q3.pdf

So at the base, no face contained the throw. Under SchemaRenderer, the nearest error boundary caught it, and one such value hid the whole table or gallery, valid rows included. The measurement did not produce a blank page, because SchemaRenderer wraps every node in a boundary. The console's list route reaches the grid through ListView, which renders it through SchemaRenderer. ObjectGallery's cover (resolveCoverUrl calls readFileValues) is a caller the card did not list. git grep found it, and it was measured above.

Pins, and the ablation

  • file-value.test.ts, the describe block whose name ends names the file by its raw segment (objectui#10614). It pins both reported values (no throw, name is the raw segment), the two together through readFileValues, and THE CONTROL: report%20q3.pdf still decodes to report q3.pdf, both as a string and as { url }.
  • file-value.bare-percent-10614.test.tsx renders the file cell through getCellRenderer('file'). It checks both values as links named by the raw segment with the right href, a single non-array value, and the control decoding beside a bare-% value in the same cell.
  • Ablation, run on the committed fix 551e8ed with ablation-replace.mjs (wrap mode, anchor if (err instanceof URIError) return seg; deleted, so the throw escapes again as it did at the base). It printed anchor x1 -> x0 and blob a2c261c8e7d4 -> e873660a8615. The two pin files then read Tests 6 failed | 39 passed (45), command exited 1, and every failure is URIError: URI malformed. The helper control stays green in that run, as a control should. Restore: blob == HEAD (a2c261c8e7d4) and git diff HEAD is empty. Direction observed: red, as expected.

Gates, at head 1980ada (after merging origin/main f99f9cd)

  • pnpm exec vitest run packages/fields/: exit 0, Test Files 209 passed | 1 skipped (210), Tests 3399 passed | 7 skipped (3406)
  • pnpm turbo run type-check --filter @object-ui/fields --concurrency=2: exit 0, Tasks: 12 successful, 12 total, and @object-ui/fields:type-check: cache miss, executing, so it ran rather than replaying a cache. tsc -p tsconfig.test.json --listFiles lists both pin files, so the test program covers them.
  • pnpm exec eslint on the three changed source and test files: exit 0, no problems
  • pnpm check:control-bytes, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:new-line-citations (0 new), check:changeset-claims, check:pending-changeset-literals, check:unreferenced-sources: exit 0 each
  • node scripts/check-changeset-presence.mjs, check-changeset-no-major.mjs, check-changeset-overwrite.mjs, check-lint-coverage.mjs: exit 0 each
  • The repo-wide pnpm lint and the full test shards belong to CI.

Acceptance notes

  • The boundary granularity is an observation, not a filed card. SchemaErrorBoundary sits at the schema node, so any throw from one cell renderer blanks the whole data-table node, not the cell. This card removes the throw. It does not change where boundaries sit.
  • The fix names a file by its raw segment. It does not validate the URL. The href is the value's own URL, unchanged.

Generated by Claude Code

…named by its raw segment

`nameFromUrl` passed a URL's last path segment to `decodeURIComponent`
unguarded, so a segment that is not a valid percent-encoding
(`https://cdn.example.com/100%.png`, `{ url: 'https://cdn.example.com/a%zz.pdf' }`)
threw `URIError: URI malformed` out of `readFileValue` during render. The
decode is now guarded once, in that helper: on `URIError` the name is the raw
segment. A valid escape (`report%20q3.pdf`) decodes as before.

Pins: both reported values at the helper and through the `file` cell renderer,
plus the valid-escape control.

Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3051.3 KB 3104.5 KB
Main entry chunk (gzip) 147.9 KB 350 KB
Entry file index-C0oqkSMe.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.57KB 6.15KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.17KB 10.58KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 546.99KB 130.83KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.91KB 62.28KB
fields (index.js) 260.14KB 65.89KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.49KB 14.64KB
plugin-charts (index.js) 74.94KB 20.89KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.60KB 35.41KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 232.96KB 61.65KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 152.20KB 39.05KB
plugin-gantt (index.js) 169.64KB 41.91KB
plugin-grid (index.js) 215.37KB 58.92KB
plugin-kanban (index.js) 48.35KB 15.08KB
plugin-list (index.js) 114.58KB 28.31KB
plugin-map (index.js) 22.42KB 7.38KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.67KB 8.95KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.84KB 21.96KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 116.21KB 38.14KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1980ada45bd4317d6b59a8411bf99ad79c7d0d94

Read-only review of #10653 (card objectui#10614) against refs/review/pr-10653 (head 1980ada) and refs/review/main (f99f9cd, which is also the merge-base). Diff: 4 files, +148/-1 — packages/fields/src/widgets/file-value.ts (+17/-1), packages/fields/src/widgets/file-value.test.ts (+35/-0), packages/fields/src/widgets/file-value.bare-percent-10614.test.tsx (new, +73), .changeset/10614-file-name-bare-percent.md (new, +23). Zero GitHub writes were made. One deviation from the brief's read list: /home/user/objectui/node_modules does not exist, so FileValueSchema was read from the same @objectstack/spec@17.4.0 dist installed under a sibling worktree's node_modules/.pnpm (read-only, no worktree file touched). The os-dev-report comment was not used as evidence; every claim below is against the diff, git, the REST reads and a scratch probe (pr-10653/probe-nameFromUrl.mjs) of the base and head helper bodies.

① Derived judgments

Binding text. Triage 5833345168: 「guard the decode in the ONE helper (fall back to the raw segment on URIError); ⛔ no per-caller try/catch. Pin both reported values, plus a valid escape as the control, and state what a cell showed before the fix.」 and 「How far it reaches (a contained cell, or a blank page) is the claimant's first measurement.」 The card: nameFromUrl in file-value.ts passes the last segment to decodeURIComponent unguarded; reported values https://cdn.example.com/100%.png and { url: 'https://cdn.example.com/a%zz.pdf' }.

  1. Behaviour change, packages/fields/src/widgets/file-value.ts:135-141 (head). Base L136 was return seg ? decodeURIComponent(seg) : url;. Head: if (!seg) return url; then try { return decodeURIComponent(seg); } catch (err) { if (err instanceof URIError) return seg; throw err; }. RIGHT against the triage: the guard is in the one helper, the fallback on URIError is the raw segment seg, and any other error is rethrown. Probe over the base and head bodies: base throws URIError: URI malformed on both reported values, head returns 100%.png and a%zz.pdf; report%20q3.pdf decodes to report q3.pdf on both; the empty-segment arm (https://cdn.example.com/ → cdn.example.com) and the data: arm (undefined, L132) are unchanged; a patched decodeURIComponent throwing TypeError propagated out of the head helper, and a real decode failure is instanceof URIError in this realm. The restructure of the empty-segment ternary into an early return is semantically identical.

  2. "ONE helper" and "no per-caller try/catch": holds. decodeURIComponent is called at exactly one site in packages/fields/src (non-test): file-value.ts:137. The diff touches no other source file. Every caller of the helper is unchanged at head and carries the same try/catch count as at the base, none of which wraps the call: packages/fields/src/index.tsx:2199 and :2241 (FileCellRenderer), :2262 (displayableImagesOf, read by ImageCellRenderer L2284 and SignatureCellRenderer L2384), packages/fields/src/widgets/FileField.tsx:169 (FileField) and :487 (FileCell), packages/fields/src/widgets/ImageField.tsx:84, packages/plugin-list/src/ObjectGallery.tsx:238 (resolveCoverUrl, the caller the PR body names beyond the card's five). No caller gained a try/catch.

  3. Accepted set and published surface: nothing moves. nameFromUrl is module-private (L131, not exported); readFileValue (L150) and readFileValues (L190) keep their signatures and return shapes; no export is added or removed in packages/fields/src/index.tsx (unchanged). The inputs in question were contract-valid before the fix — FileValueSchema in spec 17.4.0 (dist/ui/index.js:825-832) is looseObject({ url: z.string(), name: optional, size: optional, mimeType: optional, alt: optional, duration: optional }) — so the fix stops an incidental throw on already-accepted values rather than widening an accept set.

  4. New helper pins, packages/fields/src/widgets/file-value.test.ts:154-181. L155-159 pins the string value: not.toThrow() and toEqual({ url, name: '100%.png', raw: url }), which is exactly the string arm's return shape at L160. L161-167 pins the object value: name a%zz.pdf, url unchanged. L169-175 pins both through readFileValues. L177-180 THE CONTROL: report%20q3.pdf → report q3.pdf as a string and as { url }. Negative leg: at the base both values throw (probe; base L136), so L157, L163 and L170 go red at the base, and deleting head L139 (the ablation the PR body describes) leaves catch (err) { throw err; }, which is the base behaviour byte-for-byte in effect. Lit control: L177-180 is green at the base (the base already pinned report%20v2.pdf → report v2.pdf at base test L104-109) and green at the head, and would go red on a fix that returned the raw segment unconditionally or skipped the decode. So the helper pins can fail in both directions and the control distinguishes.

  5. New rendered pins, packages/fields/src/widgets/file-value.bare-percent-10614.test.tsx:57-72, through getCellRenderer('file') (index.tsx:3587) → FileCellRenderer (index.tsx:2142) → readFileValues L2199 for an array / readFileValue L2241 for a scalar → FileValueAffordance (packages/fields/src/widgets/file-affordance.tsx:87-101): the anchor's href is view.url (L82, L88) and its accessible name is view.name in the span at L100 (the icon is aria-hidden, L99). So getByRole('link', { name }) plus toHaveAttribute('href', url) are the right observables, and the assertions match the code. The fileField literal (L42) is a well-typed FileFieldMetadata (packages/types/src/field-types.ts:628-634: type: 'file', multiple?: boolean; BaseFieldMetadata requires only name). The file is collected by the root dom project (vitest.config.mts:396, packages/**/*.test.tsx) and type-checked by packages/fields/tsconfig.test.json (src/**/*.test.tsx).

  6. The rendered CONTROL (tsx L67-71) shares one cell with BARE_PERCENT. Can it fail on its own for the failure it is meant to catch? Yes: a fix that broke valid-escape decoding (raw segment returned always, or decode skipped) renders a link named report%20q3.pdf, and getByRole('link', { name: 'report q3.pdf' }) fails. But it is not a lit control under the ablation: with the guard removed the whole cell throws on the sibling value, so this test goes red for the guard's absence and cannot separate "guard gone" from "decode broken". The PR body states this honestly (its ablation count 6 failed | 39 passed (45) is consistent with the static count: 42 tests in file-value.test.ts including 10 it.each rows, plus 3 in the tsx file; the 6 are the three non-control helper pins and all three rendered pins, and "the helper control stays green"). The lit control is the helper one (item 4). Not a defect.

  7. Existing pins edited or deleted: none. file-value.test.ts is +35/-0; the base's valid-escape pin (base L104-109) and the objectui#10493 data: pins (base L112-146) are untouched. No guard was weakened.

  8. Asked and done: "state what a cell showed before the fix" — stated in the PR body's face table, changeset paragraph 2, the file-value.ts doc comment L124-127 and the tsx header L16-22 (the latter worded as "a historical reading, not re-derived here"). "Pin both reported values, plus a valid escape as the control" — items 4 and 5. Asked and missing: nothing. The claim's "render a file cell and an image field … and say what the screen showed at the base" is answered in the PR body from an uncommitted harness; the triage did not require committed rendered pins per face, and the committed rendered pin covers the file cell.

  9. Done and not asked: the doc comment L121-129 and the tsx header prose (judged in ②); the early-return restructure (no behaviour change). Nothing outside the claimed file surface (file-value.ts helper, pins beside the existing file-value tests, one changeset) was changed.

  10. Doc comment file-value.ts:121-129, sentence by sentence: "A segment that is not a valid percent-encoding … names the file by its raw segment" — true (L139). "FileValueSchema.url is a plain string, so such a value is contract-valid, and decodeURIComponent throws URIError on it" — true (spec L826; probe). "Before this guard that throw escaped readFileValue during render, so one such value took down every face that reads it" — true by code: base L136 had no catch, and every caller in item 2 calls during render with no catch. "the nearest error boundary replaced the whole table or gallery, valid rows included" — true by code: SchemaRenderer.tsx:2187-2230 wraps every schema node in SchemaErrorBoundary, whose fallback (L725-742) replaces the node's output with Component "…" failed to render, the error message and Retry; ObjectGrid renders its rows as one data-table node (ObjectGrid.tsx:4609), so the boundary replaces the whole table including valid rows. "The guard lives here, once, because every caller reaches the decode through this helper" — true (item 2). "a valid escape (report%20q3.pdf) still decodes as before" — true (probe; test L177-180).

② Semver level

.changeset/10614-file-name-bare-percent.md declares '@object-ui/fields': patch. Correct: the diff changes packages/fields/src/** of a package in the fixed group of .changeset/config.json (@object-ui/fields is listed), so AGENTS.md L168 requires a changeset; the change is a bug fix with no API, export or schema movement, so patch is the right level; it is not major (AGENTS.md L271, enforced by check-changeset-no-major.mjs). The change is not docs-only (no content/docs/** file is touched), so the empty-frontmatter form does not apply. The fixed group bumps all 39 packages together on a patch, which is the repo's convention.

Changeset prose, sentence by sentence against the diff:

  • Title "fix(fields): a file URL whose last segment holds a bare % renders, named by its raw segment (objectui#10614)" — TRUE (L139; tsx L57-61).
  • "readFileValue names a file value that carries no name of its own after the last segment of its URL, decoded with decodeURIComponent." — TRUE (L160; L170-174; L133-137).
  • "A segment that is not a valid percent-encoding (https://cdn.example.com/100%.png, or { url: 'https://cdn.example.com/a%zz.pdf' }) made that decode throw URIError: URI malformed, and the throw left readFileValue during render." — TRUE (base L136; probe; callers in ① item 2 run during render with no catch).
  • "FileValueSchema.url is a plain string, so such a value passes the contract." — TRUE (spec 17.4.0 dist/ui/index.js:826, url: zod.z.string()).
  • "What a user saw: every face that reads the value (the file, image and signature cells, FileField, FileCell, ImageField, and a gallery's cover) threw while rendering." — TRUE by code reading: each named face is a real component (index.tsx:2142, :2280, :2383; FileField.tsx:137, :441; ImageField.tsx:46; ObjectGallery.tsx:230-239) that calls readFileValue(s) on its render path with no catch. The per-face rendering was confirmed by the PR's uncommitted harness; the sentence is code-derivable, not harness-only.
  • "Under SchemaRenderer the nearest error boundary caught it, so one such value replaced a whole grid table, valid rows included, or a whole gallery with the "failed to render" panel." — TRUE by code: SchemaRenderer.tsx:2187 (per-node SchemaErrorBoundary), fallback text at L725-742, data-table node at ObjectGrid.tsx:4609. The exact panel wording is the boundary's own.
  • "The decode is now guarded once, in the one helper that performs it: on URIError the name is the raw segment (100%.png, a%zz.pdf)." — TRUE (L136-141; sole decode site).
  • "A valid escape decodes as before (report%20q3.pdf is named report q3.pdf), and a value that carries its own name or original_name is unaffected." — TRUE (L137; L170-172 read name then original_name before nameFromUrl; test L177-180).

Docs/README prose added by the diff: none under content/docs/** or any README. The only added prose outside the changeset is the file-value.ts doc comment (judged true in ① item 10) and the tsx file header L9-26, whose measurement sentences are worded as a historical reading of the base (eca2760) and whose code sentences ("nameFromUrl … passed that segment to decodeURIComponent unguarded", "The helper-level pins, and the control … sit in file-value.test.ts") are true. One loose label, "renders the record-detail face, the file cell renderer", names the cell renderer that grids, galleries and detail views resolve through getCellRenderer; style, not a false claim.

③ Boundary flags

  • PR body flags and open questions: the body lists no open questions. Its own flags, answered: (a) the reach table comes from "a scratch harness (not committed)" and is worded as such in the body and in the tsx header; its code-derivable parts (no caller catches; node-level boundary; data-table node) are confirmed above. (b) "The boundary granularity is an observation, not a filed card … This card removes the throw. It does not change where boundaries sit." — true; the diff touches no boundary. (c) "The fix names a file by its raw segment. It does not validate the URL. The href is the value's own URL, unchanged." — true (file-affordance.tsx:82-88; tsx pins assert href equals the raw URL). (d) The ablation anchor if (err instanceof URIError) return seg; deleted leaves an unconditional rethrow, which is the base's behaviour though not the base's bytes — as the body says.
  • CI at head 1980ada, 0 failures at every poll (42 check-runs at polls 1 and 2, 43 at poll 3). Poll 1 (about 14:35Z): 28 success, 3 skipped (Test (coverage), dependabot, Test (coverage shard …/4)), 11 in_progress. Poll 2 (14:44Z): 31 success, 3 skipped, 8 in_progress (Spec Main Shape Gate, Test (shard 1,3,4,5,6,7/8), Type Check). Poll 3 (14:47Z, final): 43 check-runs, 40 success, 3 skipped (the same three path-skipped coverage/dependabot runs), 0 in_progress, 0 failure. CI at the head has converged green; the 43rd run appeared between polls and completed with success.
  • Draft: yes (draft: true), which is what AGENTS.md L336 ② requires of a dispatched dev's PR (the PR carries the card's Claim:); labels tests, package: fields; assignee os-tesla; mergeable_state: blocked (a draft PR behind the merge-queue ruleset; not a failing check).
  • Clause-②: no holds: no accept set moves (the values were already contract-valid under FileValueSchema.url: z.string()), no schema changes, no published export is added, removed or re-signed (nameFromUrl is private; readFileValue/readFileValues unchanged in signature; index.tsx untouched).
  • Main under the PR's files: the merge-base is f99f9cd, refs/review/main after fetch is f99f9cd, and git ls-remote origin refs/heads/main at review time returned f99f9cd. Main has not moved since the merge-base; the PR's third commit 1980ada is a merge of that same f99f9cd.

Implemented-by: claude/issue-10614-file-name-bare-percent
Reviewed-by: session_01KUxVUa7e39aNjhkKi1gsoy

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 25, 2026 14:51
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit a9c5ea0 Sep 25, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10614-file-name-bare-percent branch September 25, 2026 15:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants