Skip to content

Mark deleted files with a roof cross, and make Timeline honest about them - #141

Merged
thalida merged 8 commits into
mainfrom
feat/issue-138-ruin-x-mark
Jul 25, 2026
Merged

Mark deleted files with a roof cross, and make Timeline honest about them#141
thalida merged 8 commits into
mainfrom
feat/issue-138-ruin-x-mark

Conversation

@thalida

@thalida thalida commented Jul 25, 2026

Copy link
Copy Markdown
Owner

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/smoothstep like the rest of the facade and clipped by the existing innerMask so 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. RUINS was a Live route 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_DIAG converts.

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_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 instead of two copies of the binary search.

formatHoverTooltip ends 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.date was truncated to YYYY-MM-DD on the wire, so every commit sharing a day parsed to the same midnight. On a time axis they collapsed onto one x position, and fractionToIndex divided by a zero span and returned the day's first commit. The other ~770 were unreachable by dragging.

git already emits %aI and scan.py already 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: 0 places each commit purely by when it happened, 1 gives 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 degenerate same-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 from indexToMs(), since track fraction is no longer invertible to a timestamp.

Also

Bloom THRESHOLD default 0.5 to 1.0: at 0.5 matte walls crossed the cutoff and bloomed with the windows. 1.0 is the point the field's own tip already described as windows-only.

Deleted-file defaults retuned: stub height 2 to 0.05, desaturation 0.1 to 0.5, cross thickness 0.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_VERSION must 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=true returned 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: statsAtDeletion including the resurrection case, bytesAt sharing linesAt'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

thalida added 6 commits July 25, 2026 14:09
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.
thalida added 2 commits July 25, 2026 14:51
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.
@thalida
thalida merged commit f2424a3 into main Jul 25, 2026
1 check passed
@thalida
thalida deleted the feat/issue-138-ruin-x-mark branch July 25, 2026 19:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Timeline: mark deleted-file ruins with a red X over the roof icon (with settings)

1 participant