Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d5ab836
Keep the loading overlay up until the world is actually on screen
thalida Aug 22, 2026
a82688a
Say in LOADING.md what now takes the overlay down
thalida Aug 22, 2026
cc376b2
Drop the last ProjectsView reference from the loading column
thalida Aug 22, 2026
9a0454e
Make a city self-contained, so two can be mounted and neither hears t…
thalida Aug 22, 2026
3c83be5
Ask the server again every time you open a project
thalida Aug 22, 2026
47788c5
Every city is a whole city: configure the instance, don't branch on a…
thalida Aug 22, 2026
c028bac
Name the route-load flag after arriving, and say which way a city talks
thalida Aug 22, 2026
a84b405
Make project state per-instance: one session, not one app's worth of …
thalida Aug 22, 2026
24c78a5
Convert the suite to sessions, and prove two projects run at once
thalida Aug 22, 2026
64e0952
Drop two imports the session conversion left behind
thalida Aug 22, 2026
766b2ea
Bring the session files under the comment cap
thalida Aug 22, 2026
7c59ed1
Put the generated types back the way the generator writes them
thalida Aug 22, 2026
fd14dd4
Say that a session is a place, not an identity
thalida Aug 22, 2026
d5925f5
Give the domain word back: a city is the thing, a scene draws it
thalida Aug 22, 2026
b1bc2b4
Keep createCity's name, and the docs a wider rename would have cost
thalida Aug 22, 2026
d2e07ac
Finish the rename, and put the long comments where long form belongs
thalida Aug 22, 2026
2c5b9ce
Weather a building against the moment you are looking at, not its rank
thalida Aug 22, 2026
3c1ec92
Stop computing a per-commit date range nothing reads
thalida Aug 22, 2026
24cdbf8
Age a tree against the day the repo was read, and drop the span behin…
thalida Aug 22, 2026
0b92c6e
Make a city one class you can hold, and give the landing its own
thalida Aug 22, 2026
7830f9e
A scene takes the session, not a bag assembled from it
thalida Aug 22, 2026
01810a3
A city's look is its own, and so is what triggers its rebuild
thalida Aug 22, 2026
7eeda72
Grow the forest from the city's own settings
thalida Aug 22, 2026
efcebc6
Light the orbs from the city they orbit
thalida Aug 22, 2026
bef451e
Paint the island, sky, streets, gem and labels from the city they are in
thalida Aug 22, 2026
67e6151
Clear the lint the sweep left behind
thalida Aug 22, 2026
2a15622
One building material per city, not one for every city
thalida Aug 23, 2026
c030eac
Read the scene state, post-processing and scrub frame from the city too
thalida Aug 23, 2026
7cb573e
Lay the city out from its own config, and pose it without moving the app
thalida Aug 23, 2026
05ad9d8
Pin the other kind of sharing: one material per city
thalida Aug 23, 2026
0d32c16
A city asks its host for chrome instead of reaching for it
thalida Aug 23, 2026
d2cdf94
Let the URL adapter follow the URL, and stop doing it twice
thalida Aug 23, 2026
af4f212
Make city/ a module: one folder, two halves, and a real boundary
thalida Aug 23, 2026
aea1a95
Trim the comment blocks this branch actually wrote
thalida Aug 23, 2026
dd06a86
Stop the gate counting a file move as a comment change
thalida Aug 23, 2026
85acdf4
Bring the rest of the city in: its backend calls, picker types, const…
thalida Aug 23, 2026
30465ec
Split the types: a city's model is the city's
thalida Aug 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
20 changes: 0 additions & 20 deletions api/models/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,17 +194,6 @@ class CommitLeader(BaseModel):
date: str


# Both fields required-nullable: the scanner always emits them (null for a repo
# with no commits), so they're present-but-nullable on the wire, not optional.
class CommitDateRange(BaseModel):
oldest: Optional[str] = Field(
description="Oldest commit date (YYYY-MM-DD), or null when the repo has no commits"
)
newest: Optional[str] = Field(
description="Newest commit date (YYYY-MM-DD), or null when the repo has no commits"
)


class DayLeader(BaseModel):
date: str
count: int
Expand Down Expand Up @@ -252,7 +241,6 @@ class RepoStats(BaseModel):
commitCount: int = Field(
description="Commits in the full history, however many `commits` carries"
)
commitDates: CommitDateRange
maxCommitsPerDay: Optional[DayLeader]
maxCommitStreakDays: int
authors: list[AuthorStat]
Expand Down Expand Up @@ -306,13 +294,6 @@ class SignatureResponse(BaseModel):
content_signature: str


