Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-timeline,i18n): refuse an undeclared timeline date axis instead of inventing one - #7467
Conversation
…stead of inventing one Steps (1) and (2) of the three-step sequence ruled on objectui#7070 (maintainer, 2026-09-01). `ObjectTimeline` resolved its date axis through five declared bindings and then closed the chain with a bare literal field name nobody has ever declared, so a name always resolved: every record read a key its object does not carry and bucketed into "No date". The floor is retired and an absent-axis refusal added, together — a refusal added while the floor stood would have been unreachable, and the floor retired without a refusal produces the silent "No date" outcome the ruling rejects. The refusal names every binding it accepts, in the shape objectui#7070 settled one renderer over. The component's success surface is now named (`timeline-canvas`) so a refusal can be distinguished from a rendered-but-empty timeline, which is the distinction the ruling turns on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
❌ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. Which half objected:
📦 Bundle Size Report
Size Limits
|
os-project-manager
commented
Sep 3, 2026
ACCEPT — verified on the branch by content, ⛔ not against the report
⭐ My brief was wrong, and the dev measured it rather than following itMy dispatch — and the card — both say "four honest rungs, then a fabricated fifth." The chain actually reads five declared bindings and a fabricated sixth: Off by one, and it was mine. The dev re-derived the chain instead of trusting the count, then checked the other five and reported that all are genuine declared bindings — no further fabrication — without extending scope to "fix" anything. That is exactly the shape the brief asked for and the opposite of what an off-by-one usually produces. The readings
⭐ The tombstone here is deliberately spelled so it does NOT quote the removed literal. The dev says why: a comment quoting it would make the close-condition scan answer 1 where the truth is 0. That is precisely the trap that has fired on me three times today, anticipated and designed out rather than documented after the fact. ⛔ The step-③ fence held, checked rather than assumed
Two judgement calls I want on the record1. The named success surface is an enabling change, declared not smuggled. 2. The The ablation tests the pairing, which is the right subjectMutation = put the floor back while the refusal stays. That is not "undo the change" — it is the specific claim this card rests on, that a refusal behind a floor is unreachable. Predicted 4 RED / 7 GREEN by construction, and the seven were named in advance as fully-declared cases unaffected in both worlds. Observed 4 failed / 7 passed, the four failures exactly the four predicted, by name. That asymmetry is the acceptance criterion for this class — it separates "stopped inventing" from "refuses everything" — and the blast-radius measurement closes it: nothing in the product starts refusing, because both plugin faces still supply LandingClause ② NO, re-declared against the diff rather than inherited from my brief: no spec key, no zod validator, no exported type. Two gates are declared NOT MEASURED rather than green — CI is still running several jobs. ⛔ Not enqueueing until every check is green — the bar is every check, not the required subset. Generated by Claude Code |
os-project-manager
commented
Sep 3, 2026
⛔ Standing down on |
os-project-manager
commented
Sep 3, 2026
Base branch brought in — ⛔ not a re-run, and the result answers #7399
Why this is a content change, not a re-runThis PR's base was
⇒ roughly 0.9 KB came back out of the chunk between those two commits. Re-running on the old base would have reproduced 512.8 KB exactly — the build is deterministic. Building against the new base is a different measurement, and it is the one that matters now. That is also what the standing rule prescribes when a base recovers: bring the base branch in and let CI re-run against it, rather than waiting. ⭐ Why this particular PR's verdict is worth more than its own greennessThis diff touches only ⇒ On the new base, this PR's
State of this PR otherwiseEvery other check was green before this update — 30 of 31, with ⛔ Still no ceiling raise. ⛔ Still no re-run of the old build. Generated by Claude Code |
❌ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. Which half objected:
📦 Bundle Size Report
Size Limits
|
os-project-manager
commented
Sep 3, 2026
⛔ RETRACTION — the |
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#7459
Steps ① and ② of the three-step sequence ruled on #7070 (project director seat, 总监批 #28, 2026-09-01, maintainer verbatim 「同意」). Step ③ — the
'created_at'floors on the two plugin faces — stays on #7070 and is not touched here.The gate: both premises re-measured on the current tree
The card's figures were taken on
2c3cd1b75. Everything was re-derived on3804c222bbefore the first edit.Premise 1 — no absent-date-axis refusal path. REPRODUCES. Zero hits for such a refusal in
packages/plugin-timeline. The control fires in the same run and in the same file: the termrefusmatched 6 lines ofObjectTimeline.tsx(the card said 5 — the file has moved, and the control is live rather than stale). Every one of those 6 is the #6655variant: 'gantt'refusal or its prose; none is about a date axis.Premise 2 — the internal floor. REPRODUCES, and the line numbers happen to still hold.
packages/plugin-timeline/src/ObjectTimeline.tsx:247-249, spelled exactly as reported.Correction to the card's arithmetic. The card describes "four honest rungs, then a fabricated fifth". The chain reads five declared bindings and then a fabricated sixth:
timeline.startDateField,timeline.dateField,mapping.date, flatstartDateField, flatdateField. All five were re-checked as asked, and all five are genuine declared bindings — the first two onListViewTimelineConfig(packages/types/src/objectql.ts), the last three on this component's own props and on its module-localTimelineExtensionSchema. No further fabrication among them.What changed
① The refusal. An object-bound timeline that declares no date axis renders a diagnostic naming every binding it accepts, instead of a chart. Follows the settled in-repo shape (
ObjectGantt's "Gantt configuration required" screen) rather than a second dialect: early return,role="alert", names the fields the author must supply. The list is interpolated from the component's ownOBJECT_BOUND_TIMELINE_DATE_BINDINGS— the same drift-proof deviceOBJECT_BOUND_TIMELINE_VARIANTSalready uses one refusal up — so the diagnostic cannot name a vocabulary the resolver no longer has.② The floor is retired, which is the only thing that makes ① reachable. The tombstone comment deliberately does not quote the removed literal: the card's close condition greps this file for it, and a comment quoting it would answer 1 where the truth is 0. Verified after the final commit: 0 hits for the floor, control term firing at 25 lines in the same file.
Enabling change, declared rather than smuggled: the component's success surface is now named
data-testid="timeline-canvas". Every other terminal state of this component already named itself (timeline-unsupported-variant,timeline-missing-date-axis,timeline-error,timeline-loading); the rendered timeline was the one outcome a test could not ask for by name, which is exactly the distinction the ruling turns on.i18n. One new key,
timeline.unconfigured.noDateAxis, in all ten locale packs plus the plugin's own default table (byte-identical toen). The German value carries no typographic quote pair on purpose —de-quote-pairing-3876.test.tsratchets that count, and it stays at 59.Blast radius, measured
Nothing in the product starts refusing. Both plugin faces still supply
startDateField: … || 'created_at'(plugin-list/src/ListView.tsx:2496,plugin-view/src/ObjectView.tsx:1354— both re-derived; the card's line numbers were stale). App-shell'stimelineViewOptionsemits no axis, but routes throughListView, which floors. The refusal is reached only by a view forced directly onto the timeline renderer with nothing declared — which is what the card predicted, and what makes ③ the visible step.The pin
packages/plugin-timeline/src/ObjectTimeline.absentDateAxisRefusal-7459.test.tsx, 11 cases, the twin ofObjectGantt.unconfiguredRefusal-7070.test.tsx.timeline-canvasandrole="list"(the ordered-list rail the real renderer emits, theolelement, which an empty timeline still emits). Neither zero is vacuous: the controls in the same file assert both PRESENT.date. Restore the floor and it goes red while rendering a completely convincing timeline off a real column — which "no events" would never have caught. Declared-ness, not existence, is what an axis is.itemslist is never refused (it carries its owntime; no field name is read), which is what keeps the three in-repo catalog fixtures rendering.variant: "gantt"throws —calculateDateRangereads a row shapeObjectTimelinenever composes #6655 variant refusal still wins for a composed gantt, pinned so it is a decision rather than an accident of placement.timeline-object-bound-gantt-refusal.test.tsxstates one file over.Verification
All figures from runs at the final commit
d2dbdfb8c; heavy runs serialized through the shared verify lock, verdict lines read rather than a bare$?.plugin-timeline+i18n+plugin-list+plugin-view:Test Files 175 passed (175) / Tests 2288 passed (2288), VERDICT command-exit 0.plugin-timeline= 21 files / 276 tests. After: 22 / 287.plugin-timeline+i18n→ exit 0,Scope: 2 of 47 workspace projects, each echoingtsc --noEmit && tsc -p tsconfig.test.json+Done(so the zero-match filter trap did not fire). The new test file is genuinely covered, not excluded:tsc -p tsconfig.test.json --listFilescounts it 1.check:i18n-keys"every in-scope call-site key resolves against the en pack (2960 keys)";check:i18n-drift"0 en value(s) changed (1 key(s) added…)";check:i18n-dead-keys(report) 0;check:control-bytes"OK (scanned 6140 tracked text file(s))";check:vi-mock-specifiersOK;check:vi-mock-inherit"122 call site(s) on @object-ui/react judged (122 inherit, 0 auto-mocked)";check:side-effects-arrayOK;check-changeset-presence"13 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)";check-changeset-no-major"No changeset declares amajorbump";check-changeset-fixedOK;check-changeset-overwriteOK.check:readme-exportsandcheck:eager-closureboth stop on a prerequisite — all 314 findings of the former are "type entry./dist/index.d.tsis not on disk — runpnpm buildfirst", and the latter reports "No eager-closure report … the console was not built". Both need a full-monorepo / console build, which CI does regardless. Recorded as unmeasured rather than as failures.--no-inline-config): exit 0, 0 errors, 29 warnings, all pre-existing in class. Declared narrowing: run on the 14 changed files rather than the repo-wide farm. Evidence it excludes nothing — file count 14 read from--format json; this config enables no type-aware linting (noprojectService/parserOptions.projectineslint.config.js), so the diff cannot move an untouched file's verdict; CI runs the full farm regardless. The tworeact-hookswarnings onObjectTimeline.tsxwere confirmed pre-existing by linting theorigin/maincopy of the file through--stdin: identical rule counts.Reverse verification (ablation) — direction predicted, then observed
Implementation committed first, so the restore leg had a real reference. Mutation: put the retired floor back while the refusal stays — the pairing itself.
variant: "gantt"throws —calculateDateRangereads a row shapeObjectTimelinenever composes #6655 ordering case — all fully declared, so unaffected in both worlds).Tests 4 failed | 7 passed (11), and the four failures are exactly the four predicted, by name.1b4575de…→8cea816b…. The anchor was asserted unique first, so a zero-match no-op would have aborted rather than reported a healthy run.git hash-object==git rev-parse HEAD:PATH(non-empty and matching),git diff HEADempty,git statusclean. Absolute-pathtrap … EXIT INT TERM.dist/leg exists for this ablation and none is claimed: the pin imports the subject by relative path from source (./ObjectTimeline), so no rebuild sits between the mutation and the measurement.Clause ②
No, and re-declared against the actual diff rather than inherited from the card. Nothing published moves: no spec key, no zod validator, no exported type.
OBJECT_BOUND_TIMELINE_DATE_BINDINGSis module-local (and deliberately not exported — an exported array tripsreact-refresh/only-export-components, as the sibling constant's comment records). The only new published surface is adata-testidattribute and one i18n key.startDateField's internal type widens tostring | undefinedinside the component; it is a local, not a prop.Changeset:
.changeset/7459-timeline-absent-date-axis-refusal.md, minor for@object-ui/plugin-timelineand@object-ui/i18n— matching the grade #7070 used for the identical class of change. Graded here and confirmed by the gate's own verdict line:check-changeset-no-major→ "✅ No changeset declares amajorbump".Not in this card
'created_at'atplugin-list/ListView.tsxandplugin-view/ObjectView.tsx. Untouched; finding(views): the gantt and timeline branches still fabricate date-axis field names on all three faces — and app-shell's own #3129 note certifies them as already fixed #7070 remains open and unblocked by this landing.progress/dependencies) — not touched, and no date-axis conclusion applied to them.🤖 Generated with Claude Code
https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
Generated by Claude Code