Commit Graph

3 Commits

Author SHA1 Message Date
35102be863 release: 0.2.0 «Urbanus I» — a private photo album, and a tagger for it
Per-person photo commands, where the mechanism is public and the album is
not. Nothing identifying is in this repository: who the people are, what
fires their photos and which file shows whom all live in one directory
outside it, and the tests, the docs and the offline e2e run use three
invented people instead.

Several names in one message intersect -- "!alfa beta" is a photo with both
-- and where an entry offers a choice the bot picks among the readings that
actually have a photo, so a question the album could answer is not refused.
A combination nobody photographed gets one line rather than silence, which
from the group's side is indistinguishable from a broken bot.

Scoping is enforced in the dispatcher, not in the handler: a command the
group is not allowed resolves to nothing, so it is not answered, not counted
as a command, and never named in a log line outside its own groups. A
handler that quietly returned would leak all three. Commands are named by
pseudonym (pack:p1), so the journal and command_uses say somebody asked for
a photo and nothing about whose.

Video works: the attachment goes out as data:video/mp4;filename=...;base64,
which is the container's own spelling, read off its /swagger/doc.json.
Told it is a photo, Signal renders a clip as an unplayable file.

tool/tagger.py serves one page on loopback to fill the album in -- names are
typed into the page, never as arguments, and it prints how many photos each
command would have when it stops, because a command with zero is dead on
arrival.

Also renames the real people who were still sitting in the stats test
fixtures. They remain in the history; the point is that nothing new is
added to it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017w1HZ3cZ7neL3rFdQ6VMBA
2026-08-30 22:35:41 +02:00
0e0c9ba619 feat: the nightly post notices things
Around twenty things could be said about a day. Saying all of them is a
wall nobody reads; saying whichever three are computed first is
arbitrary. So each candidate declares a priority and a magnitude and the
picker takes the top few, with two rules that make the result readable:
one line per family, so "set a record", "had 60% of the day" and "kept
the crown" do not become three sentences about the same person; and
fully deterministic ordering, so a day renders identically twice.

Nine candidates so far: personal records with what they beat, the
group's busiest day ever, a photo record, crown streaks (naming everyone
when it is shared), a silence broken, a monologue, one person dominating
the day, an unusual showing for that weekday, and a night owl.

Every candidate is (DayFacts) -> Highlight | None with the same
signature. On day one of this release almost all of them return None,
and a shape where "not yet" is the ordinary case is much easier to get
right than one where every guard is bespoke.

Two off-by-ones fixed by writing the sentence out and checking it:

- "odezwał się po 3 dniach ciszy" was counting four. The day somebody
  last spoke is not silent, and neither is the day they came back on, so
  the gap excludes both ends. The function is now named for that.
- A personal record on somebody's first ever day was being announced as
  a record. It is, trivially, and saying so every time is noise.

Verified against the live database, read-only on a copy: the real group
renders correctly at 324 messages, claims no average length for the
backfilled day, and fires no highlights at all -- which is right, since
there is one day of history for them to stand against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015d48t4qsQLBJnDwDeo2Nom
2026-08-30 19:43:55 +02:00
a9ffd02f33 feat: periods, records and the crown
The queries the statistics are made of, all derived rather than cached --
two sources of truth for one number is how a leaderboard comes to
disagree with itself, and this history is small enough that recomputing
is free.

Periods take an inclusive span of stored Warsaw day strings, never a
range of timestamps, so a daylight-saving change cannot move a boundary.
Trend reports None rather than 0 when there is no previous period:
"nothing to compare against yet" and "no change" are different
statements and the group will read them differently.

A tie shares the crown and keeps every tied run alive, per kuhy's call.
Streaks count *recorded* days, so a day nobody spoke does not end a run
-- there was no crown to lose -- and neither does a day the bot was
offline for, which it could not have judged.

Every record excludes the backfilled day. It is a 22-hour window filed
as a day, and leaving it in would hand somebody a personal best they
never set.

Two bugs found by running the code rather than reading it:

- current_streaks reported every streak as zero. Ending somebody's run
  dropped them from the tally along with the length they had already
  accumulated, so the surviving dict was empty by the time it returned.
  Now the run ends and the count it reached is kept.
- daily_winners depended on SQLite's grouping order to find a day's
  leader. It happened to work because uuids sorted conveniently in the
  fixtures. Ordering by messages DESC makes the first row per day the
  leader by construction, which is both correct and obviously so.

No suppressions: the period queries compare an exclusion parameter
against '' instead of pasting half a WHERE clause into an f-string,
which is a SQL-injection shape even when every input is a constant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015d48t4qsQLBJnDwDeo2Nom
2026-08-30 19:38:54 +02:00