domain/reflection_hierarchy — the nested reflection calendar¶
backend/src/domain/reflection_hierarchy.py (447 lines). When a
reflection falls due and what feeds it. The APTITUDE curriculum is a
nested calendar: ten stages pair into five components (stages 2n-1
and 2n), components split into two tiers (first six stages, then
four), and every layer closes with an invitation to reflect. "The
precedence is fixed: program beats tier beats component beats stage
beats a plain week. A user who reaches day seven of week eighteen is
not asked for four reflections; they are offered the single tier
reflection that subsumes the rest" (reflection_hierarchy.py:1-11).
Pure — no database (reflection_hierarchy.py:13-14).
Two driving ideas (reflection_hierarchy.py:16-34): everything derives
from STAGE_DURATIONS_DAYS (so _WEEKS_PER_STAGE = (3,)*8 + (6, 6) and
TOTAL_PROGRAM_WEEKS = 36 are computed, not hand-written,
reflection_hierarchy.py:56-63) — except the six-then-four tier split,
which "is a curriculum design decision — it is NOT derivable from
STAGE_DURATIONS_DAYS", hence the named constant
_TIER_ONE_LAST_STAGE = 6 (reflection_hierarchy.py:75-80); and uniform
recursion in source resolution with no boundary special-cases.
Keys and levels¶
Scope keys are "c{cycle}:{token}" with token ∈ prog, w<week>,
s<stage>, p<component>, t<tier>; the cycle prefix "isolates repeat
runs of the program: a reflection from cycle one never satisfies a
cycle-two lookup" (reflection_hierarchy.py:36-39,86).
ReflectionLevel = week < stage < component < tier < program
(reflection_hierarchy.py:89-101); _parse_key validates shape and
range (a stray s11 or t3 is rejected, reflection_hierarchy.py:124-131,285-298).
due_reflection(anchor, now=None, cycle=1) -> DueReflection | None¶
(backend/src/domain/reflection_hierarchy.py:247-267):
reference = now if now is not None else datetime.now(UTC)
elapsed = elapsed_days(anchor, reference)
if elapsed % _DAYS_PER_WEEK + 1 != _DAYS_PER_WEEK:
return None
week = elapsed // _DAYS_PER_WEEK + 1
if week > TOTAL_PROGRAM_WEEKS:
return None
level, token = _closing_level(week)
return DueReflection(level=level, key=f"c{cycle}:{token}", week=week)
Reflections come due only on the seventh day of a program week;
other days, clock skew, and weeks past 36 yield None. _closing_level
picks the widest closing layer: week 36 → PROGRAM; a week that ends no
stage → plain WEEK; a stage end escalates to TIER if the stage is 6
or 10, else COMPONENT if the stage is even, else STAGE
(reflection_hierarchy.py:219-244).
Worked precedence table (cycle 1):
| Week | Closes | Due | Key |
|---|---|---|---|
| 2 | nothing | week |
c1:w2 |
| 3 | stage 1 (odd) | stage |
c1:s1 |
| 6 | stage 2 (even → component 1) | component |
c1:p1 |
| 18 | stage 6 (tier-one cap) | tier |
c1:t1 |
| 36 | everything | program |
c1:prog |
scope_weeks(level, key) -> range¶
The inclusive program-week span a reflection covers, as
range(start, end+1); the level argument must agree with the key's
token "so callers cannot silently scope the wrong span"
(reflection_hierarchy.py:314-325). Spans: week w5 → weeks 5..5;
stage s9 → 25..30; component p1 → 1..6; tier t2 → 19..36; prog
→ 1..36 (reflection_hierarchy.py:177-200,301-311).
resolve_sources(level, key, existing, entries) -> list[SourceItem]¶
"What raw material feeds this reflection?" — a top-down walk: an
existing child reflection stands in for its whole span; otherwise recurse
(program → tiers → components → stage pairs → weeks); a WEEK bottoms
out in its own weekly reflection or that week's raw daily entries sorted
by (date, id) (reflection_hierarchy.py:341-447). The uniformity
argument (reflection_hierarchy.py:29-34): a stage's final week can
never carry its own weekly reflection — that day resolved to the STAGE
(or higher) layer instead — so the final week "simply recurses to its
dailies like any other gap" and no boundary special-casing is needed;
ascending child order yields chronological output with each reflection
ahead of the raw entries it summarizes. Reflections match by full
c{cycle}: key; entries carry no cycle, so the caller must pass only
this cycle's entries (reflection_hierarchy.py:437-440).
Example: resolving c1:p1 (component 1, weeks 1-6) where c1:s1
exists but stage 2 has no reflections and weeks 4-6 have only dailies →
[reflection(c1:s1)] + entries(week 4) + entries(week 5) + entries(week 6).
Consumers¶
The journal reflections endpoints persist due reflections as
JournalEntry rows tagged hierarchical_reflection with the
(reflection_level, reflection_scope_key) pair and a per-(user, scope)
live-row uniqueness — see api/reflections and
data-model/journal-reflection.
models/journal_entry.py derives its reflection_level CHECK from this
module's ReflectionLevel (backend/src/models/journal_entry.py:92-98).
Grounded in adepthood@fbc529d, 2026-07-31.