Mark deleted files with a roof cross, and make Timeline honest about them - #141
Merged
Conversation
A ruin read as "faint and crumbled", which is easy to mistake for a stale-but-present file. A cross over the roof says deleted outright. It composites after the icon rather than replacing it, so the file type stays readable underneath, and it is clipped to the same inner square the icon occupies so it never touches the border strip. Two RUINS settings sit beside the existing ruin tuning: X_ENABLED (on) and X_COLOR (red). RUINS is a Live route, so the shared-material effect now subscribes to it and pushes the two uniforms on Save, the same way it handles the other stores. The mark is uniform-driven, not per-instance: the shader is at the WebGL2 16-attribute ceiling. Closes #138
The hardcoded half-width was both too thin and not tunable. X_WIDTH now drives it, expressed as the stroke's fraction of the roof rather than the diagonal half-distance the shader works in, so the number means something to whoever drags the slider. RUIN_X_DIAG converts between the two. Default 0.18, roughly 1.8x the old stroke.
The tooltip prefers the scrubbed count, which for a deleted file is 0 because it is absent at that commit, not because it was empty. Reading "0 lines" as a fact about the file is wrong, so a ruin shows just its path. The static count is no better and is not used as a fallback. Media dimensions stay: they come off the manifest, not the replay, so they still describe the file that was deleted. This reverses a deliberate choice (formatHoverTooltip was kept deletion-agnostic, with the badge left to the renderer). The formatter is now deletion-aware for the metric only; the path is still plain identity text and the marker is still the renderer's job. The footer is unaffected: it reads FileNode.lines, never the replay.
Hovering a ruin read "0 lines" because the replay reports 0 for an absent file. That is a fact about the absence, not the file. The footer was no better: in Timeline it showed the union node's line count and byte size, which are max-over-history values, so the same file could report three different numbers across the tooltip, the footer, and its own height. Everything now reads one source, scrubbedStatsFor(): the replayed values at the scrub position, or the values the file last measured before it was deleted. statsAtDeletion() steps back past the deletion entry's zeroes, and takes the LATEST close so a resurrected path reports the death it is actually sitting in, not its first one. Bytes needed the backend: blob_sizes was already computed for the union manifest but never serialized, so the bundle now ships blobSizes and the replay carries bytes alongside lines. linesAt/bytesAt share one interpolator rather than two copies of the binary search. Timeline cache schema bumped to v6. formatHoverTooltip goes back to being deletion-agnostic: resolving what a deleted file measured belongs in the replay, not in a formatter.
Stub height 2 -> 0.05 (flat rubble rather than a short tower), desaturation 0.1 -> 0.5, cross thickness 0.18 -> 0.3, and the cross to #290000, a deep red that marks the roof without competing with the lit city around it.
Commit dates were truncated to YYYY-MM-DD on the wire, so every commit sharing a day parsed to the same midnight. On a time axis that put them all at one x position: the ticks stacked, and fractionToIndex divided by a zero span and returned the day's FIRST commit, so the rest were unreachable by dragging. A repo with 800 commits over five weeks could only scrub to about thirty of them. git already hands us %aI and scan.py normalizes it; only the [:10] slice threw the time away. The aggregates that genuinely mean "calendar day" (busyness, same_day_total, stats) slice the day back out. Precision alone wasn't enough. On a pure time axis a burst of 100 commits in an hour still lands in ~1px of a five-week track. The axis now blends time with ordinal position, so quiet stretches still spread and bursts still cluster, but every commit keeps a floor share of the track. That also retires the degenerate same-instant branch, since the ordinal term keeps the axis strictly increasing, and collapses the old date-lerp and its inverse into one precomputed array. Handle time now comes from indexToMs: track fraction can no longer be inverted back to a timestamp once the ordinal is mixed in. Git-history cache bumped to v15.
The file header and the INDEX_WEIGHT block were explaining a tradeoff that already lives in the PR body, not something the next reader needs inline.
Commit placement is now a slider rather than a baked constant, because the right answer is repo-shaped: 0 places each commit purely by when it happened, 1 gives every commit equal width. Default 0 — with real timestamps the time axis reads honestly on its own, and the blend was compensating for precision the wire was throwing away. Also guards the timeline cache version contract. A bundle written by older code must miss on load, or a warm cache would keep serving day-precision dates after this ships. It does miss; the test pins it, since the git-history and manifest caches had that guard and the timeline one did not.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #138
In Timeline, a deleted file's ruin now gets a cross over its roof, so deletion reads at a glance instead of being inferred from "faint and crumbled". Three follow-on asks landed on this branch too, all Timeline-adjacent; they're listed separately below.
The cross (#138)
Two diagonals of the inner roof square, anti-aliased with
fwidth/smoothsteplike the rest of the facade and clipped by the existinginnerMaskso it never runs onto the border strip. It composites after the icon, so the file-type glyph stays readable underneath rather than being replaced.It's uniform-driven rather than per-instance: the shader is at the WebGL2 16-attribute ceiling (guarded by a test since #118), and the toggle/color/thickness are global settings anyway.
RUINSwas aLiveroute with no path to the material, so it's now one of the shared-material effect's subscriptions.Three settings under Timeline → Deleted files:
X_ENABLED,X_COLOR,X_WIDTH. Thickness is expressed as the stroke's fraction of the roof, not the diagonal half-distance the shader works in, so the slider reads meaningfully;RUIN_X_DIAGconverts.A deleted file's stats were lying
Hovering a ruin showed
0 lines. That's a fact about the file's absence, not the file. The footer was worse: in Timeline it read the union node, whose line count and byte size are max-over-history values, so the same file could report three different numbers across the tooltip, the footer, and its own rendered height.Everything now reads one source,
scrubbedStatsFor(): the replayed values at the scrub position, or what the file last measured before deletion.statsAtDeletion()steps back past the deletion entry's zeroes and takes the latest close, so a resurrected path reports the death it's actually sitting in.Bytes needed the backend.
blob_sizeswas already computed for the union manifest but never serialized, so the bundle now shipsblobSizesand the replay carries bytes alongside lines.linesAt/bytesAtshare one interpolator instead of two copies of the binary search.formatHoverTooltipends up back where it started, deletion-agnostic: resolving what a deleted file measured belongs in the replay, not in a formatter. An intermediate commit had it special-casing ruins.The scrubber couldn't reach most commits
Reported against a repo with 800 commits over five weeks.
CommitEntry.datewas truncated toYYYY-MM-DDon the wire, so every commit sharing a day parsed to the same midnight. On a time axis they collapsed onto one x position, andfractionToIndexdivided by a zero span and returned the day's first commit. The other ~770 were unreachable by dragging.git already emits
%aIandscan.pyalready normalizes it to UTC; only the[:10]slice discarded the time. The aggregates that genuinely mean "calendar day" (busyness,same_day_total, stats) slice it back out.The axis can also blend time with ordinal position, so a burst of commits keeps a floor share of the track instead of collapsing into ~1px. That's exposed as Timeline -> Scrubber -> "Even spacing" rather than baked in, because the right value is repo-shaped:
0places each commit purely by when it happened,1gives every commit equal width.Default is
0, pure timestamp. With real times on the wire the clock axis reads honestly on its own; the blend was compensating for precision the wire was discarding.The blend also retires the
degeneratesame-instant branch (the ordinal term keeps the axis strictly increasing) and collapses the date-lerp and its inverse into one precomputed array. Handle time now comes fromindexToMs(), since track fraction is no longer invertible to a timestamp.Also
Bloom
THRESHOLDdefault0.5to1.0: at0.5matte walls crossed the cutoff and bloomed with the windows.1.0is the point the field's own tip already described as windows-only.Deleted-file defaults retuned: stub height
2to0.05, desaturation0.1to0.5, cross thickness0.3, cross#290000.Caches
Two bumps, so the first load after merge re-scans: git-history v15 (dates carry a time), timeline v6 (bundle ships
blobSizes).A warm cache serving a pre-bump bundle would hand the scrubber day-precision dates and stack every same-day commit, so the invalidation contract is now pinned by a test: a bundle written under the previous
_TIMELINE_CACHE_VERSIONmust miss on load. The git-history and manifest caches already had that guard; the timeline one didn't. Verified against the real repo, same URL minutes apart: cached load returned 23 distinct commit dates with no time component,no_cache=truereturned 823 distinct with times.Tests
GLSL can't be compile-tested in vitest, so shader coverage targets what drifts silently: the cross is gated on
KIND_RUIN && uRuinXEnabled, and a source-order assertion pins that it composites after the icon (a refactor could invert that invisibly). Elsewhere:statsAtDeletionincluding the resurrection case,bytesAtsharinglinesAt's curve, same-day separation and burst reachability on the scrubber, and the bundle contract that every referenced sha resolves in both blob tables.Full gate green: 359 pytest, ruff, 2830 vitest, eslint, typecheck, prettier. Verified visually in the dev stack.
🤖 Generated with Claude Code