fix(plugin-list,plugin-view): retire the 'created_at' timeline date-axis floors so the refusal becomes reachable - #7500

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-7070-step3-drop-created-at-floors
Sep 3, 2026
Merged

fix(plugin-list,plugin-view): retire the 'created_at' timeline date-axis floors so the refusal becomes reachable#7500
os-project-manager merged 2 commits into
mainfrom
claude/issue-7070-step3-drop-created-at-floors

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Part of #7070

Step ③ of the maintainer ruling of 2026-09-01 (总监批 #28, comment 5494805467): house posture 日期轴永不虚构 — a date axis is never fabricated. ListView and plugin-view's ObjectView stop flooring the timeline axis at 'created_at'.

Steps ①② are landed (20cb8db9b, PR #7467, card #7459) and this branch is cut from exactly that commit. ⛔ Nothing in ObjectTimeline, the refusal screen or the locale key is touched here.

The two lines

FaceWasNow
packages/plugin-list/src/ListView.tsx:2496startDateField: dateBinding.startDateField || 'created_at'conditional spread — the key is absent when no axis resolves
packages/plugin-view/src/ObjectView.tsx:1354startDateField: ...startDateField || ...dateField || 'created_at'alias resolved into a local, then the same conditional spread

Line numbers re-derived on main at 20cb8db9b, not inherited from the card. created_at occurs 4 times in ListView.tsx and 1 in ObjectView.tsx; those are occurrences of the string, not floors. Read per line, exactly one floor per file:

  • ListView.tsx:1753s.add('created_at') in knownObjectFields. A platform-column reference, kept.
  • ListView.tsx:1785 — prose explaining the PLATFORM_RECORD_COLUMNS carve-out, kept.
  • ListView.tsx:2496the floor. Deleted.
  • ObjectView.tsx:1354the floor. Deleted.

titleField keeps its 'name' floor at both faces: not a date axis, and the same display-name rung the gallery / kanban / gantt branches carry. Pinned as scope in both new suites.

Why it was never a harmless default

ObjectTimeline reads the flatschema.startDateField at the tail of its resolver chain — plugin-timeline's own CONTROL block proves that rung is fully honoured — so a floor here answered "the axis is bound" for every view and the refusal step ① installed was unreachable from either route. That is exactly why #7459's own measurement recorded that nothing began refusing when ①② landed.

And the invented axis was never fetched: the $select projection is collected from the DECLARED timeline / options.timeline blocks (collectViewFields), never from this prop. An undeclared view therefore got a timeline bound to a column the query had not requested and bucketed every record into "No date" — the screen that looks built, is wrong, and gives the author no signal.

The ruling also explicitly replaces the written decision that stood on the deleted ListView line ("created_at stays the last resort for a view that declares no date axis anywhere"), and asks the objectui#3129 note at the app-shell face to be realigned once ③ lands. Both are done here; the app-shell edit is comment-only.

⭐ Blast radius — measured, not asserted

Which authored surfaces in this repo reach these floors: zero. Which begin refusing: zero.

Every sweep below was run with a control that FIRED in the same run, because a zero from a grep is not a reading.

  • Authored metadata (json/yml/yaml) declaring a timeline view: 4 hits, none of them reach either face. The three examples/schema-catalog/src/schemas/plugin-timeline/*.json documents and packages/types/examples/data-display-examples.json are direct type: 'timeline' schemas carrying authored items / events — they never traverse ListView or generateViewSchema, and hasAuthoredItems short-circuits the refusal predicate (!hasAuthoredItems && !startDateField) regardless. Control: the identical regex for "calendar" fires on components-complex-view-switcher/view-switcher.json:24.
  • Authored TS/TSX samples declaring a timeline view: zero outside tests. Control: the identical regex for 'calendar' fires on six sites.
  • appearance.allowedVisualizations naming timeline in authored metadata: zero.

Two product routes were then measured rather than reasoned about, and both are why the radius is small:

  1. The console's own authoring UI cannot produce an undeclared timeline view.CreateViewDialog's REQUIRED_FIELDS table lists timeline.startDateField among "the sub-config keys a view type must have set before a record can be persisted", with a smart default auto-picked from the object's real date fields (PRIMARY_DATE_PREFERRED). Anything created in-product carries a declared axis.
  2. Interface list pages auto-derive one.InterfaceListPage:413-414 fills timeline from defaultCalendarFromObject(objectDef) when timeline is whitelisted — a field the object declares, or undefined. So such a page refuses only when the object carries no date field at all, which is the correct answer: there is no axis to declare. This is derivation from schema, not fabrication, and the fix(timeline): honor startDateField when rendering a ListView timeline #3129 note already cites it as the good shape.

What is left is metadata authored outside those paths — hand-written or migrated views. Those begin refusing, which is what the ruling ordered, and the refusal names the keys to declare so an affected view reports its own fix. The changeset carries the migration note.

⛔ No replacement fallback was added anywhere.

Verification

Red-then-green, taken naturally first. Both face-level suites were written and run against the UNMODIFIED tree at 20cb8db9b before the first source edit: plugin-view 3 failed / 5 passed, plugin-list 3 failed / 14 passed — the failures exactly the "invents no axis" cases, every render proof and CONTROL green in the same run. After the edit: 25/25.

Ablation, for the console composition test, which was written after the fix and so had never been observed red. Both floors restored, proven on disk in both directions before the run (removed-text 0 AND injected-text 1 per file, plus a blob-hash change on each), trap ... EXIT INT TERM with absolute paths, restore proven by state (git diff HEAD empty + blob equality against the HEAD blob), never by an exit code. No build/dist is involved — vitest resolves @object-ui/* to sibling src/ through the root config's alias table — so there is no stale-artifact leg to prove.

Under the restored floors: 9 failed / 24 passed. The 9 are precisely the "invents no axis" and "refuses on screen" cases at all three levels; every render proof and every CONTROL stayed green in the mutated world. The DOM dump from that run is the point of the exercise — it shows data-testid="timeline-canvas" and the text Spring Launch: a healthy, convincing, entirely wrong timeline, not an error. That is what the pin has to be able to fail on.

⚠️ One anchor note, recorded because it is the failure this protocol exists to catch: the first ablation anchor matched 2 sites in ListView.tsx — the identical conditional-spread text also builds resolvedTimeline's nested config a few lines up, which this card does not touch. The assertion refused and the run aborted rather than mutating the wrong line; both anchors are now two-line, pinned by the titleField: rung that follows the floor.

Refusal asserted positively. Every block opens with a render-proof row, and each absence marker is asserted PRESENT by a control in the same run — a test that only says "no timeline" is satisfied by a component that threw.

Test files

  • packages/plugin-list/src/__tests__/ListView.timeline-binding.test.tsx — the fix(timeline): honor startDateField when rendering a ListView timeline #3129 case that pinned the floor is rewritten, not deleted (it is where the retirement has to become visible); a new step-③ block adds the render proof, the two absence cases and the scope controls; a capability-gate block records, in both directions, that the ADR-0047 toggle is unchanged by this step — unlike the gantt face, the Timeline gate always read resolveTimelineDateBinding and never the flat floor.
  • packages/plugin-view/src/__tests__/ObjectView.timelineBinding-7070.test.tsx — new; the sibling of ObjectView.ganttBinding-7070 / calendarBinding-7029.
  • apps/console/src/__tests__/timelineAxisRefusalReach-7070.test.tsx — new, and the reason it lives in the console: it is the only package depending on plugin-list, plugin-viewandplugin-timeline, so it is the only place the join can be measured. Real faces, real renderer, no stub. Its rows deliberately carry a real created_at column so a restored floor renders a convincing chart rather than an empty one.

Runs — union re-run after the final commit, at 64d8fbe2a:

  • pnpm exec vitest run packages/plugin-list packages/plugin-view packages/plugin-timeline apps/console207 files / 2357 tests passed.
  • app-shell, narrowed and declared: the 26 files that mention timeline or are views/ObjectView* — 290 passed. The app-shell edit is comment-only; the full 608-file suite is CI's.
  • Downstream sweep (plugin-timeline, plugin-gantt refusal pin, react smoke, core normalize-list-view, types timeline pins, schema-catalog render) — 29 files / 991 passed.
  • type-check for plugin-list, plugin-view, app-shell, console — green. Each package chains tsc -p tsconfig.test.json, and both new package-level test files were confirmed present in that program with --listFiles (1 hit each) rather than assumed. It caught a real defect: an unused React import in the console test (TS6133), fixed in 64d8fbe2a.
  • Gates: check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:phantom-deps, check:self-import, check:published-tsconfig-exclude, check:side-effects-array, check-changeset-fixed, check-changeset-no-major, check-type-check-coverage, check-lint-coverage — all report their own OK line. These scan tracked files, so they were re-run after committing; the counts moved with the new files (control-bytes 6157 to 6160 tracked text files; vi-mock 4224 to 4226 source files and 122 to 123 judged call sites), which is what makes the greens readings rather than scans that never saw the diff.

Lint — narrowed, and the narrowing is measured.eslint --no-inline-config over the 6 changed .ts/.tsx files: 0 errors, 424 warnings, all pre-existing repo-wide classes (no-explicit-any, react-refresh/only-export-components); the sibling ObjectView.ganttBinding-7070.test.tsx produces 4 warnings of the same class, so the new files add no new one. Three pieces of evidence that this excludes nothing: the receiving population came from eslint's own config (all 6 files were linted, none silently ignored); the count of 6 is read from --format json, not asserted; and eslint.config.js sets no parserOptions.project / projectService, so linting here is not type-aware and a change in one file cannot move the verdict on a file it does not touch. --max-warnings is deliberately unset in this repo, per lint.yml.

Clause ②, re-declared from this diff

no. The seven files this diff touches are two renderer faces, one comment-only app-shell note, three test files and one changeset. Nothing under packages/types/, no Zod schema, no spec surface, no authorable key — the view layer stops fabricating; the published accept surface is untouched. Control for that zero: the same path filter matches packages/types/src/zod/data-display.zod.ts when fed it.

Out of scope, filed

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

…xis floors
objectui#7070 step ③ of the maintainer ruling of 2026-09-01 (总监批 #28): house
posture 日期轴永不虚构 — a date axis is never fabricated.
`ListView` and `plugin-view`'s `ObjectView` both floored `startDateField` at
'created_at' for a timeline view that declared no axis. `ObjectTimeline` reads
that flat prop at the tail of its resolver chain, so the faces answered "the
axis is bound" for every view and the refusal screen step ① installed
(objectui#7459, landed as 20cb8db) was unreachable from either route — which
is why ①② changed nothing a user could see. Both branches now forward a
declared axis or no key at all.
The floor was never a harmless default: the $select projection is collected
from the DECLARED timeline / options.timeline blocks and never from this prop,
so an undeclared view rendered a timeline bound to a column the query had not
requested and bucketed every record into "No date".
The ruling also explicitly replaces the written decision that stood on the
deleted ListView line, and asks the objectui#3129 note at the app-shell face to
be realigned to the new reality once ③ lands; both are done here.
`titleField` keeps its 'name' floor at both faces — it is not a date axis.
Tests: the two face-level pins measure the prop; a new console-level test
composes the real ListView / ObjectView with the REAL ObjectTimeline and
asserts the refusal on screen, since neither package can see the join.
Part of #7070
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
…n test
`tsc --noEmit` in apps/console reads it as TS6133; the automatic JSX runtime
makes the import unnecessary. Caught by the package's own type-check, not by
vitest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3181.1 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-PRFOV0mr.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.67KB5.75KB
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)516.24KB117.82KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
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)34.34KB9.17KB
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)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.19KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.54KB27.65KB
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)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.93KB21.13KB
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.74KB2.25KB
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)8.11KB3.32KB
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-project-managerClaude

Copy link
Copy Markdown
Collaborator

✅ Landing — 32/32 green, Clause-② no verified from the diff, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC.

Checks, read by name

29 success, 3 skipped by design (dependabot, Test (coverage), Test (coverage shard …)) on head 64d8fbe2a. The long poles: Test (shard 1/4) 11:55:07 · 2/4 11:54:07 · 3/4 11:53:02 · 4/4 11:53:32 · Type Check 11:47:40 · Lint 11:45:03. Also read individually: Bundle Analysis, README Export Check, Governed Surface Queue Guard, Docs Route Eager Closure Check, Control Byte Scan, Inert vi.mock Specifier Check.

Landing authority

Clause-②: no, re-derived by this seat from the diff rather than taken from the declaration — the gate's own rule is 「diff 是事实,卡片语义是预测」. Seven files: two renderer faces, one comment-only app-shell note, three tests, one changeset. Nothing under packages/spec/**, no *.zod.ts, nothing under packages/types/src. ⇒ neither limb fires; this seat lands it without a contract review.

What I verified by content, not from the report

probeplugin-list/ListView.tsxplugin-view/ObjectView.tsx
startDateField: … || 'created_at'1 → 01 → 0
CONTROL titleField: … || 'name' (kept on purpose)2 → 23 → 3

The control is non-zero and unmoved, so the probe discriminates and the change is surgical.

⚠️A probe of mine that was wrong, recorded because it would mislead the next reader too: a raw word-count of created_at in plugin-view/ObjectView.tsx reads 1 → 1 and looks like the floor survived. It did not — the residual occurrence is inside the new comment documenting the retirement. Only the declaration form is an honest probe here. Same class as the tombstone inflation this lane hit earlier today on a different card: count the form, never the word.

The packages/app-shell edit was checked to be comment-only by filtering the diff for non-comment added/removed lines and getting an empty set — not by trusting the claim. It realigns the #3129 note, which had been certifying the two sibling faces as still-fabricating.

Why this step matters more than its diff size suggests

#7459 installed ObjectTimeline's refusal and then measured that nothing began refusing. This is why: ObjectTimeline reads the flat schema.startDateField at the tail of its resolver chain, so a floor at either face answered "the axis is bound" for every view and the refusal was unreachable from both routes. The invented axis was also never fetched$select is collected from the declared timeline blocks, never from this prop — so an undeclared view got a timeline bound to a column the query had not requested and bucketed every record into "No date". A screen that looks built, is wrong, and tells the author nothing.

⭐ The ablation's DOM dump under restored floors is the evidence that matters: data-testid="timeline-canvas" rendering Spring Launch — a healthy, convincing, entirely wrong timeline, not a crash. That is what the pin has to be able to fail on.

Two things the implementer did that I want on the record

  1. The first ablation anchor matched two sites in ListView.tsx (the same conditional-spread text also builds resolvedTimeline's nested config, untouched here). The assertion refused and aborted rather than mutating the wrong line. That is the protocol catching precisely what it exists to catch; both anchors are now two-line.
  2. InterfaceListPage:413-414 was reported as a boundary and deliberately NOT filed as a finding. It derives the binding from defaultCalendarFromObject(objectDef) — a field the object declares, or undefined. Derivation, not fabrication, so it is consistent with 日期轴永不虚构 rather than a surviving instance of it. Naming it is what shows it was measured rather than missed, and it is the one remaining route by which a view that declared no axis still gets one.

Card state

Part of #7070 is deliberately not a closing keyword — #7070 stays open on flavour 3, now filed as #7499 (progress / dependencies, re-measured as surviving at two faces, not three; the app-shell face carries neither key). #7070 was also moved pm:queuepm:dispatched and assigned, closing a half-state the implementer correctly reported back instead of writing the assignee field itself.


Generated by Claude Code

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.

2 participants

@os-project-manager@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(plugin-list,plugin-view): retire the 'created_at' timeline date-axis floors so the refusal becomes reachable - #7500

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-7070-step3-drop-created-at-floors
Sep 3, 2026
Merged

fix(plugin-list,plugin-view): retire the 'created_at' timeline date-axis floors so the refusal becomes reachable#7500
os-project-manager merged 2 commits into
mainfrom
claude/issue-7070-step3-drop-created-at-floors

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Part of #7070

Step ③ of the maintainer ruling of 2026-09-01 (总监批 #28, comment 5494805467): house posture 日期轴永不虚构 — a date axis is never fabricated. ListView and plugin-view's ObjectView stop flooring the timeline axis at 'created_at'.

Steps ①② are landed (20cb8db9b, PR #7467, card #7459) and this branch is cut from exactly that commit. ⛔ Nothing in ObjectTimeline, the refusal screen or the locale key is touched here.

The two lines

FaceWasNow
packages/plugin-list/src/ListView.tsx:2496startDateField: dateBinding.startDateField || 'created_at'conditional spread — the key is absent when no axis resolves
packages/plugin-view/src/ObjectView.tsx:1354startDateField: ...startDateField || ...dateField || 'created_at'alias resolved into a local, then the same conditional spread

Line numbers re-derived on main at 20cb8db9b, not inherited from the card. created_at occurs 4 times in ListView.tsx and 1 in ObjectView.tsx; those are occurrences of the string, not floors. Read per line, exactly one floor per file:

  • ListView.tsx:1753s.add('created_at') in knownObjectFields. A platform-column reference, kept.
  • ListView.tsx:1785 — prose explaining the PLATFORM_RECORD_COLUMNS carve-out, kept.
  • ListView.tsx:2496the floor. Deleted.
  • ObjectView.tsx:1354the floor. Deleted.

titleField keeps its 'name' floor at both faces: not a date axis, and the same display-name rung the gallery / kanban / gantt branches carry. Pinned as scope in both new suites.

Why it was never a harmless default

ObjectTimeline reads the flatschema.startDateField at the tail of its resolver chain — plugin-timeline's own CONTROL block proves that rung is fully honoured — so a floor here answered "the axis is bound" for every view and the refusal step ① installed was unreachable from either route. That is exactly why #7459's own measurement recorded that nothing began refusing when ①② landed.

And the invented axis was never fetched: the $select projection is collected from the DECLARED timeline / options.timeline blocks (collectViewFields), never from this prop. An undeclared view therefore got a timeline bound to a column the query had not requested and bucketed every record into "No date" — the screen that looks built, is wrong, and gives the author no signal.

The ruling also explicitly replaces the written decision that stood on the deleted ListView line ("created_at stays the last resort for a view that declares no date axis anywhere"), and asks the objectui#3129 note at the app-shell face to be realigned once ③ lands. Both are done here; the app-shell edit is comment-only.

⭐ Blast radius — measured, not asserted

Which authored surfaces in this repo reach these floors: zero. Which begin refusing: zero.

Every sweep below was run with a control that FIRED in the same run, because a zero from a grep is not a reading.

  • Authored metadata (json/yml/yaml) declaring a timeline view: 4 hits, none of them reach either face. The three examples/schema-catalog/src/schemas/plugin-timeline/*.json documents and packages/types/examples/data-display-examples.json are direct type: 'timeline' schemas carrying authored items / events — they never traverse ListView or generateViewSchema, and hasAuthoredItems short-circuits the refusal predicate (!hasAuthoredItems && !startDateField) regardless. Control: the identical regex for "calendar" fires on components-complex-view-switcher/view-switcher.json:24.
  • Authored TS/TSX samples declaring a timeline view: zero outside tests. Control: the identical regex for 'calendar' fires on six sites.
  • appearance.allowedVisualizations naming timeline in authored metadata: zero.

Two product routes were then measured rather than reasoned about, and both are why the radius is small:

  1. The console's own authoring UI cannot produce an undeclared timeline view.CreateViewDialog's REQUIRED_FIELDS table lists timeline.startDateField among "the sub-config keys a view type must have set before a record can be persisted", with a smart default auto-picked from the object's real date fields (PRIMARY_DATE_PREFERRED). Anything created in-product carries a declared axis.
  2. Interface list pages auto-derive one.InterfaceListPage:413-414 fills timeline from defaultCalendarFromObject(objectDef) when timeline is whitelisted — a field the object declares, or undefined. So such a page refuses only when the object carries no date field at all, which is the correct answer: there is no axis to declare. This is derivation from schema, not fabrication, and the fix(timeline): honor startDateField when rendering a ListView timeline #3129 note already cites it as the good shape.

What is left is metadata authored outside those paths — hand-written or migrated views. Those begin refusing, which is what the ruling ordered, and the refusal names the keys to declare so an affected view reports its own fix. The changeset carries the migration note.

⛔ No replacement fallback was added anywhere.

Verification

Red-then-green, taken naturally first. Both face-level suites were written and run against the UNMODIFIED tree at 20cb8db9b before the first source edit: plugin-view 3 failed / 5 passed, plugin-list 3 failed / 14 passed — the failures exactly the "invents no axis" cases, every render proof and CONTROL green in the same run. After the edit: 25/25.

Ablation, for the console composition test, which was written after the fix and so had never been observed red. Both floors restored, proven on disk in both directions before the run (removed-text 0 AND injected-text 1 per file, plus a blob-hash change on each), trap ... EXIT INT TERM with absolute paths, restore proven by state (git diff HEAD empty + blob equality against the HEAD blob), never by an exit code. No build/dist is involved — vitest resolves @object-ui/* to sibling src/ through the root config's alias table — so there is no stale-artifact leg to prove.

Under the restored floors: 9 failed / 24 passed. The 9 are precisely the "invents no axis" and "refuses on screen" cases at all three levels; every render proof and every CONTROL stayed green in the mutated world. The DOM dump from that run is the point of the exercise — it shows data-testid="timeline-canvas" and the text Spring Launch: a healthy, convincing, entirely wrong timeline, not an error. That is what the pin has to be able to fail on.

⚠️ One anchor note, recorded because it is the failure this protocol exists to catch: the first ablation anchor matched 2 sites in ListView.tsx — the identical conditional-spread text also builds resolvedTimeline's nested config a few lines up, which this card does not touch. The assertion refused and the run aborted rather than mutating the wrong line; both anchors are now two-line, pinned by the titleField: rung that follows the floor.

Refusal asserted positively. Every block opens with a render-proof row, and each absence marker is asserted PRESENT by a control in the same run — a test that only says "no timeline" is satisfied by a component that threw.

Test files

  • packages/plugin-list/src/__tests__/ListView.timeline-binding.test.tsx — the fix(timeline): honor startDateField when rendering a ListView timeline #3129 case that pinned the floor is rewritten, not deleted (it is where the retirement has to become visible); a new step-③ block adds the render proof, the two absence cases and the scope controls; a capability-gate block records, in both directions, that the ADR-0047 toggle is unchanged by this step — unlike the gantt face, the Timeline gate always read resolveTimelineDateBinding and never the flat floor.
  • packages/plugin-view/src/__tests__/ObjectView.timelineBinding-7070.test.tsx — new; the sibling of ObjectView.ganttBinding-7070 / calendarBinding-7029.
  • apps/console/src/__tests__/timelineAxisRefusalReach-7070.test.tsx — new, and the reason it lives in the console: it is the only package depending on plugin-list, plugin-viewandplugin-timeline, so it is the only place the join can be measured. Real faces, real renderer, no stub. Its rows deliberately carry a real created_at column so a restored floor renders a convincing chart rather than an empty one.

Runs — union re-run after the final commit, at 64d8fbe2a:

  • pnpm exec vitest run packages/plugin-list packages/plugin-view packages/plugin-timeline apps/console207 files / 2357 tests passed.
  • app-shell, narrowed and declared: the 26 files that mention timeline or are views/ObjectView* — 290 passed. The app-shell edit is comment-only; the full 608-file suite is CI's.
  • Downstream sweep (plugin-timeline, plugin-gantt refusal pin, react smoke, core normalize-list-view, types timeline pins, schema-catalog render) — 29 files / 991 passed.
  • type-check for plugin-list, plugin-view, app-shell, console — green. Each package chains tsc -p tsconfig.test.json, and both new package-level test files were confirmed present in that program with --listFiles (1 hit each) rather than assumed. It caught a real defect: an unused React import in the console test (TS6133), fixed in 64d8fbe2a.
  • Gates: check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:phantom-deps, check:self-import, check:published-tsconfig-exclude, check:side-effects-array, check-changeset-fixed, check-changeset-no-major, check-type-check-coverage, check-lint-coverage — all report their own OK line. These scan tracked files, so they were re-run after committing; the counts moved with the new files (control-bytes 6157 to 6160 tracked text files; vi-mock 4224 to 4226 source files and 122 to 123 judged call sites), which is what makes the greens readings rather than scans that never saw the diff.

Lint — narrowed, and the narrowing is measured.eslint --no-inline-config over the 6 changed .ts/.tsx files: 0 errors, 424 warnings, all pre-existing repo-wide classes (no-explicit-any, react-refresh/only-export-components); the sibling ObjectView.ganttBinding-7070.test.tsx produces 4 warnings of the same class, so the new files add no new one. Three pieces of evidence that this excludes nothing: the receiving population came from eslint's own config (all 6 files were linted, none silently ignored); the count of 6 is read from --format json, not asserted; and eslint.config.js sets no parserOptions.project / projectService, so linting here is not type-aware and a change in one file cannot move the verdict on a file it does not touch. --max-warnings is deliberately unset in this repo, per lint.yml.

Clause ②, re-declared from this diff

no. The seven files this diff touches are two renderer faces, one comment-only app-shell note, three test files and one changeset. Nothing under packages/types/, no Zod schema, no spec surface, no authorable key — the view layer stops fabricating; the published accept surface is untouched. Control for that zero: the same path filter matches packages/types/src/zod/data-display.zod.ts when fed it.

Out of scope, filed

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

…xis floors
objectui#7070 step ③ of the maintainer ruling of 2026-09-01 (总监批 #28): house
posture 日期轴永不虚构 — a date axis is never fabricated.
`ListView` and `plugin-view`'s `ObjectView` both floored `startDateField` at
'created_at' for a timeline view that declared no axis. `ObjectTimeline` reads
that flat prop at the tail of its resolver chain, so the faces answered "the
axis is bound" for every view and the refusal screen step ① installed
(objectui#7459, landed as 20cb8db) was unreachable from either route — which
is why ①② changed nothing a user could see. Both branches now forward a
declared axis or no key at all.
The floor was never a harmless default: the $select projection is collected
from the DECLARED timeline / options.timeline blocks and never from this prop,
so an undeclared view rendered a timeline bound to a column the query had not
requested and bucketed every record into "No date".
The ruling also explicitly replaces the written decision that stood on the
deleted ListView line, and asks the objectui#3129 note at the app-shell face to
be realigned to the new reality once ③ lands; both are done here.
`titleField` keeps its 'name' floor at both faces — it is not a date axis.
Tests: the two face-level pins measure the prop; a new console-level test
composes the real ListView / ObjectView with the REAL ObjectTimeline and
asserts the refusal on screen, since neither package can see the join.
Part of #7070
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
…n test
`tsc --noEmit` in apps/console reads it as TS6133; the automatic JSX runtime
makes the import unnecessary. Caught by the package's own type-check, not by
vitest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3181.1 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-PRFOV0mr.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.67KB5.75KB
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)516.24KB117.82KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
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)34.34KB9.17KB
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)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.19KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.54KB27.65KB
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)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.93KB21.13KB
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.74KB2.25KB
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)8.11KB3.32KB
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-project-managerClaude

Copy link
Copy Markdown
Collaborator

✅ Landing — 32/32 green, Clause-② no verified from the diff, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC.

Checks, read by name

29 success, 3 skipped by design (dependabot, Test (coverage), Test (coverage shard …)) on head 64d8fbe2a. The long poles: Test (shard 1/4) 11:55:07 · 2/4 11:54:07 · 3/4 11:53:02 · 4/4 11:53:32 · Type Check 11:47:40 · Lint 11:45:03. Also read individually: Bundle Analysis, README Export Check, Governed Surface Queue Guard, Docs Route Eager Closure Check, Control Byte Scan, Inert vi.mock Specifier Check.

Landing authority

Clause-②: no, re-derived by this seat from the diff rather than taken from the declaration — the gate's own rule is 「diff 是事实,卡片语义是预测」. Seven files: two renderer faces, one comment-only app-shell note, three tests, one changeset. Nothing under packages/spec/**, no *.zod.ts, nothing under packages/types/src. ⇒ neither limb fires; this seat lands it without a contract review.

What I verified by content, not from the report

probeplugin-list/ListView.tsxplugin-view/ObjectView.tsx
startDateField: … || 'created_at'1 → 01 → 0
CONTROL titleField: … || 'name' (kept on purpose)2 → 23 → 3

The control is non-zero and unmoved, so the probe discriminates and the change is surgical.

⚠️A probe of mine that was wrong, recorded because it would mislead the next reader too: a raw word-count of created_at in plugin-view/ObjectView.tsx reads 1 → 1 and looks like the floor survived. It did not — the residual occurrence is inside the new comment documenting the retirement. Only the declaration form is an honest probe here. Same class as the tombstone inflation this lane hit earlier today on a different card: count the form, never the word.

The packages/app-shell edit was checked to be comment-only by filtering the diff for non-comment added/removed lines and getting an empty set — not by trusting the claim. It realigns the #3129 note, which had been certifying the two sibling faces as still-fabricating.

Why this step matters more than its diff size suggests

#7459 installed ObjectTimeline's refusal and then measured that nothing began refusing. This is why: ObjectTimeline reads the flat schema.startDateField at the tail of its resolver chain, so a floor at either face answered "the axis is bound" for every view and the refusal was unreachable from both routes. The invented axis was also never fetched$select is collected from the declared timeline blocks, never from this prop — so an undeclared view got a timeline bound to a column the query had not requested and bucketed every record into "No date". A screen that looks built, is wrong, and tells the author nothing.

⭐ The ablation's DOM dump under restored floors is the evidence that matters: data-testid="timeline-canvas" rendering Spring Launch — a healthy, convincing, entirely wrong timeline, not a crash. That is what the pin has to be able to fail on.

Two things the implementer did that I want on the record

  1. The first ablation anchor matched two sites in ListView.tsx (the same conditional-spread text also builds resolvedTimeline's nested config, untouched here). The assertion refused and aborted rather than mutating the wrong line. That is the protocol catching precisely what it exists to catch; both anchors are now two-line.
  2. InterfaceListPage:413-414 was reported as a boundary and deliberately NOT filed as a finding. It derives the binding from defaultCalendarFromObject(objectDef) — a field the object declares, or undefined. Derivation, not fabrication, so it is consistent with 日期轴永不虚构 rather than a surviving instance of it. Naming it is what shows it was measured rather than missed, and it is the one remaining route by which a view that declared no axis still gets one.

Card state

Part of #7070 is deliberately not a closing keyword — #7070 stays open on flavour 3, now filed as #7499 (progress / dependencies, re-measured as surviving at two faces, not three; the app-shell face carries neither key). #7070 was also moved pm:queuepm:dispatched and assigned, closing a half-state the implementer correctly reported back instead of writing the assignee field itself.


Generated by Claude Code

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.

2 participants

@os-project-manager@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(plugin-list,plugin-view): retire the 'created_at' timeline date-axis floors so the refusal becomes reachable - #7500

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-7070-step3-drop-created-at-floors
Sep 3, 2026
Merged

fix(plugin-list,plugin-view): retire the 'created_at' timeline date-axis floors so the refusal becomes reachable#7500
os-project-manager merged 2 commits into
mainfrom
claude/issue-7070-step3-drop-created-at-floors

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Part of #7070

Step ③ of the maintainer ruling of 2026-09-01 (总监批 #28, comment 5494805467): house posture 日期轴永不虚构 — a date axis is never fabricated. ListView and plugin-view's ObjectView stop flooring the timeline axis at 'created_at'.

Steps ①② are landed (20cb8db9b, PR #7467, card #7459) and this branch is cut from exactly that commit. ⛔ Nothing in ObjectTimeline, the refusal screen or the locale key is touched here.

The two lines

FaceWasNow
packages/plugin-list/src/ListView.tsx:2496startDateField: dateBinding.startDateField || 'created_at'conditional spread — the key is absent when no axis resolves
packages/plugin-view/src/ObjectView.tsx:1354startDateField: ...startDateField || ...dateField || 'created_at'alias resolved into a local, then the same conditional spread

Line numbers re-derived on main at 20cb8db9b, not inherited from the card. created_at occurs 4 times in ListView.tsx and 1 in ObjectView.tsx; those are occurrences of the string, not floors. Read per line, exactly one floor per file:

  • ListView.tsx:1753s.add('created_at') in knownObjectFields. A platform-column reference, kept.
  • ListView.tsx:1785 — prose explaining the PLATFORM_RECORD_COLUMNS carve-out, kept.
  • ListView.tsx:2496the floor. Deleted.
  • ObjectView.tsx:1354the floor. Deleted.

titleField keeps its 'name' floor at both faces: not a date axis, and the same display-name rung the gallery / kanban / gantt branches carry. Pinned as scope in both new suites.

Why it was never a harmless default

ObjectTimeline reads the flatschema.startDateField at the tail of its resolver chain — plugin-timeline's own CONTROL block proves that rung is fully honoured — so a floor here answered "the axis is bound" for every view and the refusal step ① installed was unreachable from either route. That is exactly why #7459's own measurement recorded that nothing began refusing when ①② landed.

And the invented axis was never fetched: the $select projection is collected from the DECLARED timeline / options.timeline blocks (collectViewFields), never from this prop. An undeclared view therefore got a timeline bound to a column the query had not requested and bucketed every record into "No date" — the screen that looks built, is wrong, and gives the author no signal.

The ruling also explicitly replaces the written decision that stood on the deleted ListView line ("created_at stays the last resort for a view that declares no date axis anywhere"), and asks the objectui#3129 note at the app-shell face to be realigned once ③ lands. Both are done here; the app-shell edit is comment-only.

⭐ Blast radius — measured, not asserted

Which authored surfaces in this repo reach these floors: zero. Which begin refusing: zero.

Every sweep below was run with a control that FIRED in the same run, because a zero from a grep is not a reading.

  • Authored metadata (json/yml/yaml) declaring a timeline view: 4 hits, none of them reach either face. The three examples/schema-catalog/src/schemas/plugin-timeline/*.json documents and packages/types/examples/data-display-examples.json are direct type: 'timeline' schemas carrying authored items / events — they never traverse ListView or generateViewSchema, and hasAuthoredItems short-circuits the refusal predicate (!hasAuthoredItems && !startDateField) regardless. Control: the identical regex for "calendar" fires on components-complex-view-switcher/view-switcher.json:24.
  • Authored TS/TSX samples declaring a timeline view: zero outside tests. Control: the identical regex for 'calendar' fires on six sites.
  • appearance.allowedVisualizations naming timeline in authored metadata: zero.

Two product routes were then measured rather than reasoned about, and both are why the radius is small:

  1. The console's own authoring UI cannot produce an undeclared timeline view.CreateViewDialog's REQUIRED_FIELDS table lists timeline.startDateField among "the sub-config keys a view type must have set before a record can be persisted", with a smart default auto-picked from the object's real date fields (PRIMARY_DATE_PREFERRED). Anything created in-product carries a declared axis.
  2. Interface list pages auto-derive one.InterfaceListPage:413-414 fills timeline from defaultCalendarFromObject(objectDef) when timeline is whitelisted — a field the object declares, or undefined. So such a page refuses only when the object carries no date field at all, which is the correct answer: there is no axis to declare. This is derivation from schema, not fabrication, and the fix(timeline): honor startDateField when rendering a ListView timeline #3129 note already cites it as the good shape.

What is left is metadata authored outside those paths — hand-written or migrated views. Those begin refusing, which is what the ruling ordered, and the refusal names the keys to declare so an affected view reports its own fix. The changeset carries the migration note.

⛔ No replacement fallback was added anywhere.

Verification

Red-then-green, taken naturally first. Both face-level suites were written and run against the UNMODIFIED tree at 20cb8db9b before the first source edit: plugin-view 3 failed / 5 passed, plugin-list 3 failed / 14 passed — the failures exactly the "invents no axis" cases, every render proof and CONTROL green in the same run. After the edit: 25/25.

Ablation, for the console composition test, which was written after the fix and so had never been observed red. Both floors restored, proven on disk in both directions before the run (removed-text 0 AND injected-text 1 per file, plus a blob-hash change on each), trap ... EXIT INT TERM with absolute paths, restore proven by state (git diff HEAD empty + blob equality against the HEAD blob), never by an exit code. No build/dist is involved — vitest resolves @object-ui/* to sibling src/ through the root config's alias table — so there is no stale-artifact leg to prove.

Under the restored floors: 9 failed / 24 passed. The 9 are precisely the "invents no axis" and "refuses on screen" cases at all three levels; every render proof and every CONTROL stayed green in the mutated world. The DOM dump from that run is the point of the exercise — it shows data-testid="timeline-canvas" and the text Spring Launch: a healthy, convincing, entirely wrong timeline, not an error. That is what the pin has to be able to fail on.

⚠️ One anchor note, recorded because it is the failure this protocol exists to catch: the first ablation anchor matched 2 sites in ListView.tsx — the identical conditional-spread text also builds resolvedTimeline's nested config a few lines up, which this card does not touch. The assertion refused and the run aborted rather than mutating the wrong line; both anchors are now two-line, pinned by the titleField: rung that follows the floor.

Refusal asserted positively. Every block opens with a render-proof row, and each absence marker is asserted PRESENT by a control in the same run — a test that only says "no timeline" is satisfied by a component that threw.

Test files

  • packages/plugin-list/src/__tests__/ListView.timeline-binding.test.tsx — the fix(timeline): honor startDateField when rendering a ListView timeline #3129 case that pinned the floor is rewritten, not deleted (it is where the retirement has to become visible); a new step-③ block adds the render proof, the two absence cases and the scope controls; a capability-gate block records, in both directions, that the ADR-0047 toggle is unchanged by this step — unlike the gantt face, the Timeline gate always read resolveTimelineDateBinding and never the flat floor.
  • packages/plugin-view/src/__tests__/ObjectView.timelineBinding-7070.test.tsx — new; the sibling of ObjectView.ganttBinding-7070 / calendarBinding-7029.
  • apps/console/src/__tests__/timelineAxisRefusalReach-7070.test.tsx — new, and the reason it lives in the console: it is the only package depending on plugin-list, plugin-viewandplugin-timeline, so it is the only place the join can be measured. Real faces, real renderer, no stub. Its rows deliberately carry a real created_at column so a restored floor renders a convincing chart rather than an empty one.

Runs — union re-run after the final commit, at 64d8fbe2a:

  • pnpm exec vitest run packages/plugin-list packages/plugin-view packages/plugin-timeline apps/console207 files / 2357 tests passed.
  • app-shell, narrowed and declared: the 26 files that mention timeline or are views/ObjectView* — 290 passed. The app-shell edit is comment-only; the full 608-file suite is CI's.
  • Downstream sweep (plugin-timeline, plugin-gantt refusal pin, react smoke, core normalize-list-view, types timeline pins, schema-catalog render) — 29 files / 991 passed.
  • type-check for plugin-list, plugin-view, app-shell, console — green. Each package chains tsc -p tsconfig.test.json, and both new package-level test files were confirmed present in that program with --listFiles (1 hit each) rather than assumed. It caught a real defect: an unused React import in the console test (TS6133), fixed in 64d8fbe2a.
  • Gates: check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:phantom-deps, check:self-import, check:published-tsconfig-exclude, check:side-effects-array, check-changeset-fixed, check-changeset-no-major, check-type-check-coverage, check-lint-coverage — all report their own OK line. These scan tracked files, so they were re-run after committing; the counts moved with the new files (control-bytes 6157 to 6160 tracked text files; vi-mock 4224 to 4226 source files and 122 to 123 judged call sites), which is what makes the greens readings rather than scans that never saw the diff.

Lint — narrowed, and the narrowing is measured.eslint --no-inline-config over the 6 changed .ts/.tsx files: 0 errors, 424 warnings, all pre-existing repo-wide classes (no-explicit-any, react-refresh/only-export-components); the sibling ObjectView.ganttBinding-7070.test.tsx produces 4 warnings of the same class, so the new files add no new one. Three pieces of evidence that this excludes nothing: the receiving population came from eslint's own config (all 6 files were linted, none silently ignored); the count of 6 is read from --format json, not asserted; and eslint.config.js sets no parserOptions.project / projectService, so linting here is not type-aware and a change in one file cannot move the verdict on a file it does not touch. --max-warnings is deliberately unset in this repo, per lint.yml.

Clause ②, re-declared from this diff

no. The seven files this diff touches are two renderer faces, one comment-only app-shell note, three test files and one changeset. Nothing under packages/types/, no Zod schema, no spec surface, no authorable key — the view layer stops fabricating; the published accept surface is untouched. Control for that zero: the same path filter matches packages/types/src/zod/data-display.zod.ts when fed it.

Out of scope, filed

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

…xis floors
objectui#7070 step ③ of the maintainer ruling of 2026-09-01 (总监批 #28): house
posture 日期轴永不虚构 — a date axis is never fabricated.
`ListView` and `plugin-view`'s `ObjectView` both floored `startDateField` at
'created_at' for a timeline view that declared no axis. `ObjectTimeline` reads
that flat prop at the tail of its resolver chain, so the faces answered "the
axis is bound" for every view and the refusal screen step ① installed
(objectui#7459, landed as 20cb8db) was unreachable from either route — which
is why ①② changed nothing a user could see. Both branches now forward a
declared axis or no key at all.
The floor was never a harmless default: the $select projection is collected
from the DECLARED timeline / options.timeline blocks and never from this prop,
so an undeclared view rendered a timeline bound to a column the query had not
requested and bucketed every record into "No date".
The ruling also explicitly replaces the written decision that stood on the
deleted ListView line, and asks the objectui#3129 note at the app-shell face to
be realigned to the new reality once ③ lands; both are done here.
`titleField` keeps its 'name' floor at both faces — it is not a date axis.
Tests: the two face-level pins measure the prop; a new console-level test
composes the real ListView / ObjectView with the REAL ObjectTimeline and
asserts the refusal on screen, since neither package can see the join.
Part of #7070
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
…n test
`tsc --noEmit` in apps/console reads it as TS6133; the automatic JSX runtime
makes the import unnecessary. Caught by the package's own type-check, not by
vitest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3181.1 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-PRFOV0mr.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.67KB5.75KB
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)516.24KB117.82KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
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)34.34KB9.17KB
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)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.19KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.54KB27.65KB
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)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.93KB21.13KB
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.74KB2.25KB
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)8.11KB3.32KB
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-project-managerClaude

Copy link
Copy Markdown
Collaborator

✅ Landing — 32/32 green, Clause-② no verified from the diff, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC.

Checks, read by name

29 success, 3 skipped by design (dependabot, Test (coverage), Test (coverage shard …)) on head 64d8fbe2a. The long poles: Test (shard 1/4) 11:55:07 · 2/4 11:54:07 · 3/4 11:53:02 · 4/4 11:53:32 · Type Check 11:47:40 · Lint 11:45:03. Also read individually: Bundle Analysis, README Export Check, Governed Surface Queue Guard, Docs Route Eager Closure Check, Control Byte Scan, Inert vi.mock Specifier Check.

Landing authority

Clause-②: no, re-derived by this seat from the diff rather than taken from the declaration — the gate's own rule is 「diff 是事实,卡片语义是预测」. Seven files: two renderer faces, one comment-only app-shell note, three tests, one changeset. Nothing under packages/spec/**, no *.zod.ts, nothing under packages/types/src. ⇒ neither limb fires; this seat lands it without a contract review.

What I verified by content, not from the report

probeplugin-list/ListView.tsxplugin-view/ObjectView.tsx
startDateField: … || 'created_at'1 → 01 → 0
CONTROL titleField: … || 'name' (kept on purpose)2 → 23 → 3

The control is non-zero and unmoved, so the probe discriminates and the change is surgical.

⚠️A probe of mine that was wrong, recorded because it would mislead the next reader too: a raw word-count of created_at in plugin-view/ObjectView.tsx reads 1 → 1 and looks like the floor survived. It did not — the residual occurrence is inside the new comment documenting the retirement. Only the declaration form is an honest probe here. Same class as the tombstone inflation this lane hit earlier today on a different card: count the form, never the word.

The packages/app-shell edit was checked to be comment-only by filtering the diff for non-comment added/removed lines and getting an empty set — not by trusting the claim. It realigns the #3129 note, which had been certifying the two sibling faces as still-fabricating.

Why this step matters more than its diff size suggests

#7459 installed ObjectTimeline's refusal and then measured that nothing began refusing. This is why: ObjectTimeline reads the flat schema.startDateField at the tail of its resolver chain, so a floor at either face answered "the axis is bound" for every view and the refusal was unreachable from both routes. The invented axis was also never fetched$select is collected from the declared timeline blocks, never from this prop — so an undeclared view got a timeline bound to a column the query had not requested and bucketed every record into "No date". A screen that looks built, is wrong, and tells the author nothing.

⭐ The ablation's DOM dump under restored floors is the evidence that matters: data-testid="timeline-canvas" rendering Spring Launch — a healthy, convincing, entirely wrong timeline, not a crash. That is what the pin has to be able to fail on.

Two things the implementer did that I want on the record

  1. The first ablation anchor matched two sites in ListView.tsx (the same conditional-spread text also builds resolvedTimeline's nested config, untouched here). The assertion refused and aborted rather than mutating the wrong line. That is the protocol catching precisely what it exists to catch; both anchors are now two-line.
  2. InterfaceListPage:413-414 was reported as a boundary and deliberately NOT filed as a finding. It derives the binding from defaultCalendarFromObject(objectDef) — a field the object declares, or undefined. Derivation, not fabrication, so it is consistent with 日期轴永不虚构 rather than a surviving instance of it. Naming it is what shows it was measured rather than missed, and it is the one remaining route by which a view that declared no axis still gets one.

Card state

Part of #7070 is deliberately not a closing keyword — #7070 stays open on flavour 3, now filed as #7499 (progress / dependencies, re-measured as surviving at two faces, not three; the app-shell face carries neither key). #7070 was also moved pm:queuepm:dispatched and assigned, closing a half-state the implementer correctly reported back instead of writing the assignee field itself.


Generated by Claude Code

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.

2 participants

@os-project-manager@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(plugin-list,plugin-view): retire the 'created_at' timeline date-axis floors so the refusal becomes reachable - #7500

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-7070-step3-drop-created-at-floors
Sep 3, 2026
Merged

fix(plugin-list,plugin-view): retire the 'created_at' timeline date-axis floors so the refusal becomes reachable#7500
os-project-manager merged 2 commits into
mainfrom
claude/issue-7070-step3-drop-created-at-floors

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Part of #7070

Step ③ of the maintainer ruling of 2026-09-01 (总监批 #28, comment 5494805467): house posture 日期轴永不虚构 — a date axis is never fabricated. ListView and plugin-view's ObjectView stop flooring the timeline axis at 'created_at'.

Steps ①② are landed (20cb8db9b, PR #7467, card #7459) and this branch is cut from exactly that commit. ⛔ Nothing in ObjectTimeline, the refusal screen or the locale key is touched here.

The two lines

FaceWasNow
packages/plugin-list/src/ListView.tsx:2496startDateField: dateBinding.startDateField || 'created_at'conditional spread — the key is absent when no axis resolves
packages/plugin-view/src/ObjectView.tsx:1354startDateField: ...startDateField || ...dateField || 'created_at'alias resolved into a local, then the same conditional spread

Line numbers re-derived on main at 20cb8db9b, not inherited from the card. created_at occurs 4 times in ListView.tsx and 1 in ObjectView.tsx; those are occurrences of the string, not floors. Read per line, exactly one floor per file:

  • ListView.tsx:1753s.add('created_at') in knownObjectFields. A platform-column reference, kept.
  • ListView.tsx:1785 — prose explaining the PLATFORM_RECORD_COLUMNS carve-out, kept.
  • ListView.tsx:2496the floor. Deleted.
  • ObjectView.tsx:1354the floor. Deleted.

titleField keeps its 'name' floor at both faces: not a date axis, and the same display-name rung the gallery / kanban / gantt branches carry. Pinned as scope in both new suites.

Why it was never a harmless default

ObjectTimeline reads the flatschema.startDateField at the tail of its resolver chain — plugin-timeline's own CONTROL block proves that rung is fully honoured — so a floor here answered "the axis is bound" for every view and the refusal step ① installed was unreachable from either route. That is exactly why #7459's own measurement recorded that nothing began refusing when ①② landed.

And the invented axis was never fetched: the $select projection is collected from the DECLARED timeline / options.timeline blocks (collectViewFields), never from this prop. An undeclared view therefore got a timeline bound to a column the query had not requested and bucketed every record into "No date" — the screen that looks built, is wrong, and gives the author no signal.

The ruling also explicitly replaces the written decision that stood on the deleted ListView line ("created_at stays the last resort for a view that declares no date axis anywhere"), and asks the objectui#3129 note at the app-shell face to be realigned once ③ lands. Both are done here; the app-shell edit is comment-only.

⭐ Blast radius — measured, not asserted

Which authored surfaces in this repo reach these floors: zero. Which begin refusing: zero.

Every sweep below was run with a control that FIRED in the same run, because a zero from a grep is not a reading.

  • Authored metadata (json/yml/yaml) declaring a timeline view: 4 hits, none of them reach either face. The three examples/schema-catalog/src/schemas/plugin-timeline/*.json documents and packages/types/examples/data-display-examples.json are direct type: 'timeline' schemas carrying authored items / events — they never traverse ListView or generateViewSchema, and hasAuthoredItems short-circuits the refusal predicate (!hasAuthoredItems && !startDateField) regardless. Control: the identical regex for "calendar" fires on components-complex-view-switcher/view-switcher.json:24.
  • Authored TS/TSX samples declaring a timeline view: zero outside tests. Control: the identical regex for 'calendar' fires on six sites.
  • appearance.allowedVisualizations naming timeline in authored metadata: zero.

Two product routes were then measured rather than reasoned about, and both are why the radius is small:

  1. The console's own authoring UI cannot produce an undeclared timeline view.CreateViewDialog's REQUIRED_FIELDS table lists timeline.startDateField among "the sub-config keys a view type must have set before a record can be persisted", with a smart default auto-picked from the object's real date fields (PRIMARY_DATE_PREFERRED). Anything created in-product carries a declared axis.
  2. Interface list pages auto-derive one.InterfaceListPage:413-414 fills timeline from defaultCalendarFromObject(objectDef) when timeline is whitelisted — a field the object declares, or undefined. So such a page refuses only when the object carries no date field at all, which is the correct answer: there is no axis to declare. This is derivation from schema, not fabrication, and the fix(timeline): honor startDateField when rendering a ListView timeline #3129 note already cites it as the good shape.

What is left is metadata authored outside those paths — hand-written or migrated views. Those begin refusing, which is what the ruling ordered, and the refusal names the keys to declare so an affected view reports its own fix. The changeset carries the migration note.

⛔ No replacement fallback was added anywhere.

Verification

Red-then-green, taken naturally first. Both face-level suites were written and run against the UNMODIFIED tree at 20cb8db9b before the first source edit: plugin-view 3 failed / 5 passed, plugin-list 3 failed / 14 passed — the failures exactly the "invents no axis" cases, every render proof and CONTROL green in the same run. After the edit: 25/25.

Ablation, for the console composition test, which was written after the fix and so had never been observed red. Both floors restored, proven on disk in both directions before the run (removed-text 0 AND injected-text 1 per file, plus a blob-hash change on each), trap ... EXIT INT TERM with absolute paths, restore proven by state (git diff HEAD empty + blob equality against the HEAD blob), never by an exit code. No build/dist is involved — vitest resolves @object-ui/* to sibling src/ through the root config's alias table — so there is no stale-artifact leg to prove.

Under the restored floors: 9 failed / 24 passed. The 9 are precisely the "invents no axis" and "refuses on screen" cases at all three levels; every render proof and every CONTROL stayed green in the mutated world. The DOM dump from that run is the point of the exercise — it shows data-testid="timeline-canvas" and the text Spring Launch: a healthy, convincing, entirely wrong timeline, not an error. That is what the pin has to be able to fail on.

⚠️ One anchor note, recorded because it is the failure this protocol exists to catch: the first ablation anchor matched 2 sites in ListView.tsx — the identical conditional-spread text also builds resolvedTimeline's nested config a few lines up, which this card does not touch. The assertion refused and the run aborted rather than mutating the wrong line; both anchors are now two-line, pinned by the titleField: rung that follows the floor.

Refusal asserted positively. Every block opens with a render-proof row, and each absence marker is asserted PRESENT by a control in the same run — a test that only says "no timeline" is satisfied by a component that threw.

Test files

  • packages/plugin-list/src/__tests__/ListView.timeline-binding.test.tsx — the fix(timeline): honor startDateField when rendering a ListView timeline #3129 case that pinned the floor is rewritten, not deleted (it is where the retirement has to become visible); a new step-③ block adds the render proof, the two absence cases and the scope controls; a capability-gate block records, in both directions, that the ADR-0047 toggle is unchanged by this step — unlike the gantt face, the Timeline gate always read resolveTimelineDateBinding and never the flat floor.
  • packages/plugin-view/src/__tests__/ObjectView.timelineBinding-7070.test.tsx — new; the sibling of ObjectView.ganttBinding-7070 / calendarBinding-7029.
  • apps/console/src/__tests__/timelineAxisRefusalReach-7070.test.tsx — new, and the reason it lives in the console: it is the only package depending on plugin-list, plugin-viewandplugin-timeline, so it is the only place the join can be measured. Real faces, real renderer, no stub. Its rows deliberately carry a real created_at column so a restored floor renders a convincing chart rather than an empty one.

Runs — union re-run after the final commit, at 64d8fbe2a:

  • pnpm exec vitest run packages/plugin-list packages/plugin-view packages/plugin-timeline apps/console207 files / 2357 tests passed.
  • app-shell, narrowed and declared: the 26 files that mention timeline or are views/ObjectView* — 290 passed. The app-shell edit is comment-only; the full 608-file suite is CI's.
  • Downstream sweep (plugin-timeline, plugin-gantt refusal pin, react smoke, core normalize-list-view, types timeline pins, schema-catalog render) — 29 files / 991 passed.
  • type-check for plugin-list, plugin-view, app-shell, console — green. Each package chains tsc -p tsconfig.test.json, and both new package-level test files were confirmed present in that program with --listFiles (1 hit each) rather than assumed. It caught a real defect: an unused React import in the console test (TS6133), fixed in 64d8fbe2a.
  • Gates: check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:phantom-deps, check:self-import, check:published-tsconfig-exclude, check:side-effects-array, check-changeset-fixed, check-changeset-no-major, check-type-check-coverage, check-lint-coverage — all report their own OK line. These scan tracked files, so they were re-run after committing; the counts moved with the new files (control-bytes 6157 to 6160 tracked text files; vi-mock 4224 to 4226 source files and 122 to 123 judged call sites), which is what makes the greens readings rather than scans that never saw the diff.

Lint — narrowed, and the narrowing is measured.eslint --no-inline-config over the 6 changed .ts/.tsx files: 0 errors, 424 warnings, all pre-existing repo-wide classes (no-explicit-any, react-refresh/only-export-components); the sibling ObjectView.ganttBinding-7070.test.tsx produces 4 warnings of the same class, so the new files add no new one. Three pieces of evidence that this excludes nothing: the receiving population came from eslint's own config (all 6 files were linted, none silently ignored); the count of 6 is read from --format json, not asserted; and eslint.config.js sets no parserOptions.project / projectService, so linting here is not type-aware and a change in one file cannot move the verdict on a file it does not touch. --max-warnings is deliberately unset in this repo, per lint.yml.

Clause ②, re-declared from this diff

no. The seven files this diff touches are two renderer faces, one comment-only app-shell note, three test files and one changeset. Nothing under packages/types/, no Zod schema, no spec surface, no authorable key — the view layer stops fabricating; the published accept surface is untouched. Control for that zero: the same path filter matches packages/types/src/zod/data-display.zod.ts when fed it.

Out of scope, filed

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

…xis floors
objectui#7070 step ③ of the maintainer ruling of 2026-09-01 (总监批 #28): house
posture 日期轴永不虚构 — a date axis is never fabricated.
`ListView` and `plugin-view`'s `ObjectView` both floored `startDateField` at
'created_at' for a timeline view that declared no axis. `ObjectTimeline` reads
that flat prop at the tail of its resolver chain, so the faces answered "the
axis is bound" for every view and the refusal screen step ① installed
(objectui#7459, landed as 20cb8db) was unreachable from either route — which
is why ①② changed nothing a user could see. Both branches now forward a
declared axis or no key at all.
The floor was never a harmless default: the $select projection is collected
from the DECLARED timeline / options.timeline blocks and never from this prop,
so an undeclared view rendered a timeline bound to a column the query had not
requested and bucketed every record into "No date".
The ruling also explicitly replaces the written decision that stood on the
deleted ListView line, and asks the objectui#3129 note at the app-shell face to
be realigned to the new reality once ③ lands; both are done here.
`titleField` keeps its 'name' floor at both faces — it is not a date axis.
Tests: the two face-level pins measure the prop; a new console-level test
composes the real ListView / ObjectView with the REAL ObjectTimeline and
asserts the refusal on screen, since neither package can see the join.
Part of #7070
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
…n test
`tsc --noEmit` in apps/console reads it as TS6133; the automatic JSX runtime
makes the import unnecessary. Caught by the package's own type-check, not by
vitest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3181.1 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-PRFOV0mr.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.67KB5.75KB
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)516.24KB117.82KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
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)34.34KB9.17KB
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)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.19KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.54KB27.65KB
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)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.93KB21.13KB
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.74KB2.25KB
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)8.11KB3.32KB
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-project-managerClaude

Copy link
Copy Markdown
Collaborator

✅ Landing — 32/32 green, Clause-② no verified from the diff, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC.

Checks, read by name

29 success, 3 skipped by design (dependabot, Test (coverage), Test (coverage shard …)) on head 64d8fbe2a. The long poles: Test (shard 1/4) 11:55:07 · 2/4 11:54:07 · 3/4 11:53:02 · 4/4 11:53:32 · Type Check 11:47:40 · Lint 11:45:03. Also read individually: Bundle Analysis, README Export Check, Governed Surface Queue Guard, Docs Route Eager Closure Check, Control Byte Scan, Inert vi.mock Specifier Check.

Landing authority

Clause-②: no, re-derived by this seat from the diff rather than taken from the declaration — the gate's own rule is 「diff 是事实,卡片语义是预测」. Seven files: two renderer faces, one comment-only app-shell note, three tests, one changeset. Nothing under packages/spec/**, no *.zod.ts, nothing under packages/types/src. ⇒ neither limb fires; this seat lands it without a contract review.

What I verified by content, not from the report

probeplugin-list/ListView.tsxplugin-view/ObjectView.tsx
startDateField: … || 'created_at'1 → 01 → 0
CONTROL titleField: … || 'name' (kept on purpose)2 → 23 → 3

The control is non-zero and unmoved, so the probe discriminates and the change is surgical.

⚠️A probe of mine that was wrong, recorded because it would mislead the next reader too: a raw word-count of created_at in plugin-view/ObjectView.tsx reads 1 → 1 and looks like the floor survived. It did not — the residual occurrence is inside the new comment documenting the retirement. Only the declaration form is an honest probe here. Same class as the tombstone inflation this lane hit earlier today on a different card: count the form, never the word.

The packages/app-shell edit was checked to be comment-only by filtering the diff for non-comment added/removed lines and getting an empty set — not by trusting the claim. It realigns the #3129 note, which had been certifying the two sibling faces as still-fabricating.

Why this step matters more than its diff size suggests

#7459 installed ObjectTimeline's refusal and then measured that nothing began refusing. This is why: ObjectTimeline reads the flat schema.startDateField at the tail of its resolver chain, so a floor at either face answered "the axis is bound" for every view and the refusal was unreachable from both routes. The invented axis was also never fetched$select is collected from the declared timeline blocks, never from this prop — so an undeclared view got a timeline bound to a column the query had not requested and bucketed every record into "No date". A screen that looks built, is wrong, and tells the author nothing.

⭐ The ablation's DOM dump under restored floors is the evidence that matters: data-testid="timeline-canvas" rendering Spring Launch — a healthy, convincing, entirely wrong timeline, not a crash. That is what the pin has to be able to fail on.

Two things the implementer did that I want on the record

  1. The first ablation anchor matched two sites in ListView.tsx (the same conditional-spread text also builds resolvedTimeline's nested config, untouched here). The assertion refused and aborted rather than mutating the wrong line. That is the protocol catching precisely what it exists to catch; both anchors are now two-line.
  2. InterfaceListPage:413-414 was reported as a boundary and deliberately NOT filed as a finding. It derives the binding from defaultCalendarFromObject(objectDef) — a field the object declares, or undefined. Derivation, not fabrication, so it is consistent with 日期轴永不虚构 rather than a surviving instance of it. Naming it is what shows it was measured rather than missed, and it is the one remaining route by which a view that declared no axis still gets one.

Card state

Part of #7070 is deliberately not a closing keyword — #7070 stays open on flavour 3, now filed as #7499 (progress / dependencies, re-measured as surviving at two faces, not three; the app-shell face carries neither key). #7070 was also moved pm:queuepm:dispatched and assigned, closing a half-state the implementer correctly reported back instead of writing the assignee field itself.


Generated by Claude Code

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.

2 participants

@os-project-manager@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(plugin-list,plugin-view): retire the 'created_at' timeline date-axis floors so the refusal becomes reachable - #7500

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-7070-step3-drop-created-at-floors
Sep 3, 2026
Merged

fix(plugin-list,plugin-view): retire the 'created_at' timeline date-axis floors so the refusal becomes reachable#7500
os-project-manager merged 2 commits into
mainfrom
claude/issue-7070-step3-drop-created-at-floors

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Part of #7070

Step ③ of the maintainer ruling of 2026-09-01 (总监批 #28, comment 5494805467): house posture 日期轴永不虚构 — a date axis is never fabricated. ListView and plugin-view's ObjectView stop flooring the timeline axis at 'created_at'.

Steps ①② are landed (20cb8db9b, PR #7467, card #7459) and this branch is cut from exactly that commit. ⛔ Nothing in ObjectTimeline, the refusal screen or the locale key is touched here.

The two lines

FaceWasNow
packages/plugin-list/src/ListView.tsx:2496startDateField: dateBinding.startDateField || 'created_at'conditional spread — the key is absent when no axis resolves
packages/plugin-view/src/ObjectView.tsx:1354startDateField: ...startDateField || ...dateField || 'created_at'alias resolved into a local, then the same conditional spread

Line numbers re-derived on main at 20cb8db9b, not inherited from the card. created_at occurs 4 times in ListView.tsx and 1 in ObjectView.tsx; those are occurrences of the string, not floors. Read per line, exactly one floor per file:

  • ListView.tsx:1753s.add('created_at') in knownObjectFields. A platform-column reference, kept.
  • ListView.tsx:1785 — prose explaining the PLATFORM_RECORD_COLUMNS carve-out, kept.
  • ListView.tsx:2496the floor. Deleted.
  • ObjectView.tsx:1354the floor. Deleted.

titleField keeps its 'name' floor at both faces: not a date axis, and the same display-name rung the gallery / kanban / gantt branches carry. Pinned as scope in both new suites.

Why it was never a harmless default

ObjectTimeline reads the flatschema.startDateField at the tail of its resolver chain — plugin-timeline's own CONTROL block proves that rung is fully honoured — so a floor here answered "the axis is bound" for every view and the refusal step ① installed was unreachable from either route. That is exactly why #7459's own measurement recorded that nothing began refusing when ①② landed.

And the invented axis was never fetched: the $select projection is collected from the DECLARED timeline / options.timeline blocks (collectViewFields), never from this prop. An undeclared view therefore got a timeline bound to a column the query had not requested and bucketed every record into "No date" — the screen that looks built, is wrong, and gives the author no signal.

The ruling also explicitly replaces the written decision that stood on the deleted ListView line ("created_at stays the last resort for a view that declares no date axis anywhere"), and asks the objectui#3129 note at the app-shell face to be realigned once ③ lands. Both are done here; the app-shell edit is comment-only.

⭐ Blast radius — measured, not asserted

Which authored surfaces in this repo reach these floors: zero. Which begin refusing: zero.

Every sweep below was run with a control that FIRED in the same run, because a zero from a grep is not a reading.

  • Authored metadata (json/yml/yaml) declaring a timeline view: 4 hits, none of them reach either face. The three examples/schema-catalog/src/schemas/plugin-timeline/*.json documents and packages/types/examples/data-display-examples.json are direct type: 'timeline' schemas carrying authored items / events — they never traverse ListView or generateViewSchema, and hasAuthoredItems short-circuits the refusal predicate (!hasAuthoredItems && !startDateField) regardless. Control: the identical regex for "calendar" fires on components-complex-view-switcher/view-switcher.json:24.
  • Authored TS/TSX samples declaring a timeline view: zero outside tests. Control: the identical regex for 'calendar' fires on six sites.
  • appearance.allowedVisualizations naming timeline in authored metadata: zero.

Two product routes were then measured rather than reasoned about, and both are why the radius is small:

  1. The console's own authoring UI cannot produce an undeclared timeline view.CreateViewDialog's REQUIRED_FIELDS table lists timeline.startDateField among "the sub-config keys a view type must have set before a record can be persisted", with a smart default auto-picked from the object's real date fields (PRIMARY_DATE_PREFERRED). Anything created in-product carries a declared axis.
  2. Interface list pages auto-derive one.InterfaceListPage:413-414 fills timeline from defaultCalendarFromObject(objectDef) when timeline is whitelisted — a field the object declares, or undefined. So such a page refuses only when the object carries no date field at all, which is the correct answer: there is no axis to declare. This is derivation from schema, not fabrication, and the fix(timeline): honor startDateField when rendering a ListView timeline #3129 note already cites it as the good shape.

What is left is metadata authored outside those paths — hand-written or migrated views. Those begin refusing, which is what the ruling ordered, and the refusal names the keys to declare so an affected view reports its own fix. The changeset carries the migration note.

⛔ No replacement fallback was added anywhere.

Verification

Red-then-green, taken naturally first. Both face-level suites were written and run against the UNMODIFIED tree at 20cb8db9b before the first source edit: plugin-view 3 failed / 5 passed, plugin-list 3 failed / 14 passed — the failures exactly the "invents no axis" cases, every render proof and CONTROL green in the same run. After the edit: 25/25.

Ablation, for the console composition test, which was written after the fix and so had never been observed red. Both floors restored, proven on disk in both directions before the run (removed-text 0 AND injected-text 1 per file, plus a blob-hash change on each), trap ... EXIT INT TERM with absolute paths, restore proven by state (git diff HEAD empty + blob equality against the HEAD blob), never by an exit code. No build/dist is involved — vitest resolves @object-ui/* to sibling src/ through the root config's alias table — so there is no stale-artifact leg to prove.

Under the restored floors: 9 failed / 24 passed. The 9 are precisely the "invents no axis" and "refuses on screen" cases at all three levels; every render proof and every CONTROL stayed green in the mutated world. The DOM dump from that run is the point of the exercise — it shows data-testid="timeline-canvas" and the text Spring Launch: a healthy, convincing, entirely wrong timeline, not an error. That is what the pin has to be able to fail on.

⚠️ One anchor note, recorded because it is the failure this protocol exists to catch: the first ablation anchor matched 2 sites in ListView.tsx — the identical conditional-spread text also builds resolvedTimeline's nested config a few lines up, which this card does not touch. The assertion refused and the run aborted rather than mutating the wrong line; both anchors are now two-line, pinned by the titleField: rung that follows the floor.

Refusal asserted positively. Every block opens with a render-proof row, and each absence marker is asserted PRESENT by a control in the same run — a test that only says "no timeline" is satisfied by a component that threw.

Test files

  • packages/plugin-list/src/__tests__/ListView.timeline-binding.test.tsx — the fix(timeline): honor startDateField when rendering a ListView timeline #3129 case that pinned the floor is rewritten, not deleted (it is where the retirement has to become visible); a new step-③ block adds the render proof, the two absence cases and the scope controls; a capability-gate block records, in both directions, that the ADR-0047 toggle is unchanged by this step — unlike the gantt face, the Timeline gate always read resolveTimelineDateBinding and never the flat floor.
  • packages/plugin-view/src/__tests__/ObjectView.timelineBinding-7070.test.tsx — new; the sibling of ObjectView.ganttBinding-7070 / calendarBinding-7029.
  • apps/console/src/__tests__/timelineAxisRefusalReach-7070.test.tsx — new, and the reason it lives in the console: it is the only package depending on plugin-list, plugin-viewandplugin-timeline, so it is the only place the join can be measured. Real faces, real renderer, no stub. Its rows deliberately carry a real created_at column so a restored floor renders a convincing chart rather than an empty one.

Runs — union re-run after the final commit, at 64d8fbe2a:

  • pnpm exec vitest run packages/plugin-list packages/plugin-view packages/plugin-timeline apps/console207 files / 2357 tests passed.
  • app-shell, narrowed and declared: the 26 files that mention timeline or are views/ObjectView* — 290 passed. The app-shell edit is comment-only; the full 608-file suite is CI's.
  • Downstream sweep (plugin-timeline, plugin-gantt refusal pin, react smoke, core normalize-list-view, types timeline pins, schema-catalog render) — 29 files / 991 passed.
  • type-check for plugin-list, plugin-view, app-shell, console — green. Each package chains tsc -p tsconfig.test.json, and both new package-level test files were confirmed present in that program with --listFiles (1 hit each) rather than assumed. It caught a real defect: an unused React import in the console test (TS6133), fixed in 64d8fbe2a.
  • Gates: check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:phantom-deps, check:self-import, check:published-tsconfig-exclude, check:side-effects-array, check-changeset-fixed, check-changeset-no-major, check-type-check-coverage, check-lint-coverage — all report their own OK line. These scan tracked files, so they were re-run after committing; the counts moved with the new files (control-bytes 6157 to 6160 tracked text files; vi-mock 4224 to 4226 source files and 122 to 123 judged call sites), which is what makes the greens readings rather than scans that never saw the diff.

Lint — narrowed, and the narrowing is measured.eslint --no-inline-config over the 6 changed .ts/.tsx files: 0 errors, 424 warnings, all pre-existing repo-wide classes (no-explicit-any, react-refresh/only-export-components); the sibling ObjectView.ganttBinding-7070.test.tsx produces 4 warnings of the same class, so the new files add no new one. Three pieces of evidence that this excludes nothing: the receiving population came from eslint's own config (all 6 files were linted, none silently ignored); the count of 6 is read from --format json, not asserted; and eslint.config.js sets no parserOptions.project / projectService, so linting here is not type-aware and a change in one file cannot move the verdict on a file it does not touch. --max-warnings is deliberately unset in this repo, per lint.yml.

Clause ②, re-declared from this diff

no. The seven files this diff touches are two renderer faces, one comment-only app-shell note, three test files and one changeset. Nothing under packages/types/, no Zod schema, no spec surface, no authorable key — the view layer stops fabricating; the published accept surface is untouched. Control for that zero: the same path filter matches packages/types/src/zod/data-display.zod.ts when fed it.

Out of scope, filed

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

…xis floors
objectui#7070 step ③ of the maintainer ruling of 2026-09-01 (总监批 #28): house
posture 日期轴永不虚构 — a date axis is never fabricated.
`ListView` and `plugin-view`'s `ObjectView` both floored `startDateField` at
'created_at' for a timeline view that declared no axis. `ObjectTimeline` reads
that flat prop at the tail of its resolver chain, so the faces answered "the
axis is bound" for every view and the refusal screen step ① installed
(objectui#7459, landed as 20cb8db) was unreachable from either route — which
is why ①② changed nothing a user could see. Both branches now forward a
declared axis or no key at all.
The floor was never a harmless default: the $select projection is collected
from the DECLARED timeline / options.timeline blocks and never from this prop,
so an undeclared view rendered a timeline bound to a column the query had not
requested and bucketed every record into "No date".
The ruling also explicitly replaces the written decision that stood on the
deleted ListView line, and asks the objectui#3129 note at the app-shell face to
be realigned to the new reality once ③ lands; both are done here.
`titleField` keeps its 'name' floor at both faces — it is not a date axis.
Tests: the two face-level pins measure the prop; a new console-level test
composes the real ListView / ObjectView with the REAL ObjectTimeline and
asserts the refusal on screen, since neither package can see the join.
Part of #7070
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
…n test
`tsc --noEmit` in apps/console reads it as TS6133; the automatic JSX runtime
makes the import unnecessary. Caught by the package's own type-check, not by
vitest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3181.1 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-PRFOV0mr.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.67KB5.75KB
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)516.24KB117.82KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
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)34.34KB9.17KB
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)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.19KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.54KB27.65KB
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)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.93KB21.13KB
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.74KB2.25KB
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)8.11KB3.32KB
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-project-managerClaude

Copy link
Copy Markdown
Collaborator

✅ Landing — 32/32 green, Clause-② no verified from the diff, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC.

Checks, read by name

29 success, 3 skipped by design (dependabot, Test (coverage), Test (coverage shard …)) on head 64d8fbe2a. The long poles: Test (shard 1/4) 11:55:07 · 2/4 11:54:07 · 3/4 11:53:02 · 4/4 11:53:32 · Type Check 11:47:40 · Lint 11:45:03. Also read individually: Bundle Analysis, README Export Check, Governed Surface Queue Guard, Docs Route Eager Closure Check, Control Byte Scan, Inert vi.mock Specifier Check.

Landing authority

Clause-②: no, re-derived by this seat from the diff rather than taken from the declaration — the gate's own rule is 「diff 是事实,卡片语义是预测」. Seven files: two renderer faces, one comment-only app-shell note, three tests, one changeset. Nothing under packages/spec/**, no *.zod.ts, nothing under packages/types/src. ⇒ neither limb fires; this seat lands it without a contract review.

What I verified by content, not from the report

probeplugin-list/ListView.tsxplugin-view/ObjectView.tsx
startDateField: … || 'created_at'1 → 01 → 0
CONTROL titleField: … || 'name' (kept on purpose)2 → 23 → 3

The control is non-zero and unmoved, so the probe discriminates and the change is surgical.

⚠️A probe of mine that was wrong, recorded because it would mislead the next reader too: a raw word-count of created_at in plugin-view/ObjectView.tsx reads 1 → 1 and looks like the floor survived. It did not — the residual occurrence is inside the new comment documenting the retirement. Only the declaration form is an honest probe here. Same class as the tombstone inflation this lane hit earlier today on a different card: count the form, never the word.

The packages/app-shell edit was checked to be comment-only by filtering the diff for non-comment added/removed lines and getting an empty set — not by trusting the claim. It realigns the #3129 note, which had been certifying the two sibling faces as still-fabricating.

Why this step matters more than its diff size suggests

#7459 installed ObjectTimeline's refusal and then measured that nothing began refusing. This is why: ObjectTimeline reads the flat schema.startDateField at the tail of its resolver chain, so a floor at either face answered "the axis is bound" for every view and the refusal was unreachable from both routes. The invented axis was also never fetched$select is collected from the declared timeline blocks, never from this prop — so an undeclared view got a timeline bound to a column the query had not requested and bucketed every record into "No date". A screen that looks built, is wrong, and tells the author nothing.

⭐ The ablation's DOM dump under restored floors is the evidence that matters: data-testid="timeline-canvas" rendering Spring Launch — a healthy, convincing, entirely wrong timeline, not a crash. That is what the pin has to be able to fail on.

Two things the implementer did that I want on the record

  1. The first ablation anchor matched two sites in ListView.tsx (the same conditional-spread text also builds resolvedTimeline's nested config, untouched here). The assertion refused and aborted rather than mutating the wrong line. That is the protocol catching precisely what it exists to catch; both anchors are now two-line.
  2. InterfaceListPage:413-414 was reported as a boundary and deliberately NOT filed as a finding. It derives the binding from defaultCalendarFromObject(objectDef) — a field the object declares, or undefined. Derivation, not fabrication, so it is consistent with 日期轴永不虚构 rather than a surviving instance of it. Naming it is what shows it was measured rather than missed, and it is the one remaining route by which a view that declared no axis still gets one.

Card state

Part of #7070 is deliberately not a closing keyword — #7070 stays open on flavour 3, now filed as #7499 (progress / dependencies, re-measured as surviving at two faces, not three; the app-shell face carries neither key). #7070 was also moved pm:queuepm:dispatched and assigned, closing a half-state the implementer correctly reported back instead of writing the assignee field itself.


Generated by Claude Code

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.

2 participants

@os-project-manager@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(plugin-list,plugin-view): retire the 'created_at' timeline date-axis floors so the refusal becomes reachable - #7500

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-7070-step3-drop-created-at-floors
Sep 3, 2026
Merged

fix(plugin-list,plugin-view): retire the 'created_at' timeline date-axis floors so the refusal becomes reachable#7500
os-project-manager merged 2 commits into
mainfrom
claude/issue-7070-step3-drop-created-at-floors

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Part of #7070

Step ③ of the maintainer ruling of 2026-09-01 (总监批 #28, comment 5494805467): house posture 日期轴永不虚构 — a date axis is never fabricated. ListView and plugin-view's ObjectView stop flooring the timeline axis at 'created_at'.

Steps ①② are landed (20cb8db9b, PR #7467, card #7459) and this branch is cut from exactly that commit. ⛔ Nothing in ObjectTimeline, the refusal screen or the locale key is touched here.

The two lines

FaceWasNow
packages/plugin-list/src/ListView.tsx:2496startDateField: dateBinding.startDateField || 'created_at'conditional spread — the key is absent when no axis resolves
packages/plugin-view/src/ObjectView.tsx:1354startDateField: ...startDateField || ...dateField || 'created_at'alias resolved into a local, then the same conditional spread

Line numbers re-derived on main at 20cb8db9b, not inherited from the card. created_at occurs 4 times in ListView.tsx and 1 in ObjectView.tsx; those are occurrences of the string, not floors. Read per line, exactly one floor per file:

  • ListView.tsx:1753s.add('created_at') in knownObjectFields. A platform-column reference, kept.
  • ListView.tsx:1785 — prose explaining the PLATFORM_RECORD_COLUMNS carve-out, kept.
  • ListView.tsx:2496the floor. Deleted.
  • ObjectView.tsx:1354the floor. Deleted.

titleField keeps its 'name' floor at both faces: not a date axis, and the same display-name rung the gallery / kanban / gantt branches carry. Pinned as scope in both new suites.

Why it was never a harmless default

ObjectTimeline reads the flatschema.startDateField at the tail of its resolver chain — plugin-timeline's own CONTROL block proves that rung is fully honoured — so a floor here answered "the axis is bound" for every view and the refusal step ① installed was unreachable from either route. That is exactly why #7459's own measurement recorded that nothing began refusing when ①② landed.

And the invented axis was never fetched: the $select projection is collected from the DECLARED timeline / options.timeline blocks (collectViewFields), never from this prop. An undeclared view therefore got a timeline bound to a column the query had not requested and bucketed every record into "No date" — the screen that looks built, is wrong, and gives the author no signal.

The ruling also explicitly replaces the written decision that stood on the deleted ListView line ("created_at stays the last resort for a view that declares no date axis anywhere"), and asks the objectui#3129 note at the app-shell face to be realigned once ③ lands. Both are done here; the app-shell edit is comment-only.

⭐ Blast radius — measured, not asserted

Which authored surfaces in this repo reach these floors: zero. Which begin refusing: zero.

Every sweep below was run with a control that FIRED in the same run, because a zero from a grep is not a reading.

  • Authored metadata (json/yml/yaml) declaring a timeline view: 4 hits, none of them reach either face. The three examples/schema-catalog/src/schemas/plugin-timeline/*.json documents and packages/types/examples/data-display-examples.json are direct type: 'timeline' schemas carrying authored items / events — they never traverse ListView or generateViewSchema, and hasAuthoredItems short-circuits the refusal predicate (!hasAuthoredItems && !startDateField) regardless. Control: the identical regex for "calendar" fires on components-complex-view-switcher/view-switcher.json:24.
  • Authored TS/TSX samples declaring a timeline view: zero outside tests. Control: the identical regex for 'calendar' fires on six sites.
  • appearance.allowedVisualizations naming timeline in authored metadata: zero.

Two product routes were then measured rather than reasoned about, and both are why the radius is small:

  1. The console's own authoring UI cannot produce an undeclared timeline view.CreateViewDialog's REQUIRED_FIELDS table lists timeline.startDateField among "the sub-config keys a view type must have set before a record can be persisted", with a smart default auto-picked from the object's real date fields (PRIMARY_DATE_PREFERRED). Anything created in-product carries a declared axis.
  2. Interface list pages auto-derive one.InterfaceListPage:413-414 fills timeline from defaultCalendarFromObject(objectDef) when timeline is whitelisted — a field the object declares, or undefined. So such a page refuses only when the object carries no date field at all, which is the correct answer: there is no axis to declare. This is derivation from schema, not fabrication, and the fix(timeline): honor startDateField when rendering a ListView timeline #3129 note already cites it as the good shape.

What is left is metadata authored outside those paths — hand-written or migrated views. Those begin refusing, which is what the ruling ordered, and the refusal names the keys to declare so an affected view reports its own fix. The changeset carries the migration note.

⛔ No replacement fallback was added anywhere.

Verification

Red-then-green, taken naturally first. Both face-level suites were written and run against the UNMODIFIED tree at 20cb8db9b before the first source edit: plugin-view 3 failed / 5 passed, plugin-list 3 failed / 14 passed — the failures exactly the "invents no axis" cases, every render proof and CONTROL green in the same run. After the edit: 25/25.

Ablation, for the console composition test, which was written after the fix and so had never been observed red. Both floors restored, proven on disk in both directions before the run (removed-text 0 AND injected-text 1 per file, plus a blob-hash change on each), trap ... EXIT INT TERM with absolute paths, restore proven by state (git diff HEAD empty + blob equality against the HEAD blob), never by an exit code. No build/dist is involved — vitest resolves @object-ui/* to sibling src/ through the root config's alias table — so there is no stale-artifact leg to prove.

Under the restored floors: 9 failed / 24 passed. The 9 are precisely the "invents no axis" and "refuses on screen" cases at all three levels; every render proof and every CONTROL stayed green in the mutated world. The DOM dump from that run is the point of the exercise — it shows data-testid="timeline-canvas" and the text Spring Launch: a healthy, convincing, entirely wrong timeline, not an error. That is what the pin has to be able to fail on.

⚠️ One anchor note, recorded because it is the failure this protocol exists to catch: the first ablation anchor matched 2 sites in ListView.tsx — the identical conditional-spread text also builds resolvedTimeline's nested config a few lines up, which this card does not touch. The assertion refused and the run aborted rather than mutating the wrong line; both anchors are now two-line, pinned by the titleField: rung that follows the floor.

Refusal asserted positively. Every block opens with a render-proof row, and each absence marker is asserted PRESENT by a control in the same run — a test that only says "no timeline" is satisfied by a component that threw.

Test files

  • packages/plugin-list/src/__tests__/ListView.timeline-binding.test.tsx — the fix(timeline): honor startDateField when rendering a ListView timeline #3129 case that pinned the floor is rewritten, not deleted (it is where the retirement has to become visible); a new step-③ block adds the render proof, the two absence cases and the scope controls; a capability-gate block records, in both directions, that the ADR-0047 toggle is unchanged by this step — unlike the gantt face, the Timeline gate always read resolveTimelineDateBinding and never the flat floor.
  • packages/plugin-view/src/__tests__/ObjectView.timelineBinding-7070.test.tsx — new; the sibling of ObjectView.ganttBinding-7070 / calendarBinding-7029.
  • apps/console/src/__tests__/timelineAxisRefusalReach-7070.test.tsx — new, and the reason it lives in the console: it is the only package depending on plugin-list, plugin-viewandplugin-timeline, so it is the only place the join can be measured. Real faces, real renderer, no stub. Its rows deliberately carry a real created_at column so a restored floor renders a convincing chart rather than an empty one.

Runs — union re-run after the final commit, at 64d8fbe2a:

  • pnpm exec vitest run packages/plugin-list packages/plugin-view packages/plugin-timeline apps/console207 files / 2357 tests passed.
  • app-shell, narrowed and declared: the 26 files that mention timeline or are views/ObjectView* — 290 passed. The app-shell edit is comment-only; the full 608-file suite is CI's.
  • Downstream sweep (plugin-timeline, plugin-gantt refusal pin, react smoke, core normalize-list-view, types timeline pins, schema-catalog render) — 29 files / 991 passed.
  • type-check for plugin-list, plugin-view, app-shell, console — green. Each package chains tsc -p tsconfig.test.json, and both new package-level test files were confirmed present in that program with --listFiles (1 hit each) rather than assumed. It caught a real defect: an unused React import in the console test (TS6133), fixed in 64d8fbe2a.
  • Gates: check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:phantom-deps, check:self-import, check:published-tsconfig-exclude, check:side-effects-array, check-changeset-fixed, check-changeset-no-major, check-type-check-coverage, check-lint-coverage — all report their own OK line. These scan tracked files, so they were re-run after committing; the counts moved with the new files (control-bytes 6157 to 6160 tracked text files; vi-mock 4224 to 4226 source files and 122 to 123 judged call sites), which is what makes the greens readings rather than scans that never saw the diff.

Lint — narrowed, and the narrowing is measured.eslint --no-inline-config over the 6 changed .ts/.tsx files: 0 errors, 424 warnings, all pre-existing repo-wide classes (no-explicit-any, react-refresh/only-export-components); the sibling ObjectView.ganttBinding-7070.test.tsx produces 4 warnings of the same class, so the new files add no new one. Three pieces of evidence that this excludes nothing: the receiving population came from eslint's own config (all 6 files were linted, none silently ignored); the count of 6 is read from --format json, not asserted; and eslint.config.js sets no parserOptions.project / projectService, so linting here is not type-aware and a change in one file cannot move the verdict on a file it does not touch. --max-warnings is deliberately unset in this repo, per lint.yml.

Clause ②, re-declared from this diff

no. The seven files this diff touches are two renderer faces, one comment-only app-shell note, three test files and one changeset. Nothing under packages/types/, no Zod schema, no spec surface, no authorable key — the view layer stops fabricating; the published accept surface is untouched. Control for that zero: the same path filter matches packages/types/src/zod/data-display.zod.ts when fed it.

Out of scope, filed

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

…xis floors
objectui#7070 step ③ of the maintainer ruling of 2026-09-01 (总监批 #28): house
posture 日期轴永不虚构 — a date axis is never fabricated.
`ListView` and `plugin-view`'s `ObjectView` both floored `startDateField` at
'created_at' for a timeline view that declared no axis. `ObjectTimeline` reads
that flat prop at the tail of its resolver chain, so the faces answered "the
axis is bound" for every view and the refusal screen step ① installed
(objectui#7459, landed as 20cb8db) was unreachable from either route — which
is why ①② changed nothing a user could see. Both branches now forward a
declared axis or no key at all.
The floor was never a harmless default: the $select projection is collected
from the DECLARED timeline / options.timeline blocks and never from this prop,
so an undeclared view rendered a timeline bound to a column the query had not
requested and bucketed every record into "No date".
The ruling also explicitly replaces the written decision that stood on the
deleted ListView line, and asks the objectui#3129 note at the app-shell face to
be realigned to the new reality once ③ lands; both are done here.
`titleField` keeps its 'name' floor at both faces — it is not a date axis.
Tests: the two face-level pins measure the prop; a new console-level test
composes the real ListView / ObjectView with the REAL ObjectTimeline and
asserts the refusal on screen, since neither package can see the join.
Part of #7070
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
…n test
`tsc --noEmit` in apps/console reads it as TS6133; the automatic JSX runtime
makes the import unnecessary. Caught by the package's own type-check, not by
vitest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3181.1 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-PRFOV0mr.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.67KB5.75KB
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)516.24KB117.82KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
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)34.34KB9.17KB
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)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.19KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.54KB27.65KB
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)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.93KB21.13KB
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.74KB2.25KB
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)8.11KB3.32KB
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-project-managerClaude

Copy link
Copy Markdown
Collaborator

✅ Landing — 32/32 green, Clause-② no verified from the diff, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC.

Checks, read by name

29 success, 3 skipped by design (dependabot, Test (coverage), Test (coverage shard …)) on head 64d8fbe2a. The long poles: Test (shard 1/4) 11:55:07 · 2/4 11:54:07 · 3/4 11:53:02 · 4/4 11:53:32 · Type Check 11:47:40 · Lint 11:45:03. Also read individually: Bundle Analysis, README Export Check, Governed Surface Queue Guard, Docs Route Eager Closure Check, Control Byte Scan, Inert vi.mock Specifier Check.

Landing authority

Clause-②: no, re-derived by this seat from the diff rather than taken from the declaration — the gate's own rule is 「diff 是事实,卡片语义是预测」. Seven files: two renderer faces, one comment-only app-shell note, three tests, one changeset. Nothing under packages/spec/**, no *.zod.ts, nothing under packages/types/src. ⇒ neither limb fires; this seat lands it without a contract review.

What I verified by content, not from the report

probeplugin-list/ListView.tsxplugin-view/ObjectView.tsx
startDateField: … || 'created_at'1 → 01 → 0
CONTROL titleField: … || 'name' (kept on purpose)2 → 23 → 3

The control is non-zero and unmoved, so the probe discriminates and the change is surgical.

⚠️A probe of mine that was wrong, recorded because it would mislead the next reader too: a raw word-count of created_at in plugin-view/ObjectView.tsx reads 1 → 1 and looks like the floor survived. It did not — the residual occurrence is inside the new comment documenting the retirement. Only the declaration form is an honest probe here. Same class as the tombstone inflation this lane hit earlier today on a different card: count the form, never the word.

The packages/app-shell edit was checked to be comment-only by filtering the diff for non-comment added/removed lines and getting an empty set — not by trusting the claim. It realigns the #3129 note, which had been certifying the two sibling faces as still-fabricating.

Why this step matters more than its diff size suggests

#7459 installed ObjectTimeline's refusal and then measured that nothing began refusing. This is why: ObjectTimeline reads the flat schema.startDateField at the tail of its resolver chain, so a floor at either face answered "the axis is bound" for every view and the refusal was unreachable from both routes. The invented axis was also never fetched$select is collected from the declared timeline blocks, never from this prop — so an undeclared view got a timeline bound to a column the query had not requested and bucketed every record into "No date". A screen that looks built, is wrong, and tells the author nothing.

⭐ The ablation's DOM dump under restored floors is the evidence that matters: data-testid="timeline-canvas" rendering Spring Launch — a healthy, convincing, entirely wrong timeline, not a crash. That is what the pin has to be able to fail on.

Two things the implementer did that I want on the record

  1. The first ablation anchor matched two sites in ListView.tsx (the same conditional-spread text also builds resolvedTimeline's nested config, untouched here). The assertion refused and aborted rather than mutating the wrong line. That is the protocol catching precisely what it exists to catch; both anchors are now two-line.
  2. InterfaceListPage:413-414 was reported as a boundary and deliberately NOT filed as a finding. It derives the binding from defaultCalendarFromObject(objectDef) — a field the object declares, or undefined. Derivation, not fabrication, so it is consistent with 日期轴永不虚构 rather than a surviving instance of it. Naming it is what shows it was measured rather than missed, and it is the one remaining route by which a view that declared no axis still gets one.

Card state

Part of #7070 is deliberately not a closing keyword — #7070 stays open on flavour 3, now filed as #7499 (progress / dependencies, re-measured as surviving at two faces, not three; the app-shell face carries neither key). #7070 was also moved pm:queuepm:dispatched and assigned, closing a half-state the implementer correctly reported back instead of writing the assignee field itself.


Generated by Claude Code

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.

2 participants

@os-project-manager@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(plugin-list,plugin-view): retire the 'created_at' timeline date-axis floors so the refusal becomes reachable - #7500

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-7070-step3-drop-created-at-floors
Sep 3, 2026
Merged

fix(plugin-list,plugin-view): retire the 'created_at' timeline date-axis floors so the refusal becomes reachable#7500
os-project-manager merged 2 commits into
mainfrom
claude/issue-7070-step3-drop-created-at-floors

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Part of #7070

Step ③ of the maintainer ruling of 2026-09-01 (总监批 #28, comment 5494805467): house posture 日期轴永不虚构 — a date axis is never fabricated. ListView and plugin-view's ObjectView stop flooring the timeline axis at 'created_at'.

Steps ①② are landed (20cb8db9b, PR #7467, card #7459) and this branch is cut from exactly that commit. ⛔ Nothing in ObjectTimeline, the refusal screen or the locale key is touched here.

The two lines

FaceWasNow
packages/plugin-list/src/ListView.tsx:2496startDateField: dateBinding.startDateField || 'created_at'conditional spread — the key is absent when no axis resolves
packages/plugin-view/src/ObjectView.tsx:1354startDateField: ...startDateField || ...dateField || 'created_at'alias resolved into a local, then the same conditional spread

Line numbers re-derived on main at 20cb8db9b, not inherited from the card. created_at occurs 4 times in ListView.tsx and 1 in ObjectView.tsx; those are occurrences of the string, not floors. Read per line, exactly one floor per file:

  • ListView.tsx:1753s.add('created_at') in knownObjectFields. A platform-column reference, kept.
  • ListView.tsx:1785 — prose explaining the PLATFORM_RECORD_COLUMNS carve-out, kept.
  • ListView.tsx:2496the floor. Deleted.
  • ObjectView.tsx:1354the floor. Deleted.

titleField keeps its 'name' floor at both faces: not a date axis, and the same display-name rung the gallery / kanban / gantt branches carry. Pinned as scope in both new suites.

Why it was never a harmless default

ObjectTimeline reads the flatschema.startDateField at the tail of its resolver chain — plugin-timeline's own CONTROL block proves that rung is fully honoured — so a floor here answered "the axis is bound" for every view and the refusal step ① installed was unreachable from either route. That is exactly why #7459's own measurement recorded that nothing began refusing when ①② landed.

And the invented axis was never fetched: the $select projection is collected from the DECLARED timeline / options.timeline blocks (collectViewFields), never from this prop. An undeclared view therefore got a timeline bound to a column the query had not requested and bucketed every record into "No date" — the screen that looks built, is wrong, and gives the author no signal.

The ruling also explicitly replaces the written decision that stood on the deleted ListView line ("created_at stays the last resort for a view that declares no date axis anywhere"), and asks the objectui#3129 note at the app-shell face to be realigned once ③ lands. Both are done here; the app-shell edit is comment-only.

⭐ Blast radius — measured, not asserted

Which authored surfaces in this repo reach these floors: zero. Which begin refusing: zero.

Every sweep below was run with a control that FIRED in the same run, because a zero from a grep is not a reading.

  • Authored metadata (json/yml/yaml) declaring a timeline view: 4 hits, none of them reach either face. The three examples/schema-catalog/src/schemas/plugin-timeline/*.json documents and packages/types/examples/data-display-examples.json are direct type: 'timeline' schemas carrying authored items / events — they never traverse ListView or generateViewSchema, and hasAuthoredItems short-circuits the refusal predicate (!hasAuthoredItems && !startDateField) regardless. Control: the identical regex for "calendar" fires on components-complex-view-switcher/view-switcher.json:24.
  • Authored TS/TSX samples declaring a timeline view: zero outside tests. Control: the identical regex for 'calendar' fires on six sites.
  • appearance.allowedVisualizations naming timeline in authored metadata: zero.

Two product routes were then measured rather than reasoned about, and both are why the radius is small:

  1. The console's own authoring UI cannot produce an undeclared timeline view.CreateViewDialog's REQUIRED_FIELDS table lists timeline.startDateField among "the sub-config keys a view type must have set before a record can be persisted", with a smart default auto-picked from the object's real date fields (PRIMARY_DATE_PREFERRED). Anything created in-product carries a declared axis.
  2. Interface list pages auto-derive one.InterfaceListPage:413-414 fills timeline from defaultCalendarFromObject(objectDef) when timeline is whitelisted — a field the object declares, or undefined. So such a page refuses only when the object carries no date field at all, which is the correct answer: there is no axis to declare. This is derivation from schema, not fabrication, and the fix(timeline): honor startDateField when rendering a ListView timeline #3129 note already cites it as the good shape.

What is left is metadata authored outside those paths — hand-written or migrated views. Those begin refusing, which is what the ruling ordered, and the refusal names the keys to declare so an affected view reports its own fix. The changeset carries the migration note.

⛔ No replacement fallback was added anywhere.

Verification

Red-then-green, taken naturally first. Both face-level suites were written and run against the UNMODIFIED tree at 20cb8db9b before the first source edit: plugin-view 3 failed / 5 passed, plugin-list 3 failed / 14 passed — the failures exactly the "invents no axis" cases, every render proof and CONTROL green in the same run. After the edit: 25/25.

Ablation, for the console composition test, which was written after the fix and so had never been observed red. Both floors restored, proven on disk in both directions before the run (removed-text 0 AND injected-text 1 per file, plus a blob-hash change on each), trap ... EXIT INT TERM with absolute paths, restore proven by state (git diff HEAD empty + blob equality against the HEAD blob), never by an exit code. No build/dist is involved — vitest resolves @object-ui/* to sibling src/ through the root config's alias table — so there is no stale-artifact leg to prove.

Under the restored floors: 9 failed / 24 passed. The 9 are precisely the "invents no axis" and "refuses on screen" cases at all three levels; every render proof and every CONTROL stayed green in the mutated world. The DOM dump from that run is the point of the exercise — it shows data-testid="timeline-canvas" and the text Spring Launch: a healthy, convincing, entirely wrong timeline, not an error. That is what the pin has to be able to fail on.

⚠️ One anchor note, recorded because it is the failure this protocol exists to catch: the first ablation anchor matched 2 sites in ListView.tsx — the identical conditional-spread text also builds resolvedTimeline's nested config a few lines up, which this card does not touch. The assertion refused and the run aborted rather than mutating the wrong line; both anchors are now two-line, pinned by the titleField: rung that follows the floor.

Refusal asserted positively. Every block opens with a render-proof row, and each absence marker is asserted PRESENT by a control in the same run — a test that only says "no timeline" is satisfied by a component that threw.

Test files

  • packages/plugin-list/src/__tests__/ListView.timeline-binding.test.tsx — the fix(timeline): honor startDateField when rendering a ListView timeline #3129 case that pinned the floor is rewritten, not deleted (it is where the retirement has to become visible); a new step-③ block adds the render proof, the two absence cases and the scope controls; a capability-gate block records, in both directions, that the ADR-0047 toggle is unchanged by this step — unlike the gantt face, the Timeline gate always read resolveTimelineDateBinding and never the flat floor.
  • packages/plugin-view/src/__tests__/ObjectView.timelineBinding-7070.test.tsx — new; the sibling of ObjectView.ganttBinding-7070 / calendarBinding-7029.
  • apps/console/src/__tests__/timelineAxisRefusalReach-7070.test.tsx — new, and the reason it lives in the console: it is the only package depending on plugin-list, plugin-viewandplugin-timeline, so it is the only place the join can be measured. Real faces, real renderer, no stub. Its rows deliberately carry a real created_at column so a restored floor renders a convincing chart rather than an empty one.

Runs — union re-run after the final commit, at 64d8fbe2a:

  • pnpm exec vitest run packages/plugin-list packages/plugin-view packages/plugin-timeline apps/console207 files / 2357 tests passed.
  • app-shell, narrowed and declared: the 26 files that mention timeline or are views/ObjectView* — 290 passed. The app-shell edit is comment-only; the full 608-file suite is CI's.
  • Downstream sweep (plugin-timeline, plugin-gantt refusal pin, react smoke, core normalize-list-view, types timeline pins, schema-catalog render) — 29 files / 991 passed.
  • type-check for plugin-list, plugin-view, app-shell, console — green. Each package chains tsc -p tsconfig.test.json, and both new package-level test files were confirmed present in that program with --listFiles (1 hit each) rather than assumed. It caught a real defect: an unused React import in the console test (TS6133), fixed in 64d8fbe2a.
  • Gates: check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:phantom-deps, check:self-import, check:published-tsconfig-exclude, check:side-effects-array, check-changeset-fixed, check-changeset-no-major, check-type-check-coverage, check-lint-coverage — all report their own OK line. These scan tracked files, so they were re-run after committing; the counts moved with the new files (control-bytes 6157 to 6160 tracked text files; vi-mock 4224 to 4226 source files and 122 to 123 judged call sites), which is what makes the greens readings rather than scans that never saw the diff.

Lint — narrowed, and the narrowing is measured.eslint --no-inline-config over the 6 changed .ts/.tsx files: 0 errors, 424 warnings, all pre-existing repo-wide classes (no-explicit-any, react-refresh/only-export-components); the sibling ObjectView.ganttBinding-7070.test.tsx produces 4 warnings of the same class, so the new files add no new one. Three pieces of evidence that this excludes nothing: the receiving population came from eslint's own config (all 6 files were linted, none silently ignored); the count of 6 is read from --format json, not asserted; and eslint.config.js sets no parserOptions.project / projectService, so linting here is not type-aware and a change in one file cannot move the verdict on a file it does not touch. --max-warnings is deliberately unset in this repo, per lint.yml.

Clause ②, re-declared from this diff

no. The seven files this diff touches are two renderer faces, one comment-only app-shell note, three test files and one changeset. Nothing under packages/types/, no Zod schema, no spec surface, no authorable key — the view layer stops fabricating; the published accept surface is untouched. Control for that zero: the same path filter matches packages/types/src/zod/data-display.zod.ts when fed it.

Out of scope, filed

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

…xis floors
objectui#7070 step ③ of the maintainer ruling of 2026-09-01 (总监批 #28): house
posture 日期轴永不虚构 — a date axis is never fabricated.
`ListView` and `plugin-view`'s `ObjectView` both floored `startDateField` at
'created_at' for a timeline view that declared no axis. `ObjectTimeline` reads
that flat prop at the tail of its resolver chain, so the faces answered "the
axis is bound" for every view and the refusal screen step ① installed
(objectui#7459, landed as 20cb8db) was unreachable from either route — which
is why ①② changed nothing a user could see. Both branches now forward a
declared axis or no key at all.
The floor was never a harmless default: the $select projection is collected
from the DECLARED timeline / options.timeline blocks and never from this prop,
so an undeclared view rendered a timeline bound to a column the query had not
requested and bucketed every record into "No date".
The ruling also explicitly replaces the written decision that stood on the
deleted ListView line, and asks the objectui#3129 note at the app-shell face to
be realigned to the new reality once ③ lands; both are done here.
`titleField` keeps its 'name' floor at both faces — it is not a date axis.
Tests: the two face-level pins measure the prop; a new console-level test
composes the real ListView / ObjectView with the REAL ObjectTimeline and
asserts the refusal on screen, since neither package can see the join.
Part of #7070
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
…n test
`tsc --noEmit` in apps/console reads it as TS6133; the automatic JSX runtime
makes the import unnecessary. Caught by the package's own type-check, not by
vitest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3181.1 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-PRFOV0mr.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.67KB5.75KB
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)516.24KB117.82KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
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)34.34KB9.17KB
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)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.19KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.54KB27.65KB
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)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.93KB21.13KB
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.74KB2.25KB
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)8.11KB3.32KB
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-project-managerClaude

Copy link
Copy Markdown
Collaborator

✅ Landing — 32/32 green, Clause-② no verified from the diff, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC.

Checks, read by name

29 success, 3 skipped by design (dependabot, Test (coverage), Test (coverage shard …)) on head 64d8fbe2a. The long poles: Test (shard 1/4) 11:55:07 · 2/4 11:54:07 · 3/4 11:53:02 · 4/4 11:53:32 · Type Check 11:47:40 · Lint 11:45:03. Also read individually: Bundle Analysis, README Export Check, Governed Surface Queue Guard, Docs Route Eager Closure Check, Control Byte Scan, Inert vi.mock Specifier Check.

Landing authority

Clause-②: no, re-derived by this seat from the diff rather than taken from the declaration — the gate's own rule is 「diff 是事实,卡片语义是预测」. Seven files: two renderer faces, one comment-only app-shell note, three tests, one changeset. Nothing under packages/spec/**, no *.zod.ts, nothing under packages/types/src. ⇒ neither limb fires; this seat lands it without a contract review.

What I verified by content, not from the report

probeplugin-list/ListView.tsxplugin-view/ObjectView.tsx
startDateField: … || 'created_at'1 → 01 → 0
CONTROL titleField: … || 'name' (kept on purpose)2 → 23 → 3

The control is non-zero and unmoved, so the probe discriminates and the change is surgical.

⚠️A probe of mine that was wrong, recorded because it would mislead the next reader too: a raw word-count of created_at in plugin-view/ObjectView.tsx reads 1 → 1 and looks like the floor survived. It did not — the residual occurrence is inside the new comment documenting the retirement. Only the declaration form is an honest probe here. Same class as the tombstone inflation this lane hit earlier today on a different card: count the form, never the word.

The packages/app-shell edit was checked to be comment-only by filtering the diff for non-comment added/removed lines and getting an empty set — not by trusting the claim. It realigns the #3129 note, which had been certifying the two sibling faces as still-fabricating.

Why this step matters more than its diff size suggests

#7459 installed ObjectTimeline's refusal and then measured that nothing began refusing. This is why: ObjectTimeline reads the flat schema.startDateField at the tail of its resolver chain, so a floor at either face answered "the axis is bound" for every view and the refusal was unreachable from both routes. The invented axis was also never fetched$select is collected from the declared timeline blocks, never from this prop — so an undeclared view got a timeline bound to a column the query had not requested and bucketed every record into "No date". A screen that looks built, is wrong, and tells the author nothing.

⭐ The ablation's DOM dump under restored floors is the evidence that matters: data-testid="timeline-canvas" rendering Spring Launch — a healthy, convincing, entirely wrong timeline, not a crash. That is what the pin has to be able to fail on.

Two things the implementer did that I want on the record

  1. The first ablation anchor matched two sites in ListView.tsx (the same conditional-spread text also builds resolvedTimeline's nested config, untouched here). The assertion refused and aborted rather than mutating the wrong line. That is the protocol catching precisely what it exists to catch; both anchors are now two-line.
  2. InterfaceListPage:413-414 was reported as a boundary and deliberately NOT filed as a finding. It derives the binding from defaultCalendarFromObject(objectDef) — a field the object declares, or undefined. Derivation, not fabrication, so it is consistent with 日期轴永不虚构 rather than a surviving instance of it. Naming it is what shows it was measured rather than missed, and it is the one remaining route by which a view that declared no axis still gets one.

Card state

Part of #7070 is deliberately not a closing keyword — #7070 stays open on flavour 3, now filed as #7499 (progress / dependencies, re-measured as surviving at two faces, not three; the app-shell face carries neither key). #7070 was also moved pm:queuepm:dispatched and assigned, closing a half-state the implementer correctly reported back instead of writing the assignee field itself.


Generated by Claude Code

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.

2 participants

@os-project-manager@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(plugin-list,plugin-view): retire the 'created_at' timeline date-axis floors so the refusal becomes reachable - #7500

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-7070-step3-drop-created-at-floors
Sep 3, 2026
Merged

fix(plugin-list,plugin-view): retire the 'created_at' timeline date-axis floors so the refusal becomes reachable#7500
os-project-manager merged 2 commits into
mainfrom
claude/issue-7070-step3-drop-created-at-floors

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Part of #7070

Step ③ of the maintainer ruling of 2026-09-01 (总监批 #28, comment 5494805467): house posture 日期轴永不虚构 — a date axis is never fabricated. ListView and plugin-view's ObjectView stop flooring the timeline axis at 'created_at'.

Steps ①② are landed (20cb8db9b, PR #7467, card #7459) and this branch is cut from exactly that commit. ⛔ Nothing in ObjectTimeline, the refusal screen or the locale key is touched here.

The two lines

FaceWasNow
packages/plugin-list/src/ListView.tsx:2496startDateField: dateBinding.startDateField || 'created_at'conditional spread — the key is absent when no axis resolves
packages/plugin-view/src/ObjectView.tsx:1354startDateField: ...startDateField || ...dateField || 'created_at'alias resolved into a local, then the same conditional spread

Line numbers re-derived on main at 20cb8db9b, not inherited from the card. created_at occurs 4 times in ListView.tsx and 1 in ObjectView.tsx; those are occurrences of the string, not floors. Read per line, exactly one floor per file:

  • ListView.tsx:1753s.add('created_at') in knownObjectFields. A platform-column reference, kept.
  • ListView.tsx:1785 — prose explaining the PLATFORM_RECORD_COLUMNS carve-out, kept.
  • ListView.tsx:2496the floor. Deleted.
  • ObjectView.tsx:1354the floor. Deleted.

titleField keeps its 'name' floor at both faces: not a date axis, and the same display-name rung the gallery / kanban / gantt branches carry. Pinned as scope in both new suites.

Why it was never a harmless default

ObjectTimeline reads the flatschema.startDateField at the tail of its resolver chain — plugin-timeline's own CONTROL block proves that rung is fully honoured — so a floor here answered "the axis is bound" for every view and the refusal step ① installed was unreachable from either route. That is exactly why #7459's own measurement recorded that nothing began refusing when ①② landed.

And the invented axis was never fetched: the $select projection is collected from the DECLARED timeline / options.timeline blocks (collectViewFields), never from this prop. An undeclared view therefore got a timeline bound to a column the query had not requested and bucketed every record into "No date" — the screen that looks built, is wrong, and gives the author no signal.

The ruling also explicitly replaces the written decision that stood on the deleted ListView line ("created_at stays the last resort for a view that declares no date axis anywhere"), and asks the objectui#3129 note at the app-shell face to be realigned once ③ lands. Both are done here; the app-shell edit is comment-only.

⭐ Blast radius — measured, not asserted

Which authored surfaces in this repo reach these floors: zero. Which begin refusing: zero.

Every sweep below was run with a control that FIRED in the same run, because a zero from a grep is not a reading.

  • Authored metadata (json/yml/yaml) declaring a timeline view: 4 hits, none of them reach either face. The three examples/schema-catalog/src/schemas/plugin-timeline/*.json documents and packages/types/examples/data-display-examples.json are direct type: 'timeline' schemas carrying authored items / events — they never traverse ListView or generateViewSchema, and hasAuthoredItems short-circuits the refusal predicate (!hasAuthoredItems && !startDateField) regardless. Control: the identical regex for "calendar" fires on components-complex-view-switcher/view-switcher.json:24.
  • Authored TS/TSX samples declaring a timeline view: zero outside tests. Control: the identical regex for 'calendar' fires on six sites.
  • appearance.allowedVisualizations naming timeline in authored metadata: zero.

Two product routes were then measured rather than reasoned about, and both are why the radius is small:

  1. The console's own authoring UI cannot produce an undeclared timeline view.CreateViewDialog's REQUIRED_FIELDS table lists timeline.startDateField among "the sub-config keys a view type must have set before a record can be persisted", with a smart default auto-picked from the object's real date fields (PRIMARY_DATE_PREFERRED). Anything created in-product carries a declared axis.
  2. Interface list pages auto-derive one.InterfaceListPage:413-414 fills timeline from defaultCalendarFromObject(objectDef) when timeline is whitelisted — a field the object declares, or undefined. So such a page refuses only when the object carries no date field at all, which is the correct answer: there is no axis to declare. This is derivation from schema, not fabrication, and the fix(timeline): honor startDateField when rendering a ListView timeline #3129 note already cites it as the good shape.

What is left is metadata authored outside those paths — hand-written or migrated views. Those begin refusing, which is what the ruling ordered, and the refusal names the keys to declare so an affected view reports its own fix. The changeset carries the migration note.

⛔ No replacement fallback was added anywhere.

Verification

Red-then-green, taken naturally first. Both face-level suites were written and run against the UNMODIFIED tree at 20cb8db9b before the first source edit: plugin-view 3 failed / 5 passed, plugin-list 3 failed / 14 passed — the failures exactly the "invents no axis" cases, every render proof and CONTROL green in the same run. After the edit: 25/25.

Ablation, for the console composition test, which was written after the fix and so had never been observed red. Both floors restored, proven on disk in both directions before the run (removed-text 0 AND injected-text 1 per file, plus a blob-hash change on each), trap ... EXIT INT TERM with absolute paths, restore proven by state (git diff HEAD empty + blob equality against the HEAD blob), never by an exit code. No build/dist is involved — vitest resolves @object-ui/* to sibling src/ through the root config's alias table — so there is no stale-artifact leg to prove.

Under the restored floors: 9 failed / 24 passed. The 9 are precisely the "invents no axis" and "refuses on screen" cases at all three levels; every render proof and every CONTROL stayed green in the mutated world. The DOM dump from that run is the point of the exercise — it shows data-testid="timeline-canvas" and the text Spring Launch: a healthy, convincing, entirely wrong timeline, not an error. That is what the pin has to be able to fail on.

⚠️ One anchor note, recorded because it is the failure this protocol exists to catch: the first ablation anchor matched 2 sites in ListView.tsx — the identical conditional-spread text also builds resolvedTimeline's nested config a few lines up, which this card does not touch. The assertion refused and the run aborted rather than mutating the wrong line; both anchors are now two-line, pinned by the titleField: rung that follows the floor.

Refusal asserted positively. Every block opens with a render-proof row, and each absence marker is asserted PRESENT by a control in the same run — a test that only says "no timeline" is satisfied by a component that threw.

Test files

  • packages/plugin-list/src/__tests__/ListView.timeline-binding.test.tsx — the fix(timeline): honor startDateField when rendering a ListView timeline #3129 case that pinned the floor is rewritten, not deleted (it is where the retirement has to become visible); a new step-③ block adds the render proof, the two absence cases and the scope controls; a capability-gate block records, in both directions, that the ADR-0047 toggle is unchanged by this step — unlike the gantt face, the Timeline gate always read resolveTimelineDateBinding and never the flat floor.
  • packages/plugin-view/src/__tests__/ObjectView.timelineBinding-7070.test.tsx — new; the sibling of ObjectView.ganttBinding-7070 / calendarBinding-7029.
  • apps/console/src/__tests__/timelineAxisRefusalReach-7070.test.tsx — new, and the reason it lives in the console: it is the only package depending on plugin-list, plugin-viewandplugin-timeline, so it is the only place the join can be measured. Real faces, real renderer, no stub. Its rows deliberately carry a real created_at column so a restored floor renders a convincing chart rather than an empty one.

Runs — union re-run after the final commit, at 64d8fbe2a:

  • pnpm exec vitest run packages/plugin-list packages/plugin-view packages/plugin-timeline apps/console207 files / 2357 tests passed.
  • app-shell, narrowed and declared: the 26 files that mention timeline or are views/ObjectView* — 290 passed. The app-shell edit is comment-only; the full 608-file suite is CI's.
  • Downstream sweep (plugin-timeline, plugin-gantt refusal pin, react smoke, core normalize-list-view, types timeline pins, schema-catalog render) — 29 files / 991 passed.
  • type-check for plugin-list, plugin-view, app-shell, console — green. Each package chains tsc -p tsconfig.test.json, and both new package-level test files were confirmed present in that program with --listFiles (1 hit each) rather than assumed. It caught a real defect: an unused React import in the console test (TS6133), fixed in 64d8fbe2a.
  • Gates: check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:phantom-deps, check:self-import, check:published-tsconfig-exclude, check:side-effects-array, check-changeset-fixed, check-changeset-no-major, check-type-check-coverage, check-lint-coverage — all report their own OK line. These scan tracked files, so they were re-run after committing; the counts moved with the new files (control-bytes 6157 to 6160 tracked text files; vi-mock 4224 to 4226 source files and 122 to 123 judged call sites), which is what makes the greens readings rather than scans that never saw the diff.

Lint — narrowed, and the narrowing is measured.eslint --no-inline-config over the 6 changed .ts/.tsx files: 0 errors, 424 warnings, all pre-existing repo-wide classes (no-explicit-any, react-refresh/only-export-components); the sibling ObjectView.ganttBinding-7070.test.tsx produces 4 warnings of the same class, so the new files add no new one. Three pieces of evidence that this excludes nothing: the receiving population came from eslint's own config (all 6 files were linted, none silently ignored); the count of 6 is read from --format json, not asserted; and eslint.config.js sets no parserOptions.project / projectService, so linting here is not type-aware and a change in one file cannot move the verdict on a file it does not touch. --max-warnings is deliberately unset in this repo, per lint.yml.

Clause ②, re-declared from this diff

no. The seven files this diff touches are two renderer faces, one comment-only app-shell note, three test files and one changeset. Nothing under packages/types/, no Zod schema, no spec surface, no authorable key — the view layer stops fabricating; the published accept surface is untouched. Control for that zero: the same path filter matches packages/types/src/zod/data-display.zod.ts when fed it.

Out of scope, filed

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

…xis floors
objectui#7070 step ③ of the maintainer ruling of 2026-09-01 (总监批 #28): house
posture 日期轴永不虚构 — a date axis is never fabricated.
`ListView` and `plugin-view`'s `ObjectView` both floored `startDateField` at
'created_at' for a timeline view that declared no axis. `ObjectTimeline` reads
that flat prop at the tail of its resolver chain, so the faces answered "the
axis is bound" for every view and the refusal screen step ① installed
(objectui#7459, landed as 20cb8db) was unreachable from either route — which
is why ①② changed nothing a user could see. Both branches now forward a
declared axis or no key at all.
The floor was never a harmless default: the $select projection is collected
from the DECLARED timeline / options.timeline blocks and never from this prop,
so an undeclared view rendered a timeline bound to a column the query had not
requested and bucketed every record into "No date".
The ruling also explicitly replaces the written decision that stood on the
deleted ListView line, and asks the objectui#3129 note at the app-shell face to
be realigned to the new reality once ③ lands; both are done here.
`titleField` keeps its 'name' floor at both faces — it is not a date axis.
Tests: the two face-level pins measure the prop; a new console-level test
composes the real ListView / ObjectView with the REAL ObjectTimeline and
asserts the refusal on screen, since neither package can see the join.
Part of #7070
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
…n test
`tsc --noEmit` in apps/console reads it as TS6133; the automatic JSX runtime
makes the import unnecessary. Caught by the package's own type-check, not by
vitest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3181.1 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-PRFOV0mr.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.67KB5.75KB
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)516.24KB117.82KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
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)34.34KB9.17KB
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)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.19KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.54KB27.65KB
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)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.93KB21.13KB
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.74KB2.25KB
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)8.11KB3.32KB
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-project-managerClaude

Copy link
Copy Markdown
Collaborator

✅ Landing — 32/32 green, Clause-② no verified from the diff, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC.

Checks, read by name

29 success, 3 skipped by design (dependabot, Test (coverage), Test (coverage shard …)) on head 64d8fbe2a. The long poles: Test (shard 1/4) 11:55:07 · 2/4 11:54:07 · 3/4 11:53:02 · 4/4 11:53:32 · Type Check 11:47:40 · Lint 11:45:03. Also read individually: Bundle Analysis, README Export Check, Governed Surface Queue Guard, Docs Route Eager Closure Check, Control Byte Scan, Inert vi.mock Specifier Check.

Landing authority

Clause-②: no, re-derived by this seat from the diff rather than taken from the declaration — the gate's own rule is 「diff 是事实,卡片语义是预测」. Seven files: two renderer faces, one comment-only app-shell note, three tests, one changeset. Nothing under packages/spec/**, no *.zod.ts, nothing under packages/types/src. ⇒ neither limb fires; this seat lands it without a contract review.

What I verified by content, not from the report

probeplugin-list/ListView.tsxplugin-view/ObjectView.tsx
startDateField: … || 'created_at'1 → 01 → 0
CONTROL titleField: … || 'name' (kept on purpose)2 → 23 → 3

The control is non-zero and unmoved, so the probe discriminates and the change is surgical.

⚠️A probe of mine that was wrong, recorded because it would mislead the next reader too: a raw word-count of created_at in plugin-view/ObjectView.tsx reads 1 → 1 and looks like the floor survived. It did not — the residual occurrence is inside the new comment documenting the retirement. Only the declaration form is an honest probe here. Same class as the tombstone inflation this lane hit earlier today on a different card: count the form, never the word.

The packages/app-shell edit was checked to be comment-only by filtering the diff for non-comment added/removed lines and getting an empty set — not by trusting the claim. It realigns the #3129 note, which had been certifying the two sibling faces as still-fabricating.

Why this step matters more than its diff size suggests

#7459 installed ObjectTimeline's refusal and then measured that nothing began refusing. This is why: ObjectTimeline reads the flat schema.startDateField at the tail of its resolver chain, so a floor at either face answered "the axis is bound" for every view and the refusal was unreachable from both routes. The invented axis was also never fetched$select is collected from the declared timeline blocks, never from this prop — so an undeclared view got a timeline bound to a column the query had not requested and bucketed every record into "No date". A screen that looks built, is wrong, and tells the author nothing.

⭐ The ablation's DOM dump under restored floors is the evidence that matters: data-testid="timeline-canvas" rendering Spring Launch — a healthy, convincing, entirely wrong timeline, not a crash. That is what the pin has to be able to fail on.

Two things the implementer did that I want on the record

  1. The first ablation anchor matched two sites in ListView.tsx (the same conditional-spread text also builds resolvedTimeline's nested config, untouched here). The assertion refused and aborted rather than mutating the wrong line. That is the protocol catching precisely what it exists to catch; both anchors are now two-line.
  2. InterfaceListPage:413-414 was reported as a boundary and deliberately NOT filed as a finding. It derives the binding from defaultCalendarFromObject(objectDef) — a field the object declares, or undefined. Derivation, not fabrication, so it is consistent with 日期轴永不虚构 rather than a surviving instance of it. Naming it is what shows it was measured rather than missed, and it is the one remaining route by which a view that declared no axis still gets one.

Card state

Part of #7070 is deliberately not a closing keyword — #7070 stays open on flavour 3, now filed as #7499 (progress / dependencies, re-measured as surviving at two faces, not three; the app-shell face carries neither key). #7070 was also moved pm:queuepm:dispatched and assigned, closing a half-state the implementer correctly reported back instead of writing the assignee field itself.


Generated by Claude Code

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.

2 participants

@os-project-manager@claude