Thanks to visit codestin.com
Credit goes to github.com

Skip to content

spaces: doc.trail, doc.periods, and the three progress charts - #445

Open
nyblnet wants to merge 1 commit into
mainfrom
spaces-trail
Open

spaces: doc.trail, doc.periods, and the three progress charts#445
nyblnet wants to merge 1 commit into
mainfrom
spaces-trail

Conversation

@nyblnet

@nyblnet nyblnet commented Sep 11, 2026

Copy link
Copy Markdown
Owner

A tracker could always tell you what is open today and never what was open last
Tuesday, because the only artefact that ever knew was gone. This adds the two
format fields that fix that, and the three charts that read them.

Implements the approved doc.trail / doc.periods design, with the
maintainer's three changes: status options rather than phase groups, the
trail stripped from published copies with no checkbox, and — the one
structural change — one shared, proportional record budget instead of two
absolute ceilings.

The shapes

// doc.trail — key: "<YYYY-MM-DD>" | "<seriesId>/<YYYY-MM-DD>"
interface TrailRow {
  n: Record<string, number>    // issues per STATUS OPTION id, zeroes omitted
  e?: Record<string, number>   // summed estimate per option; absent if nothing is estimated
  x?: number                   // issues counted that carried no estimate
  s?: number                   // days this row stands for after thinning
  cut?: true                   // oldest survivor; older rows were dropped
}

// doc.periods — id -> the window a chart is about
interface Period {
  label: string; from: string; to: string     // inclusive local day labels
  source?: ViewSource; filter?: ViewFilter     // the scope, FROZEN at commit
  series?: string
  base?: { at: string; n: number; e?: number; x?: number }   // totals, never page ids
  closed?: true
}

// the block
{ type: 'chart', kind: 'burndown'|'burnup'|'cfd', period: 'pd-7c1a', html: 'Burndown — Sprint 12' }

Both are maps, declared in DOC_MAPS and handed to the kernel's shape(), so
the CRDT merges them per key: two people working on the same day cannot lose
each other's row. The same list drives a parse-time fold of /^trail\./
top-level keys, which is what an older peer writes when its shape does not know
the map — cheap, deterministic, self-healing, and no kernel change.

The parts that are the actual work

A gap draws as a gap. A missing key breaks the line and hatches the region,
labelled not recorded. No interpolation, no carry-forward, no zero — a carried
value looks exactly like data, and an interpolated weekend shows work happening
on Sunday. A day with nothing open is still a point at zero, and the two look
different. Thinned rows are a third state: dashed, so a weekly sample is never
mistaken for a daily reading.

Editing the past does not rewrite it. One code path writes one key —
today's. There is no "recompute the last 7 days" and no repair pass. backfill
refuses to overwrite. The trail is excluded from undo snapshots (a row lands
outside any commit; an undo could otherwise restore a stale row over a newer
one) — but doc.periods is not excluded, because starting a period is
something a person did.

Timezones. A row key is a local day label, minted once from
journal.todayISO(). Everything after that is UTC ordinal arithmetic on the
label, so no DST boundary or locale can move it. Run under UTC, Berlin,
Kiritimati (UTC+14) and Niue (UTC-11).

One budget, proportional. clamp(25% of content bytes, 64 KB, 256 KB),
where content is title/home/theme/pages — not assets, since one
photograph would otherwise buy a document more room to record cadence in. Trail
and history tier independently inside it and the trail thins first: its
allowance is the budget less whatever doc.revisions currently holds, read
generically so neither side needs to know about the other. History is not
merged, so today the trail simply has the whole budget.

The one deviation from the brief, and why

The brief said to host these on kernel/src/charts.ts. I read it and measured
it instead, and it cannot host these three: renderCartesian maps every datum
through num(v, 0), so an absent day would draw as zero — the one thing
these charts exist not to do; there is no stack support anywhere in the file,
and a CFD is stacked bands by definition; and stroke-dasharray is set only to
animate a sweep, so a sample could not be drawn distinguishably from a daily
reading. Kernel is serialized and this is not a kernel change, so spaces draws
its own SVG — as graph.ts already does rather than shipping d3. No kernel
file is touched.
If the kernel zone would rather grow null-handling, stacking
and per-segment styling, that is a separate PR and this one can move onto it.

Verification

scripts/test-spaces-trail.ts157 behavioural checks, green under UTC,
Europe/Berlin, Pacific/Kiritimati and Pacific/Niue. It builds documents, writes
rows, draws the charts and asserts on the numbers and the geometry; no
source-grep assertions, which this zone has twice measured passing straight
through a live regression.

