From 506a827d776be92971d16c6c25728d40d66f2c92 Mon Sep 17 00:00:00 2001 From: Eric Wang Date: Tue, 1 Sep 2026 01:39:43 -0700 Subject: [PATCH] =?UTF-8?q?feat(strip):=20the=20future=20has=20a=20shape?= =?UTF-8?q?=20=E2=80=94=20unfold=20what=20folding=20cannot=20pay=20for,=20?= =?UTF-8?q?dim=20what=20you=20sleep=20through?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fold's own argument expired with v0.36.0: "the future is one fact — the count" was true while every hollow cell was identical, and the rest model made them different. A live frame made the cost concrete: `▮▯▯...×(✕6)` spends a nine-column token to hide five cells that said more drawn out. The fold keeps only the job it still does — column economy on the long early-week run — behind WEEK_FUTURE_UNFOLD_MAX (10): ten cells or fewer draw in full, the last ~2 days of every week. With the learned shape, a future ▯ whose 5h slot has fewer than REST_SLOT_AWAKE_MIN_SECS (9000) awake seconds — under half the cell — draws dim: capacity on the grid that is not really available. The 5h strip applies the same half-the-cell rule to its 1h cells. × outranks the tint, ▮ and history are untouched, unlearned renders byte-identical apart from the unfold rule, and the dim count sits within the same one-cell grid-vs-clock tolerance the fold count has always carried. Ships with ccpace v0.6.0 (the night on the ledger), which dims the same cells by the same rule. No forecast.cache change. 470 bats. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 35 ++++++++++++ DESIGN.md | 19 +++++-- README.md | 20 ++++--- docs/api/state-dir.md | 12 +++- statusline.sh | 103 +++++++++++++++++++++++++++-------- t/helpers.bash | 2 + t/statusline.bats | 124 +++++++++++++++++++++++++++++++++++------- 7 files changed, 258 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6585138..e7596cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,40 @@ # Changelog +## v0.38.0 — 2026-09-01 — the future has a shape + +**The fold's own argument expired.** "History is information; the future +is one fact — the count" was true while every hollow cell was identical. +v0.36.0 taught the cells when you sleep, and a live frame made the cost +concrete: `▮▯▯...×(✕6)` spends a nine-column token to hide five cells +that would have said more drawn out. So the fold keeps only the job it +still does — column economy on the long early-week run — and the rule is +now break-even: a future of `WEEK_FUTURE_UNFOLD_MAX` (10) cells or fewer +draws in full, which is the last ~2 days of every week. Above that, +nothing changes: two kept cells, the counted token, the same number the +budget line prices. The old measurement stands where it was made — 30 +hollow cells do read as "too much future". + +**A cell you sleep through draws dim.** With the learned shape (same +read validation and 14-day gate as the walk), a future `▯` whose 5h slot +has fewer than `REST_SLOT_AWAKE_MIN_SECS` (9000) awake seconds — under +half the cell — dims: capacity on the grid that is not really available. +The 5h strip applies the same half-the-cell rule to its 1h cells, so +`▃▮▯▯` with the last two dim says the window outlives your evening. `×` +outranks the tint (a cell the pool cannot cover is unusable for a +stronger reason), `▮` and history are untouched, and unlearned every `▯` +stays plain — the render is byte-identical to v0.37.0 apart from the +unfold rule. The tint is the one refinement this row carries by color +alone, and the one place that is honest about it: a dim `▯` is still a +window ahead, so a reader without the tint loses nothing actionable. The +dim count may sit one off the budget's `~N awake` — cells are grid +slots, the sentence is clocks, the same one-cell tolerance the fold +count has always carried. + +Ships with [ccpace](https://github.com/thevibeworks/ccpace) v0.6.0 ("the +night on the ledger"), which dims the same cells by the same rule; +`REST_SLOT_AWAKE_MIN_SECS` joins the shared reading rules in +`docs/api/state-dir.md`. No `forecast.cache` change of any kind. + ## v0.37.0 — 2026-09-01 — what a point buys **Two pools drain toward one wall and nothing said which one binds.** The diff --git a/DESIGN.md b/DESIGN.md index b57eeb9..35298f2 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -99,11 +99,15 @@ the badge states the end (`5h[38%@23:00]`) and the notice names the wall (`5h caps ~14:20`). Cells carry the shape, badges carry state, notices do the warning. History draws in full; the live row folds the 7d future after two kept cells into `...▯(x14)` — the 5h windows *after* the one -you are in, all alike, `×` red when the tail projects dry (the `week` +you are in, `×` red when the tail projects dry (the `week` report still draws every slot). The count is the budget line's own, -priced from the same instant, and it folds as soon as folding hides two -cells: the future is one fact, and the ink belongs to history. Each strip -ends +priced from the same instant. The fold pays only while the tail is wider +than its own token: ten cells or fewer draw in full, because the future +stopped being one fact the day the cells learned your rhythm — a hollow +cell you sleep through most of draws *dim*, so the drawn tail is a shape +(tonight, the night, tomorrow morning), not a run of identical slots. +Unlearned, every `▯` stays plain and only the fold rule changes. Each +strip ends with its pace (used ÷ elapsed; dim <1x, pressure ≥1x, hidden until 5% of the window has run) and the reset its right edge is — axis labels, not restated badges. @@ -113,8 +117,11 @@ badges. ▁ baseline: ran, negligible — the shortest bar of the same block ░ unknown — no sample; never drawn as idle ▮ now -▯ ahead — the hollow of ▮; on 5h, the hours left in the window -...▯(x14) the folded future: 14 more 5h windows after this one (live 7d row) +▯ ahead — the hollow of ▮; on 5h, the hours left in the window; + dim when your learned hours say you sleep through most of it + (× outranks it: unreachable beats asleep) +...▯(x14) the folded future: 14 more 5h windows after this one (live 7d + row, only while the tail is wider than this token) × pace won't cover it (7d only: learned forecast, linear when cold) ``` diff --git a/README.md b/README.md index fe5da9b..3e5c846 100644 --- a/README.md +++ b/README.md @@ -318,14 +318,16 @@ fire. left, the last a 3h stub), height = the 7d points that window burned, with a thin gap at each local midnight so days read as clusters — and a day that held five windows shows it — without a ruler. History draws in - full; the future folds as soon as folding hides two cells: two hollow - cells after `▮`, then `...▯(✕28)` — 28 more 5h windows **after the one - you are in**, all alike (`×` red when the tail projects dry). That count - is the budget line's own `~28✕5h left`, priced from the same instant: - one row, one arithmetic. The future is one fact and the strip's ink belongs to - history; a closing week needs no help to draw to its edge, since four - cells or fewer leave nothing worth folding. The `week` subcommand's wide - ledger still draws every slot. + full; the future folds only while folding pays: with more than ten cells + ahead, two hollow cells after `▮`, then `...▯(✕28)` — 28 more 5h windows + **after the one you are in** (`×` red when the tail projects dry). That + count is the budget line's own `~28✕5h left`, priced from the same + instant: one row, one arithmetic. Ten cells or fewer — the last ~2 days + of every week — draw in full, because the cells stopped being all alike: + once the learned rest model knows your hours, a hollow cell you + sleep through most of draws **dim**, so the drawn tail is a shape + (tonight, the night, tomorrow morning), not a count. The `week` + subcommand's wide ledger still draws every slot. | Cell | Meaning | |------|---------| @@ -333,7 +335,7 @@ fire. | `▁` | the baseline: ran and cost under a point, or ran idle inside the log's coverage. The shortest bar of the same Block Elements run as `▂▃▄`, so the zero line shares their font, width and height — it used to be `ˍ` (U+02CD), a modifier *letter*, which terminals resolve through the text face and which broke the strip's metrics mid-row. Override with `LEDGER_BASE_GLYPH` | | `░` | unknown: the log has no sample for that cell (never drawn as idle — a gap in the record is not a quiet session) | | `▮` | the cell you are in now | -| `▯` | a cell still ahead of you — the hollow of `▮`, an empty slot waiting. On the 5h strip the run of them is the hours left in the window | +| `▯` | a cell still ahead of you — the hollow of `▮`, an empty slot waiting. On the 5h strip the run of them is the hours left in the window. **Dim** when your learned hours say you sleep through most of it (under half the cell awake): capacity on the grid that is not really available. `×` outranks it — unreachable beats asleep — and a reader who cannot see the tint loses nothing actionable: a dim `▯` is still a window ahead | | `×` | a cell the pool will not cover at the current pace — **7d only**: the learned forecast's dry point, linear when untrained. The 5h window's wall belongs to the `5h caps ~14:20` notice, which states the time instead of shading cells | | `...▯(✕28)` | the folded 7d future: 28 more 5h windows after the one you are in, one token instead of a run of hollow cells (`×` red when the tail ends dry). The count is what the budget line prices — not the number of cells the fold happened to hide | | `✕` | not a cell — the multiplication sign, the row's one operator (`...▯(✕28)`, `0.7✕`, `19✕5h left`). Deliberately not `×` (U+00D7), which is already a reading: cells are the ink, the operator is punctuation, and `...×(×28)` has to say both at once. One terminal column and no emoji fallback, so the row still meets line 1's edge; override with `MULT_GLYPH` (`╳` and `✖` look stronger but are ambiguous-width and emoji-presentation respectively) | diff --git a/docs/api/state-dir.md b/docs/api/state-dir.md index 2e69ae1..1065318 100644 --- a/docs/api/state-dir.md +++ b/docs/api/state-dir.md @@ -7,7 +7,7 @@ read concurrently. - Contract version: **2** (bump on any breaking layout/field change; this file is the changelog) -- Synced with: statusline.sh v0.37.0 +- Synced with: statusline.sh v0.38.0 - Permissions: the script runs under `umask 077` — files are owner-only. Caches hold account PII (email, uuid, org names). @@ -251,6 +251,16 @@ equal count would spend a clause to say nothing. At `awake == 0` both the awake clause and the ration that would divide by it go away; the landing still speaks. +A REST SLOT is a ledger cell you sleep through: fewer than +`REST_SLOT_AWAKE_MIN_SECS = 9000` awake seconds in its 5h span — under +half the cell, the break-even a reader can hold in their head. Both +ledgers (this statusline's week strip, ccpace's block) draw such a future +cell as a dim `▯`; the general rule is half the cell, which is how the 5h +strip applies it to 1h cells (1800). `×` outranks the tint — a cell the +pool cannot cover is unusable for a stronger reason — and the dim count +may sit one off the sentence's `~N awake`: cells are grid slots, the +sentence is clocks, the same one-cell tolerance the fold count carries. + `scoped_*` mirror the all-model fields for the `weekly_scoped` limit — the per-model weekly cap, `Fable` at time of writing. `scoped_name` is the scope the profile is ABOUT (`null` while unobserved); burn is diff --git a/statusline.sh b/statusline.sh index a361fc8..7b13514 100755 --- a/statusline.sh +++ b/statusline.sh @@ -2109,6 +2109,13 @@ SCOPE_STRAND_MIN_PCT=10 # scoped points the mix strands before it is wo SCOPE_MIX_MIN_7D=60 # ...and only once the account's own end is in sight SCOPE_MIX_MIN_SCOPE=5 # a model untouched this week is the underuse voice's job SCOPE_SAME_WALL_SEC=120 # resets this close are one wall; further apart, no ratio +# A 5h ledger slot is a NIGHT when under half of it is waking time (awake +# seconds < 9000, hours with mult >= REST_MULT_MAX). Half is the break-even a +# reader can hold in their head: more asleep than awake and the slot is +# capacity you will not reach. Shared with ccpace (docs/api/state-dir.md) so +# both ledgers dim the same cells; the strip applies the same half-the-cell +# rule to its 1h cells, where the threshold is 1800. +REST_SLOT_AWAKE_MIN_SECS=9000 # Rotate usage.jsonl when it exceeds the cap. Single .1 backup; the profile # builder reads .1 + current, so rotation never costs learned history. @@ -2979,7 +2986,7 @@ run_session_summary() { # ▁ ran, burned under a point — the baseline # ░ unknown: no samples on record for that window # ▮ the window you are in now -# ▯ a window still ahead of you +# ▯ a window still ahead of you (dim: one you likely sleep through) # × a window the pool will not cover at the current pace # The report draws every slot, so what follows ▮ IS the budget line's # "~N✕5h left" laid out cell by cell — ▮ itself is where you are, not a @@ -3002,7 +3009,16 @@ run_session_summary() { # modifier letter used to sit here and broke the row's metrics. # ░ unknown — outside the sample log's coverage # ▮ the cell you are in now -# ▯ a cell still ahead of you (the hollow of ▮: an empty slot) +# ▯ a cell still ahead of you (the hollow of ▮: an empty slot); +# DIM when the learned rhythm says you sleep through most of it — +# capacity on the grid that is not really available. The one +# refinement this row carries by tint alone, and the one place +# that is honest: a dim ▯ is still a window ahead, so a reader +# who cannot see the tint loses nothing actionable. × outranks +# it — a cell the pool cannot cover is unusable for a stronger +# reason — and the dim count may sit one off the budget's +# "~N awake": these are grid slots, the sentence is clocks, the +# same one-cell tolerance the fold count documents below. # × a cell the pool will not cover at the current pace — 7d only # ...▯(✕11) the folded future: 11 more 5h windows AFTER the one you are in, # all alike (× red when the tail projects dry) — live 7d strip @@ -3035,15 +3051,20 @@ WEEK_CACHE_TTL_SECS=300 # keeps WEEK_FUTURE_KEEP hollow cells, then folds the rest into `...▯(✕11)` # (count = windows_ahead, the 5h windows after this one; × red when the tail # projects dry). -# History is information; the future is one fact, and the fact is the count. -# The threshold is 2, not a column break-even: eleven hollow cells were -# measured to read as "too much future" long before they were expensive, and -# the count is the part that has to survive mid-week, when a pressure notice -# owns the pin and this row is the only place the remaining windows appear. -# A closing week still draws to its edge without help — four cells or fewer -# leave nothing worth folding. +# "History is information; the future is one fact" was this fold's original +# argument, and the rest tint SUPERSEDED it: future cells now carry which +# windows the reader is awake for, so the future is a shape, not one fact. +# What remains of the fold is column economy. The token `...×(✕11)` is nine +# columns; hiding fewer cells than that pays nothing, so a future of +# WEEK_FUTURE_UNFOLD_MAX cells or fewer draws in full — the last ~2 days of +# every week — and only the long early-week run still folds. The old +# measurement stands where it was made: 30 hollow cells DO read as "too much +# future", and the count is still the part that has to survive mid-week, +# when a pressure notice owns the pin and this row is the only place the +# remaining windows appear. WEEK_FUTURE_KEEP=2 WEEK_FUTURE_MIN_HIDE=2 +WEEK_FUTURE_UNFOLD_MAX=10 # The period start on the same 5-min grid the window keys are rounded to. # resets_at is jittered by the API (15:59:59.76 one fetch, 16:00:00.47 the @@ -3196,13 +3217,15 @@ week_dry_slot() { # windows AHEAD of the current one, counted from real clocks by the caller; # the strip must not re-derive it, since a 34-cell grid spans 170h against a # 168h period and a count read off the drawing disagrees with the budget -# sentence beside it — and finally an optional now-cell (-1: derive it from -# the period start, which is what a grid-aligned strip wants). +# sentence beside it — an optional now-cell (-1: derive it from +# the period start, which is what a grid-aligned strip wants), and finally +# the learned hour multipliers ("" unlearned): with them, a future ▯ whose +# cell is mostly rest hours draws dim (see REST_SLOT_AWAKE_MIN_SECS). # Cells and gaps come out of one awk so the row is one string with a color # run per role. build_ledger_strip() { local pct="$1" now="$2" ps="$3" dry="$4" hist="$5" cells_n="$6" cell_secs="$7" gaps="${8:-0}" fut="${9:-0}" - local nleft="${10:-0}" nowslot_in="${11:--1}" + local nleft="${10:-0}" nowslot_in="${11:--1}" mults="${12:-}" local span_lo="" span_hi="" cells="" [ -n "$hist" ] && read -r span_lo span_hi cells <<<"$hist" local fill_color tzoff_s @@ -3210,8 +3233,9 @@ build_ledger_strip() { tzoff_s=$(date +%z | awk '{ s=substr($0,1,1)=="-"?-1:1; h=substr($0,2,2)+0; m=substr($0,4,2)+0; print s*(h*3600+m*60) }') awk -v w="$cells_n" -v cs="$cell_secs" -v ps="$ps" -v now="$now" -v dry="$dry" \ -v gaps="$gaps" -v tz="$tzoff_s" -v fut="$fut" \ - -v minhide="$WEEK_FUTURE_MIN_HIDE" -v nleft="$nleft" -v mult="$MULT_GLYPH" \ - -v nsin="$nowslot_in" \ + -v minhide="$WEEK_FUTURE_MIN_HIDE" -v unfoldmax="$WEEK_FUTURE_UNFOLD_MAX" \ + -v nleft="$nleft" -v mult="$MULT_GLYPH" \ + -v nsin="$nowslot_in" -v mults="$mults" -v restmult="$REST_MULT_MAX" \ -v lo="${span_lo:--1}" -v hi="${span_hi:--1}" -v cells="$cells" \ -v C_FILL="$fill_color" -v C_DIM="$DIM" -v C_NOW="$BOLD" \ -v C_DRY="$RED" -v C_OFF="$RESET" -v BASE="$LEDGER_BASE_GLYPH" ' @@ -3225,15 +3249,33 @@ build_ledger_strip() { if (c <= 11) return "▅"; if (c <= 15) return "▆" if (c <= 20) return "▇"; return "█" } + # Awake seconds of the cell starting at t0: the same hour arithmetic + # awake_secs walks, inlined because a fork per cell is 34 forks per + # render. hm[] is 1-based (split); an empty mults string leaves + # have_rest 0 and no cell ever asks. + function cell_awake(t0, t2, end2, lt2, seg2, aw2) { + end2 = t0 + cs; aw2 = 0; t2 = t0 + while (t2 < end2) { + lt2 = t2 + tz + seg2 = t2 + 3600 - (lt2 % 3600) + if (seg2 > end2) seg2 = end2 + if (hm[int((lt2 % 86400) / 3600) + 1] + 0 >= restmult) aw2 += seg2 - t2 + t2 = seg2 + } + return aw2 + } BEGIN { n = split(cells, a, ",") for (i = 1; i <= n; i++) { split(a[i], kv, ":"); cost[kv[1]] = kv[2] } + have_rest = (split(mults, hm, " ") == 24) nowslot = (nsin >= 0 ? nsin : int((now - ps) / cs)) - # fold the future tail: everything past the kept cells is the - # same hollow slot, so name it once with a count instead of - # drawing it N times + # fold the future tail — but only when folding pays: the token is + # nine columns, and a future of unfoldmax cells or fewer costs no + # more drawn in full while carrying the rest shape the token + # cannot (see WEEK_FUTURE_UNFOLD_MAX) lim = w - if (fut > 0 && nleft > 0 && w - (nowslot + 1 + fut) >= minhide) + if (fut > 0 && nleft > 0 && w - (nowslot + 1) > unfoldmax \ + && w - (nowslot + 1 + fut) >= minhide) lim = nowslot + 1 + fut s = ""; prev = ""; pday = -1 for (i = 0; i < lim; i++) { @@ -3255,7 +3297,14 @@ build_ledger_strip() { else { g = "░"; c = C_DIM } } else if (i == nowslot) { g = "▮"; c = C_NOW } else if (dry >= 0 && i >= dry) { g = "×"; c = C_DRY } - else { g = "▯"; c = "" } + else { + # a window still ahead — dim when the learned rhythm says + # the reader sleeps through most of it (under half the + # cell awake). × above outranks this: unreachable beats + # asleep. Unlearned, every ▯ stays plain. + g = "▯"; c = "" + if (have_rest && 2 * cell_awake(start) < cs) c = C_DIM + } if (c != prev) { s = s C_OFF c; prev = c } s = s g } @@ -3277,9 +3326,13 @@ build_ledger_strip() { # 7d strip: 34 ✕ 5h cells from the period start, day-gapped. $5 is # week_history_cells' line; $6 folds the future tail after that many kept # cells (the live row passes WEEK_FUTURE_KEEP; the `week` report draws all and -# never folds); $7 is windows_ahead, the number the fold token prints. +# never folds); $7 is windows_ahead, the number the fold token prints. $8 is +# a test seam: the hour multipliers, read from the learned profile when the +# caller does not say (an UNSET $8, not an empty one — "" is how a test +# states "unlearned" against a fixture that would otherwise speak). build_week_strip() { - build_ledger_strip "$1" "$2" "$3" "$4" "$5" "$WEEK_CELLS" 18000 1 "${6:-0}" "${7:-0}" + local mults="${8-$(hour_profile_mults)}" + build_ledger_strip "$1" "$2" "$3" "$4" "$5" "$WEEK_CELLS" 18000 1 "${6:-0}" "${7:-0}" -1 "$mults" } # 5h strip: the five hours of the current window, always all five. $4 is @@ -3292,6 +3345,11 @@ build_week_strip() { # reflow every hour, which is the difference between an axis and a bar that # grows at you. # +# A dim hollow hour is NOT a forecast of the pool — it is the reader's own +# learned rhythm (an hour they are usually asleep for), the same rule the 7d +# cells apply at half-the-cell: for a 1h cell that is 1800 awake seconds. +# `▃▮▯▯▯` with the last two dim says the window outlives the evening. +# # ▮ rides the real clock, not the grid. five_period_start rounds to 5 minutes # so week_scan's cache key holds still across renders (a resets_at that jitters # by a second would re-run a whole-log jq pass every render), and that rounding @@ -3303,7 +3361,8 @@ build_five_strip() { local nowslot=$(( 4 - ${5:-0} / 3600 )) [ "$nowslot" -lt 0 ] && nowslot=0 [ "$nowslot" -gt $((FIVE_CELLS - 1)) ] && nowslot=$((FIVE_CELLS - 1)) - build_ledger_strip "$1" "$2" "$3" -1 "$4" "$FIVE_CELLS" "$FIVE_CELL_SECS" 0 0 0 "$nowslot" + local mults="${6-$(hour_profile_mults)}" + build_ledger_strip "$1" "$2" "$3" -1 "$4" "$FIVE_CELLS" "$FIVE_CELL_SECS" 0 0 0 "$nowslot" "$mults" } # Does a history line carry at least one cell BEFORE the now-cell? A single diff --git a/t/helpers.bash b/t/helpers.bash index 57902d9..03f854f 100644 --- a/t/helpers.bash +++ b/t/helpers.bash @@ -64,6 +64,8 @@ MULT_GLYPH="✕" LEDGER_BASE_GLYPH="▁" FORECAST_SCHEMA=2 REST_MULT_MAX=0.25 +REST_SLOT_AWAKE_MIN_SECS=9000 +WEEK_FUTURE_UNFOLD_MAX=10 SCOPE_STRAND_MIN_PCT=10 SCOPE_MIX_MIN_7D=60 SCOPE_MIX_MIN_SCOPE=5 diff --git a/t/statusline.bats b/t/statusline.bats index 95bfa83..2181eeb 100644 --- a/t/statusline.bats +++ b/t/statusline.bats @@ -1530,15 +1530,11 @@ _seed_week_store() { [[ "$plain" == "7d "* ]] strip=$(printf '%s' "${plain#7d }" | sed 's/ [0-9.]*✕ @.*$//; s/ @.*$//') bar=$(printf '%s' "$strip" | tr -d ' ') - # the live row draws history + ▮ + WEEK_FUTURE_KEEP and folds the rest; - # the grid it draws them on is still 34 cells (`week` renders all of it) - [[ "$bar" =~ ^(.*)\.\.\.▯\(✕[0-9]+\)$ ]] - drawn="${BASH_REMATCH[1]}" - seven_secs=$(get_reset_seconds "$(jq -r .seven_day.resets_at "$tmpdir/usage.cache")") - ps=$(week_period_start "$(date +%s)" "$seven_secs") - nowslot=$(( ($(date +%s) - ps) / 18000 )) - [ "$(printf '%s' "$drawn" | grep -o . | wc -l)" -eq $((nowslot + 1 + WEEK_FUTURE_KEEP)) ] - [ $((nowslot + 1 + WEEK_FUTURE_KEEP)) -lt 34 ] + # 31h from the reset there are six cells ahead — ten or fewer draw in + # full (WEEK_FUTURE_UNFOLD_MAX), so the whole 34-cell grid shows; the + # mid-week fold has its own test on a five-day frame + [[ "$bar" =~ ▮▯+$ ]] + [ "$(printf '%s' "$bar" | grep -o . | wc -l)" -eq 34 ] [[ "$bar" =~ [▂▃▄▅▆▇█]▁[▂▃▄▅▆▇█] ]] [[ "$bar" == *▮* ]] # day gaps: single spaces inside the strip, several of them across a week @@ -1795,11 +1791,12 @@ _seed_week_store() { rm -rf "$tmpdir" } -@test "build_week_row: the future folds as soon as it hides two cells" { - # The threshold is not a column break-even. The future is one fact — the - # count — and mid-week, when a pressure notice owns the pin, this row is - # the only place the remaining windows appear. Eleven hollow cells read as - # "too much future" long before they were expensive. +@test "build_week_row: the future folds only when folding pays" { + # The fold's old rule ("as soon as it hides two cells") served "the + # future is one fact"; the rest tint superseded that — future cells now + # carry a shape — so what remains of the fold is column economy: a tail + # of WEEK_FUTURE_UNFOLD_MAX cells or fewer costs no more drawn in full + # than the nine-column token that would replace it. tmpdir=$(mktemp -d) CLAUDE_ACCOUNT_DIR="$tmpdir" now=$(date +%s) @@ -1809,18 +1806,107 @@ _seed_week_store() { local p; p=$(strip_ansi "$(build_week_row "$u" always)" | sed 's/ [0-9.]*✕ @.*$//; s/ @.*$//' | tr -d ' ') printf '%s' "${p#*▮}" } - # ~6 windows left: KEEP=2 drawn, the rest folded and counted + # ~6 windows left: cheaper drawn than folded, so every cell shows six=$(_tail_of $((now + 6 * 18000 - 3600))) - [[ "$six" =~ ^▯▯\.\.\.▯\(✕([0-9]+)\)$ ]] - [ "${BASH_REMATCH[1]}" -eq 6 ] - # the last cells of the week: with one cell to hide the fold would spend - # more ink than it saves, so the strip draws to its edge + [[ "$six" != *"..."* ]] + [[ "$six" =~ ^▯{5,7}$ ]] + # ~13 windows left: the tail is wider than the token, so it still folds + # to KEEP=2 cells and the count + mid=$(_tail_of $((now + 13 * 18000 - 3600))) + [[ "$mid" =~ ^▯▯\.\.\.▯\(✕([0-9]+)\)$ ]] + [ "${BASH_REMATCH[1]}" -eq 13 ] + # the last cells of the week: nothing to fold at all close=$(_tail_of $((now + 18000 + 600))) [[ "$close" != *"..."* ]] [[ "$close" =~ ^▯{0,3}$ ]] rm -rf "$tmpdir" } +# Mark dim runs so a test can assert WHERE the tint sits, not just that some +# escape exists: {D} opens dim, {R} is the reset that closes every run. +_mark_dim() { + printf '%s' "$1" | sed -e $'s/\x1b\\[2m/{D}/g' -e $'s/\x1b\\[0m/{R}/g' \ + -e $'s/\x1b\\[[0-9;]*m//g' +} + +@test "build_week_strip: the unfold boundary is the fold token's own width" { + # F = cells after ▮ on the 34-cell grid. Ten or fewer draw in full; + # eleven is the first tail wider than the token and folds. + local ps now10 now11 t10 t11 + ps=$(( ($(date +%s) / 86400 - 6) * 86400 )) + now10=$((ps + 23 * 18000 + 100)) # nowslot 23 -> F = 10 + now11=$((ps + 22 * 18000 + 100)) # nowslot 22 -> F = 11 + t10=$(strip_ansi "$(build_week_strip 50 "$now10" "$ps" -1 "" 2 10 "")") + t11=$(strip_ansi "$(build_week_strip 50 "$now11" "$ps" -1 "" 2 11 "")") + [[ "$t10" != *"..."* ]] + [[ "$t11" == *"...▯(✕11)"* ]] +} + +@test "build_week_strip: a cell you sleep through draws dim" { + # ps at UTC midnight; rest hours 0-8 (mult 0.1), awake 9-23. nowslot 27 + # puts six cells ahead: 20:00 (4h awake, plain), 01:00 (rest, dim), + # 06:00 (2h awake < half the cell, dim), then three daytime cells plain. + local mults="0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54" + local ps now out + ps=$(( ($(date +%s) / 86400 - 6) * 86400 )) + now=$((ps + 27 * 18000 + 100)) + out=$(_mark_dim "$(TZ=UTC build_week_strip 50 "$now" "$ps" -1 "" 2 6 "$mults")") + [[ "${out#*▮}" == "{R}▯{R}{D}▯▯{R}▯▯▯{R}" ]] +} + +@test "build_week_strip: × outranks rest — unreachable beats asleep" { + local mults="0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54" + local ps now raw + ps=$(( ($(date +%s) / 86400 - 6) * 86400 )) + now=$((ps + 27 * 18000 + 100)) + raw=$(TZ=UTC build_week_strip 50 "$now" "$ps" 31 "" 2 6 "$mults") + # the dry wall keeps its glyph and its red: no dim × anywhere + [[ "$(strip_ansi "$raw")" == *"×××"* ]] + [[ "$raw" != *$'\033[2m×'* ]] + # and the rest cells before it still dim + [[ "$(_mark_dim "$raw")" == *"▯{R}{D}▯▯{R}×××"* ]] +} + +@test "build_week_strip: unlearned, every future cell stays plain" { + local ps now raw + ps=$(( ($(date +%s) / 86400 - 6) * 86400 )) + now=$((ps + 27 * 18000 + 100)) + raw=$(TZ=UTC build_week_strip 50 "$now" "$ps" -1 "" 2 6 "") + # no dim escape anywhere after the now-marker (history ░ cells are dim + # by design and sit before it) + [[ "${raw#*▮}" != *$'\033[2m'* ]] +} + +@test "build_five_strip: hollow hours you sleep through draw dim" { + # a 22:00-03:00 UTC window, two hours in: the hours ahead are 01:00 and + # 02:00, both rest — the window outlives the evening and the strip says so + local mults="0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54 1.54" + local fps fnow out + fps=$(( ($(date +%s) / 86400 - 1) * 86400 + 22 * 3600 )) + fnow=$((fps + 2 * 3600 + 60)) + out=$(_mark_dim "$(TZ=UTC build_five_strip 40 "$fnow" "$fps" "" 10740 "$mults")") + [[ "${out#*▮}" == "{R}{D}▯▯{R}" ]] +} + +@test "build_week_strip: the tint flows from the learned cache, gated on history" { + # No mults argument: the builder reads hour_profile_mults, which refuses + # a cache younger than 14 days — the same evidence gate every learned + # surface waits behind. + tmpdir=$(mktemp -d) + CLAUDE_ACCOUNT_DIR="$tmpdir" + local hp ps now + hp='{"0":0.1,"1":0.1,"2":0.1,"3":0.1,"4":0.1,"5":0.1,"6":0.1,"7":0.1,"8":0.1,"9":1.54,"10":1.54,"11":1.54,"12":1.54,"13":1.54,"14":1.54,"15":1.54,"16":1.54,"17":1.54,"18":1.54,"19":1.54,"20":1.54,"21":1.54,"22":1.54,"23":1.54}' + ps=$(( ($(date +%s) / 86400 - 6) * 86400 )) + now=$((ps + 27 * 18000 + 100)) + printf '{"days_history":21,"hour_profile":%s}\n' "$hp" >"$tmpdir/forecast.cache" + learned=$(TZ=UTC build_week_strip 50 "$now" "$ps" -1 "" 2 6) + [[ "${learned#*▮}" == *$'\033[2m▯'* ]] + printf '{"days_history":5,"hour_profile":%s}\n' "$hp" >"$tmpdir/forecast.cache" + young=$(TZ=UTC build_week_strip 50 "$now" "$ps" -1 "" 2 6) + [[ "${young#*▮}" != *$'\033[2m'* ]] + rm -rf "$tmpdir" +} + @test "build_week_row: the fold token and the budget line count the same windows" { # one line, one arithmetic: the row folded `...▯(✕N)` and the budget # sentence beside it priced `~N✕5h left` from the same instant, and a