class DateRangeMs(BaseModel):
minCreated: int
maxCreated: int
minModified: int
maxModified: int


class TimelineChange(BaseModel):
path: str
sha: Optional[str] = Field(description="New blob sha, or null when deleted")
Expand Down Expand Up @@ -340,7 +321,6 @@ class TimelineBundle(BaseModel):
blobLines: dict[str, Optional[int]]
blobSizes: dict[str, Optional[int]]
commitLineRanges: list[RangeStat]
commitDateRanges: list[DateRangeMs]
notes: list[str]


Expand Down
7 changes: 0 additions & 7 deletions api/scan/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from api.models.manifest import (
AuthorStat,
BusynessThresholds,
CommitDateRange,
CommitEntry,
CommitLeader,
DayLeader,
Expand Down Expand Up @@ -242,7 +241,6 @@ def compute_repo_stats(tree: DirNode, commits: list[CommitEntry]) -> RepoStats:
newest_dir = d

grandest = sparsest = None # commits, by files changed
oldest_commit = newest_commit = None # commit-date range
# The walk yields oldest first, so the ends ARE the leaders. Comparing dates
# instead would tie on a busy day and keep the first, which is not newest.
first = commits[0] if commits else None
Expand All @@ -259,10 +257,6 @@ def compute_repo_stats(tree: DirNode, commits: list[CommitEntry]) -> RepoStats:
# multi-counting. 0 until manifest-wrap bakes it; read that as 1.
d = day_of(c.date)
day_totals[d] = max(day_totals.get(d, 0), c.same_day_total or 1)
if oldest_commit is None or d < oldest_commit:
oldest_commit = d
if newest_commit is None or d > newest_commit:
newest_commit = d

busiest_day: DayLeader | None = None
if day_totals:
Expand Down Expand Up @@ -320,7 +314,6 @@ def _commit_leader(c: Optional[CommitEntry]) -> CommitLeader | None:
newestCommit=_commit_leader(last),
commitCount=len(commits),
# YYYY-MM-DD sorts lexically in chronological order, so min/max are exact.
commitDates=CommitDateRange(oldest=oldest_commit, newest=newest_commit),
maxCommitsPerDay=busiest_day,
maxCommitStreakDays=_longest_streak(list(day_totals.keys())),
authors=authors,
Expand Down
92 changes: 2 additions & 90 deletions api/scan/timeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
from api.core.constants import TimelineStage
from api.models.manifest import (
CommitEntry,
DateRangeMs,
FileNode,
Manifest,
RangeStat,
Expand All @@ -31,7 +30,6 @@
from api.scan.blobmeta import MISSING_BLOB, blob_file_node, resolve_blob_stats
from api.scan.manifest import wrap_manifest
from api.core.progress import Throttle, log
from api.utils.dates import iso_to_ms
from api.core.exceptions import NotAGitRepoError
from api.scan.signatures import derive_tree_signals, new_signature
from api.scan.skiprules import SkipRules
Expand All @@ -45,9 +43,9 @@

_HISTORY_HEARTBEAT_EVERY = 2000 # commits between progress ticks

# Four steps, a quarter of the percent each. The last is the router's:
# Three steps, a third of the percent each. The last is the router's:
# serialising the bundle is the step nothing else can report.
ASSEMBLE_STEPS = 4
ASSEMBLE_STEPS = 3
# Commits between ticks inside a step that reports its own progress.
_ASSEMBLE_HEARTBEAT_EVERY = 200

Expand Down Expand Up @@ -290,81 +288,6 @@ def compute_commit_line_ranges(
return ranges


def compute_commit_date_ranges(
deltas: list[CommitDelta],
commits: list[CommitEntry],
git_created: dict[str, str],
git_modified: dict[str, str],
*,
on_commit: Callable[[int, int], None] | None = None,
) -> list[DateRangeMs]:
"""Per-commit created/modified ms ranges over the files present at each
commit; range[HEAD] equals the live manifest's dateRanges (weathering
normalizes against these). replay.ts walks the same deltas for a different
output (per-frame scrub index) — neither is a copy of the other."""
commit_ms = [iso_to_ms(c.date) or 0 for c in commits]
# Parsed once per path, not per (commit, path): the loop below runs ~98M
# times on a big repo, and re-parsing there is what made this the slow step.
created_ms = {p: iso_to_ms(v) for p, v in git_created.items()}
modified_ms = {p: iso_to_ms(v) for p, v in git_modified.items()}

final_idx: dict[str, int] = {}
genesis_idx: dict[str, int] = {}
for i, delta in enumerate(deltas):
for path, sha in delta.changes:
final_idx[path] = i
if sha is not None and path not in genesis_idx:
genesis_idx[path] = i

present: set[str] = set()
last_change: dict[str, int] = {}
ranges: list[DateRangeMs] = []
for i, delta in enumerate(deltas):
for path, sha in delta.changes:
last_change[path] = i
if sha is None:
present.discard(path)
else:
present.add(path)

min_created = min_modified = None
max_created = max_modified = None
for path in present:
lm = last_change.get(path, 0)
modified = None
if lm >= final_idx.get(path, 0):
modified = modified_ms.get(path)
if modified is None:
modified = commit_ms[lm] if lm < len(commit_ms) else 0
created = created_ms.get(path)
if created is None:
gi = genesis_idx.get(path, 0)
created = commit_ms[gi] if gi < len(commit_ms) else 0

if min_created is None or created < min_created:
min_created = created
if max_created is None or created > max_created:
max_created = created
if min_modified is None or modified < min_modified:
min_modified = modified
if max_modified is None or modified > max_modified:
max_modified = modified

# An empty present set collapses to a zero span, which the client already
# treats as "no spread" (freshest / newest).
ranges.append(
DateRangeMs(
minCreated=min_created or 0,
maxCreated=max_created or 0,
minModified=min_modified or 0,
maxModified=max_modified or 0,
)
)
if on_commit is not None and i % _ASSEMBLE_HEARTBEAT_EVERY == 0:
on_commit(i, len(deltas))
return ranges


def build_timeline_bundle(
root: str,
source: SourceRef,
Expand Down Expand Up @@ -450,16 +373,6 @@ def build_timeline_bundle(
)
assemble_tick(on_progress, 2) # per-commit line ranges
commit_line_ranges = compute_commit_line_ranges(deltas, blob_lines)
# The long one: it walks every file present at every commit, so it reports
# from inside rather than sitting on its own step for minutes.
assemble_tick(on_progress, 3)
commit_date_ranges = compute_commit_date_ranges(
deltas,
commits,
history.created,
history.modified,
on_commit=lambda i, total: assemble_tick(on_progress, 3, i / max(total, 1)),
)
log("timeline bundle complete")

return TimelineBundle(
Expand All @@ -475,6 +388,5 @@ def build_timeline_bundle(
blobLines=blob_lines,
blobSizes=blob_sizes,
commitLineRanges=commit_line_ranges,
commitDateRanges=commit_date_ranges,
notes=notes,
)
2 changes: 0 additions & 2 deletions api/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,6 @@ def make_manifest(src: str = "/repo", **overrides: Any) -> Manifest:
"dirtyFileCount": 0,
"codeBytes": 0,
"commitCount": 0,
"commitDates": {"oldest": None, "newest": None},
"maxCommitStreakDays": 0,
"authors": [],
},
Expand All @@ -565,7 +564,6 @@ def make_timeline_bundle(**overrides: Any) -> TimelineBundle:
"blobLines": {},
"blobSizes": {},
"commitLineRanges": [],
"commitDateRanges": [],
"notes": [],
}
return TimelineBundle.model_validate({**fields, **overrides})
Expand Down
5 changes: 0 additions & 5 deletions api/tests/scan/test_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from api.models.manifest import (
AuthorStat,
BusynessThresholds,
CommitDateRange,
CommitLeader,
RangeStat,
)
Expand Down Expand Up @@ -178,7 +177,6 @@ def test_commit_leaders_authors_and_streak():
assert s.newestCommit.sha == "ddd"
assert s.newestCommit.date == "2022-02-10"
assert s.minFilesPerCommit == CommitLeader(sha="ccc", files=1, date="2022-01-03")
assert s.commitDates == CommitDateRange(oldest="2022-01-01", newest="2022-02-10")
assert s.maxCommitsPerDay.count == 2
assert s.maxCommitStreakDays == 3
assert s.authors[0] == AuthorStat(name="Ada", commits=3, hue=_author_hue("Ada"))
Expand All @@ -203,8 +201,6 @@ def test_newest_commit_is_the_last_one_that_day():

assert s.newestCommit.sha == "ccc"
assert s.oldestCommit.sha == "aaa"
# The day range is still a day, whatever the timestamps carry.
assert s.commitDates == CommitDateRange(oldest="2026-08-15", newest="2026-08-15")


def test_empty_tree_and_no_commits():
Expand All @@ -213,7 +209,6 @@ def test_empty_tree_and_no_commits():
assert s.maxFilesPerCommit is None
assert s.maxCommitStreakDays == 0
assert s.authors == []
assert s.commitDates == CommitDateRange(oldest=None, newest=None)
assert s.lineCountRange == RangeStat(min=0, max=0)
assert s.mediaCount == 0

Expand Down
65 changes: 1 addition & 64 deletions api/tests/scan/test_timeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import os
import subprocess
from pathlib import Path
from api.models.manifest import DateRangeMs, RangeStat
from api.models.manifest import RangeStat

from api.scan.timeline import walk_deltas
from api.git import SourceRef
Expand Down Expand Up @@ -350,69 +350,6 @@ def test_head_line_range_matches_live_scan(tmp_path: Path) -> None:
assert ranges[-1] == live_range # Timeline HEAD == Live


def test_head_date_range_matches_live_scan(tmp_path: Path) -> None:
"""The date analog of the line-range contract: commitDateRanges[-1] (HEAD)
equals the live scan's dateRanges for the same repo, so weathering at HEAD
normalizes identically. Compared as epoch ms, which is what the client
consumes."""
from api.scan.timeline import compute_commit_date_ranges, walk_deltas
from api.utils.dates import iso_to_ms
from api.git.meta import collect_git_history
from api.scan.scanner import scan_tree

_init(tmp_path)
(tmp_path / "a.txt").write_text("one\n")
_commit(tmp_path, "c1")
(tmp_path / "b.txt").write_text("two\n")
_commit(tmp_path, "c2")
(tmp_path / "a.txt").write_text("one\nmore\n")
_commit(tmp_path, "c3")

live = None
for ev in scan_tree(str(tmp_path), SourceRef(str(tmp_path)), use_cache=False):
if ev.phase == "manifest-complete":
live = ev.manifest
assert live is not None
live_dates = live.dateRanges

deltas = walk_deltas(tmp_path)
git_created, git_modified, commits = collect_git_history(tmp_path, use_cache=False)
ranges = compute_commit_date_ranges(deltas, commits, git_created, git_modified)

assert ranges[-1] == DateRangeMs(
minCreated=iso_to_ms(live_dates.minCreated),
maxCreated=iso_to_ms(live_dates.maxCreated),
minModified=iso_to_ms(live_dates.minModified),
maxModified=iso_to_ms(live_dates.maxModified),
)
# One range per commit, and the created span never runs backwards.
assert len(ranges) == len(deltas)
assert all(r.minCreated <= r.maxCreated for r in ranges)


def test_commit_date_ranges_track_the_present_set(tmp_path: Path) -> None:
"""A deleted file drops out of the range at the commit that removed it."""
from api.scan.timeline import compute_commit_date_ranges, walk_deltas
from api.git.meta import collect_git_history

_init(tmp_path)
(tmp_path / "old.txt").write_text("x\n")
_commit(tmp_path, "c1")
(tmp_path / "new.txt").write_text("y\n")
_commit(tmp_path, "c2")
(tmp_path / "old.txt").unlink()
_commit(tmp_path, "c3")

deltas = walk_deltas(tmp_path)
git_created, git_modified, commits = collect_git_history(tmp_path, use_cache=False)
ranges = compute_commit_date_ranges(deltas, commits, git_created, git_modified)

# At c1 only old.txt exists, so the span is a single instant.
assert ranges[0].minCreated == ranges[0].maxCreated
# At HEAD only new.txt survives, so its creation is both ends again.
assert ranges[-1].minCreated == ranges[-1].maxCreated


def test_bundle_replay_matches_reconstruct(tmp_path: Path) -> None:
"""Replaying deltas[0..i] reproduces reconstruct_manifest's file set + lines
at that ref — ties the bundle to the proven phase-1 reconstruction. The
Expand Down
1 change: 0 additions & 1 deletion api/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,6 @@ def test_manifest_serializes(self) -> None:
"oldestCommit": None,
"newestCommit": None,
"commitCount": 0,
"commitDates": {"oldest": None, "newest": None},
"maxCommitsPerDay": None,
"maxCommitStreakDays": 0,
"authors": [],
Expand Down
Loading
Loading