Sabotaged, each restored after: carrying a value forward into a gap (7 fail),
rewriting yesterday from today (2), pruning that gives up instead of tiering
(5), thinning that averages two rows (1 — and this one passed vacuously first
time against a uniform fixture, which is why the fixture's counts now move),
dropping trail from DOC_MAPS (8, including both replica checks), ignoring
history's bytes in the shared budget (1), erasing the sample/day distinction
(3), and removing the undo exclusion (1).

Browser-verified on a uniquely-named build over 127.0.0.1, marker checked on
every probe, DOM re-queried after every action. Measured there: the hatched band
sits between the two runs with no dot within 0.5px of the zero axis; the
segment arriving at a thinned sample has computed stroke-dasharray: 5px, 4px
while its neighbour's is none; a dangling period renders "this chart's
period is gone"
; and starting a period from the block's own control writes
base from live state with base.at = today, backfills today's row, and
fabricates no past key.

Also green: the whole spaces suite (model, agent, journal, calc, trail, undo,
invite, roundtrip, size), test-sync.ts (45,368 checks), test-sync-spaces,
test-sync-spaces-session, test-sync-shape, tsc --noEmit, shell-gate,
--manifest, and build-spaces-i18n --check at 623 × 8 = 100%, with the new
keys grepped out of packed.ts rather than trusted to the percentage.

Byte cost. A year of full daily rows measures 61.5 KB (172 B/row);
counts-only 31.9 KB. The shell grows +30.9 KB to 289 KB — charts.ts, the
three record modules and 26 strings × 8 locales.

Not done, and deliberately

No UI for editing a period's label, dates or scope beyond "new two-week period
from today" — the scope selectors exist in the format and are frozen correctly,
but choosing them needs a form this PR does not add. No multi-series UI (the key
shape ships; the picker does not). No bento.* agent verbs for the trail. No
backfill from doc.revisions — that is the migration to write the day history
lands.

A tracker could tell you what is open today and never what was open last
Tuesday, because the only artefact that ever knew was gone. A space now
records one small row per local day — issues per status option, summed
estimates, and the count that carried no estimate — and a new `chart` block
draws burndown, burnup and cumulative flow from it over a named period.

The format additions, both maps merged per key under collaboration so two
people working on one day cannot lose each other's row:

  doc.trail    "<YYYY-MM-DD>" | "<seriesId>/<YYYY-MM-DD>" -> { n, e?, x?, s?, cut? }
  doc.periods  id -> { label, from, to, source?, filter?, series?, base?, closed? }

A row holds counts and nothing else: no page ids, no assignee breakdown, no
per-issue anything. That is a budget rule and a privacy rule at once, and it
is why a reading copy and a page extract carry none of it — the aggregate is
dull, the cadence is not.

Three things the design turns on:

  · A GAP IS DRAWN AS A GAP. No interpolation, no carry-forward, no zero;
    the line breaks and the region is hatched. A thinned row stands for the
    span it replaced and draws dashed, so a weekly sample is never mistaken
    for a daily reading. Thinning selects an observation somebody made and
    never averages two into a number nobody saw.
  · TODAY IS DERIVED, every earlier day is read. Change an estimate now and
    today's point moves; yesterday's row cannot, because no code path
    addresses a past key.
  · ONE RECORD BUDGET, proportional: 25% of the document's own content
    between 64 KB and 256 KB, shared with version history when it lands, the
    trail thinning first. The record never outweighs what it is a record of.

Charts draw their own SVG rather than calling kernel's charts-lite, which
coerces an absent datum to zero (num(v,0)), has no stacking, and has no
per-segment dash — the three things these charts are about. Reasoning in
docs/DECISIONS.md; no kernel file is touched.

scripts/test-spaces-trail.ts is behavioural throughout: it builds documents,
writes rows, draws charts and asserts on the numbers and the geometry. 157
checks under four timezones including UTC+14 and UTC-11. Verified in a real
browser on a uniquely-named build: the gap band sits between the two runs
with no point at zero, and a sample segment's computed stroke-dasharray is
5px,4px while its neighbour's is none.
@github-actions

Copy link
Copy Markdown

Build size

main (0100083) → spaces-trail (1cf3afb)

app base PR change
bento/slides 677.9 KiB 677.9 KiB 0.0 KiB (0.00%)
bento/spaces 271.3 KiB 282.6 KiB +11.3 KiB (+4.17%)
bento/dash 424.1 KiB 424.1 KiB 0.0 KiB (0.00%)

Updated: 2026-09-11T20:14:56Z

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.

1 participant