From 5dfba21b49a502ff13cfd1250418c52fec3c8061 Mon Sep 17 00:00:00 2001 From: "Founders' are 'found' to implement." Date: Wed, 4 Nov 2026 23:50:37 -0800 Subject: [PATCH 1/9] docs(studysprint): define parallel 0.6.0 product track --- .../STUDYSPRINT-0.6.0-PARALLEL-TRACK.md | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 docs/roadmap/STUDYSPRINT-0.6.0-PARALLEL-TRACK.md diff --git a/docs/roadmap/STUDYSPRINT-0.6.0-PARALLEL-TRACK.md b/docs/roadmap/STUDYSPRINT-0.6.0-PARALLEL-TRACK.md new file mode 100644 index 0000000..d82c773 --- /dev/null +++ b/docs/roadmap/STUDYSPRINT-0.6.0-PARALLEL-TRACK.md @@ -0,0 +1,159 @@ +# StudySprint 0.6.0 — Parallel Product Track + +Status: proposal + implementation contract +Baseline: production foundation 1.0.0; webApp 0.5.2; APK 0.5.2 +Branch: `experiment/0.6.0` +Rule: this track MUST NOT mutate `main`, production tags, or existing 0.5.x release assets. + +## Product objective + +Make StudySprint adapt to a student's real week rather than asking the student to adapt to a static plan. Preserve the product's defining trust boundary: no study plan is generated from unconfirmed or unverified course evidence. + +Primary user: a first-generation college student balancing classes, work, commuting, family responsibilities, and limited study time. + +## Version topology + +- `main`: protected production baseline; current foundation and independently versioned surfaces remain authoritative. +- `experiment/0.6.0`: integration branch for the next product line. +- `feature/0.6.0-intelligent-intake` +- `feature/0.6.0-adaptive-planner` +- `feature/0.6.0-learning-intelligence` +- `feature/0.6.0-student-command-center` +- `feature/0.6.0-study-companion` +- `release/0.6.0`: cut only after integration gates pass. + +The webApp, APK, and TUI keep independent SemVer lines as required by VERSIONING.md. A product milestone does not force all surfaces to ship simultaneously. + +## Product increments + +### 1. Intelligent course intake + +- Accept structured syllabus data first; add PDF/text extraction as a separate, testable adapter. +- Normalize course, term, weeks, learning outcomes, readings, assessments, due dates, and estimated workload into a canonical course model. +- Attach source location, extraction method, content hash, confidence, and verification state to each extracted fact. +- Show a trust receipt and require student confirmation before any extracted facts can influence a plan. +- Flag missing dates, ambiguous time zones, contradictory assessment dates, and duplicate course records. +- Never silently invent a due date, reading range, or workload estimate. + +Acceptance: fixture-based parser tests; malformed/ambiguous input tests; evidence provenance tests; explicit confirmation gate tests; no-network offline path remains supported. + +### 2. Adaptive study planner + +- Build weekly plans from confirmed course evidence, available time, fixed commitments, due dates, and student-selected session length. +- Support a manual time budget and protected unavailable periods. +- Rebalance missed sessions without guilt language; retain a visible explanation of what moved and why. +- Prioritize imminent assessments and overdue high-value retrieval practice without deleting the student's other commitments. +- Keep manual overrides stable until the student asks to re-plan. + +Acceptance: deterministic planner fixtures; no-overlap scheduling; workload-cap tests; missed-session recovery tests; stable manual override tests; explainable prioritization receipts. + +### 3. Learning intelligence + +- Use existing FSRS/SM-2 review history as the primary evidence for review scheduling. +- Add topic-level mastery only when backed by explicit answers, confidence ratings, or review events. +- Distinguish observed performance from inferred mastery; display sample size and recency. +- Recommend the next review using retrieval difficulty, recency, assessment proximity, and available time. +- Provide an explain-back prompt and short formative checks; do not label a student as having mastered a topic from a single response. + +Acceptance: scheduler parity with current core; reproducible mastery aggregation; low-sample warnings; explainable recommendation tests; no ungrounded mastery claims. + +### 4. Student command center + +- A compact Today surface: next best action, available study time, deadlines, and a one-tap recovery action. +- A week view that shows workload distribution and collisions across courses. +- Course switcher and current course context stay visible. +- Accessible keyboard navigation, responsive mobile layout, reduced-motion support, and offline-readable state. +- Progress reporting emphasizes recovered consistency and useful work, not punitive streak loss. + +Acceptance: responsive viewport tests; keyboard and screen-reader checks; offline shell regression; multi-course isolation tests; persisted state migration tests. + +### 5. Grounded study companion + +- Tutor only from confirmed course evidence, selected readings, and the student's own review history. +- Every course-specific answer carries a source reference or clearly says when the available evidence is insufficient. +- Offer Socratic hints, worked examples, recall questions, and explain-back checks. +- Never fabricate quotations, page numbers, syllabus requirements, or instructor intent. +- Keep external model/network use opt-in and fail closed when unavailable; the existing offline product remains usable. + +Acceptance: source-grounding tests; abstention tests; prompt-injection/content-boundary tests; privacy and network opt-in tests; offline fallback tests. + +## Shared domain model + +Keep core business logic in the Python domain layer; surfaces are adapters. + +- `CourseEvidence`: course_id, source_id, source_hash, locator, evidence_kind, extracted_value, confidence, verification_status. +- `AvailabilityWindow`: start, end, timezone, recurrence, source, protected. +- `StudyTask`: course_id, topic_id, evidence_refs, estimated_minutes, due_at, priority_inputs, status. +- `StudySession`: task_refs, start, end, duration, plan_revision, origin, override_reason. +- `LearningSignal`: topic_id, signal_kind, observed_at, score_or_grade, sample_size, source_ref. +- `RecommendationReceipt`: recommendation_id, inputs, rationale, evidence_refs, generated_at, model_or_algorithm_version. + +Do not persist derived mastery as if it were observed evidence. Derived values must be reproducible from source signals. + +## Architecture and compatibility + +1. Keep existing trust receipt, evidence gate, course isolation, scheduler implementations, and offline mode as non-regression invariants. +2. Implement domain logic in core modules with typed inputs/outputs and deterministic tests. +3. Expose versioned adapters for webApp, APK (shared web bundle where applicable), and TUI. +4. Keep experimental API work behind an adapter; no new hosted dependency is required for the core experience. +5. Prefer local-first persistence. Any sync or AI provider integration must be opt-in, scoped, documented, and removable without data loss. +6. Migrations must be additive, idempotent, and tested against copies of v0.5.x data. + +## Delivery sequence + +| Stage | Work | Exit gate | +|---|---|---| +| 0 | Baseline audit + parity snapshot | Current main tests and parity matrix captured | +| 1 | Canonical evidence/task models | Schema + migration + fixtures green | +| 2 | Adaptive planner core | Deterministic scheduling and recovery suite green | +| 3 | Today/Week command center | Accessibility, mobile, offline, and course isolation gates green | +| 4 | Learning signals + explainability | Provenance, sample-size, and scheduler parity gates green | +| 5 | Intelligent intake adapters | Parse/ambiguity/confirmation gates green | +| 6 | Grounded companion adapter | Citation, abstention, privacy, and offline gates green | +| 7 | Surface parity + release candidate | Full tests, demo, parity matrix, artifact reproducibility | +| 8 | Independent surface cuts | Release-cut workflow per surface and version | + +## Release gates + +A 0.6.0 release candidate is blocked unless all are true: + +- Existing production 0.5.2 tags and assets remain unchanged. +- Full offline suite passes: `python3 -m pytest tests/`. +- `python3 demo.py` completes and its evidence snapshot is committed when decisions change. +- Surface parity matrix is updated with bound/partial/absent evidence, not marketing claims. +- Data migrations are tested from a representative 0.5.x fixture. +- WebApp, APK, and TUI have independent version decisions and artifact checksums. +- No secrets, API tokens, student records, or sensitive learning signals enter fixtures or telemetry. +- Release notes state known gaps, offline behavior, data handling, and rollback instructions. + +## Rollout and rollback + +- First ship behind opt-in feature flags on the experimental branch. +- Dogfood with synthetic courses and deterministic fixture data. +- Promote only after the release candidate gates pass. +- Keep the last stable data format readable; disable new planner/companion behavior without deleting student data. +- Roll back by disabling the feature flag and restoring the prior surface artifact; never rewrite or move a published tag. + +## Explicit non-goals for 0.6.0 + +- Replacing the existing evidence/trust gate. +- Making cloud AI mandatory. +- Claiming cognitive or clinical diagnoses. +- Ranking students or comparing them to other students. +- Automatically sharing student data with instructors, employers, or third parties. +- Breaking the current TUI, webApp, or APK version lines. + +## Initial implementation issues + +1. [P0] Capture v0.5.2 baseline tests, parity matrix, and data fixtures. +2. [P0] Add canonical evidence and availability models with migration tests. +3. [P0] Implement deterministic adaptive planner core and recovery receipts. +4. [P1] Build Today/Week command center on the existing webApp shell. +5. [P1] Add topic learning signals with evidence and sample-size display. +6. [P1] Add syllabus document intake adapter with explicit confirmation. +7. [P2] Add grounded companion behind opt-in provider adapter. +8. [P1] Port and verify each accepted capability across TUI/APK/webApp. +9. [P0] Run full release gates and cut each surface independently. + +--- +This document defines the parallel product track; it is not a release declaration. A release exists only after the repository's VERSIONING.md release process completes on the relevant release ref. From e735ab6b2c0b0f6fc48a74d95eca7a0b34f93d7b Mon Sep 17 00:00:00 2001 From: "Founders' are 'found' to implement." Date: Wed, 4 Nov 2026 23:51:30 -0800 Subject: [PATCH 2/9] feat(studysprint): add deterministic evidence-gated adaptive planner --- study_adaptive.py | 217 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 study_adaptive.py diff --git a/study_adaptive.py b/study_adaptive.py new file mode 100644 index 0000000..1428c8f --- /dev/null +++ b/study_adaptive.py @@ -0,0 +1,217 @@ +"""StudySprint 0.6.0 adaptive planner — deterministic, evidence-gated MVP. + +The planner is deliberately pure Python: no model/network dependency, no +implicit clock, and no mutation of source tasks. Availability timestamps must +be ISO-8601 values with an explicit UTC offset. Tasks are schedulable only +when their evidence is explicitly verified and confirmed. +""" +from __future__ import annotations + +from datetime import datetime, timedelta, timezone +from typing import Any + + +SCHEMA = "studysprint-adaptive-plan/v1" + + +def _aware(value: str, field: str) -> datetime: + try: + parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00")) + except (TypeError, ValueError) as exc: + raise ValueError(f"{field} must be an ISO-8601 timestamp") from exc + if parsed.tzinfo is None or parsed.utcoffset() is None: + raise ValueError(f"{field} must include an explicit UTC offset") + return parsed + + +def _deadline(task: dict[str, Any]) -> datetime | None: + value = task.get("due_at") + if value in (None, ""): + return None + return _aware(value, "task.due_at").astimezone(timezone.utc) + + +def _validate_windows(windows: list[dict]) -> list[dict]: + normalized = [] + for index, window in enumerate(windows or []): + start = _aware(window.get("start"), f"availability[{index}].start") + end = _aware(window.get("end"), f"availability[{index}].end") + if end <= start: + raise ValueError(f"availability[{index}] end must follow start") + normalized.append({"start": start, "end": end, + "source": window.get("source", "student")}) + normalized.sort(key=lambda item: item["start"].astimezone(timezone.utc)) + for previous, current in zip(normalized, normalized[1:]): + if current["start"].astimezone(timezone.utc) < previous["end"].astimezone(timezone.utc): + raise ValueError("availability windows must not overlap") + return normalized + + +def build_adaptive_plan( + tasks: list[dict], + availability: list[dict], + *, + session_minutes: int = 25, + daily_budget_minutes: int = 120, + break_minutes: int = 5, + now: str, +) -> dict: + """Allocate verified tasks into available windows without overbooking. + + Each task requires: task_id, estimated_minutes, evidence_refs, + evidence_verified=True, student_confirmed=True. Optional fields: + course, topic, due_at (ISO-8601 with offset), priority (1..5). + + `now` is explicit and offset-aware so the result is reproducible. + Sessions are emitted in UTC with the original local date retained. + """ + instant = _aware(now, "now").astimezone(timezone.utc) + if session_minutes < 1 or daily_budget_minutes < 1 or break_minutes < 0: + raise ValueError("session, budget, and break values must be positive") + if session_minutes > daily_budget_minutes: + raise ValueError("session_minutes cannot exceed daily_budget_minutes") + + windows = _validate_windows(availability) + eligible, excluded = [], [] + seen = set() + for index, raw in enumerate(tasks or []): + task = dict(raw) + task_id = str(task.get("task_id") or "").strip() + if not task_id: + raise ValueError(f"tasks[{index}] requires task_id") + if task_id in seen: + raise ValueError(f"duplicate task_id: {task_id}") + seen.add(task_id) + estimate = task.get("estimated_minutes") + if not isinstance(estimate, int) or isinstance(estimate, bool) or estimate < 1: + raise ValueError(f"task {task_id} estimated_minutes must be a positive integer") + if task.get("status") in {"complete", "cancelled"}: + excluded.append({"task_id": task_id, "reason": "not-open"}) + continue + if task.get("evidence_verified") is not True or task.get("student_confirmed") is not True: + excluded.append({"task_id": task_id, "reason": "evidence-not-confirmed"}) + continue + refs = task.get("evidence_refs") + if not isinstance(refs, list) or not refs or not all(str(ref).strip() for ref in refs): + excluded.append({"task_id": task_id, "reason": "missing-evidence-reference"}) + continue + priority = task.get("priority", 3) + if not isinstance(priority, int) or isinstance(priority, bool) or not 1 <= priority <= 5: + raise ValueError(f"task {task_id} priority must be an integer from 1 to 5") + task["_deadline"] = _deadline(task) + task["_remaining"] = estimate + task["_priority"] = priority + eligible.append(task) + + # Due date first, then priority, then stable id. No random or wall-clock input. + eligible.sort(key=lambda task: ( + task["_deadline"] or datetime.max.replace(tzinfo=timezone.utc), + -task["_priority"], str(task["task_id"]))) + + cursors = [window["start"] for window in windows] + sessions, daily_used = [], {} + unscheduled = [] + for task in eligible: + remaining = task["_remaining"] + task_sessions = 0 + for index, window in enumerate(windows): + cursor = cursors[index] + end = window["end"] + while remaining > 0: + utc_cursor = cursor.astimezone(timezone.utc) + utc_end = end.astimezone(timezone.utc) + if utc_cursor >= utc_end: + break + if utc_cursor < instant: + cursor = instant.astimezone(cursor.tzinfo) + utc_cursor = cursor.astimezone(timezone.utc) + local_day = cursor.date().isoformat() + budget_left = daily_budget_minutes - daily_used.get(local_day, 0) + if budget_left <= 0: + break + available = int((utc_end - utc_cursor).total_seconds() // 60) + if available <= 0: + break + duration = min(session_minutes, remaining, budget_left, available) + if duration <= 0: + break + finish = cursor + timedelta(minutes=duration) + deadline = task["_deadline"] + if deadline and finish.astimezone(timezone.utc) > deadline: + break + sessions.append({ + "session_id": f"{task['task_id']}:{task_sessions + 1}", + "task_id": task["task_id"], + "course": task.get("course"), + "topic": task.get("topic"), + "start": cursor.astimezone(timezone.utc).isoformat().replace("+00:00", "Z"), + "end": finish.astimezone(timezone.utc).isoformat().replace("+00:00", "Z"), + "local_date": local_day, + "duration_minutes": duration, + "evidence_refs": list(task["evidence_refs"]), + "rationale": { + "priority": task["_priority"], + "due_at": task.get("due_at"), + "availability_source": window["source"], + }, + }) + daily_used[local_day] = daily_used.get(local_day, 0) + duration + remaining -= duration + task_sessions += 1 + cursor = finish + timedelta(minutes=break_minutes) + cursors[index] = cursor + if remaining <= 0: + break + if remaining > 0: + unscheduled.append({ + "task_id": task["task_id"], + "remaining_minutes": remaining, + "reason": "insufficient-available-time-or-deadline-window", + }) + + sessions.sort(key=lambda item: item["start"]) + return { + "schema": SCHEMA, + "as_of": instant.isoformat().replace("+00:00", "Z"), + "sessions": sessions, + "scheduled_minutes": sum(s["duration_minutes"] for s in sessions), + "unscheduled": unscheduled, + "excluded": excluded, + "daily_minutes": dict(sorted(daily_used.items())), + "constraints": { + "session_minutes": session_minutes, + "daily_budget_minutes": daily_budget_minutes, + "break_minutes": break_minutes, + "availability_windows": len(windows), + }, + } + + +def replan_after_missed( + tasks: list[dict], + availability: list[dict], + *, + missed_task_ids: list[str], + now: str, + **planner_options, +) -> dict: + """Rebuild a plan with missed tasks returned to the open queue. + + The caller supplies current availability and the remaining estimates; + completed tasks must remain marked complete. A receipt records exactly + which task ids were reintroduced. + """ + missed = {str(task_id) for task_id in (missed_task_ids or [])} + updated = [] + for raw in tasks or []: + task = dict(raw) + if str(task.get("task_id")) in missed and task.get("status") != "complete": + task["status"] = "open" + updated.append(task) + result = build_adaptive_plan( + updated, availability, now=now, **planner_options) + result["recovery"] = { + "missed_task_ids": sorted(missed), + "policy": "recompute-from-current-availability", + } + return result From 6a3b3ba812e08a6e1b32ff2273ec1e8165aa43c0 Mon Sep 17 00:00:00 2001 From: "Founders' are 'found' to implement." Date: Wed, 4 Nov 2026 23:51:32 -0800 Subject: [PATCH 3/9] test(studysprint): cover adaptive planner constraints and recovery --- tests/test_study_adaptive.py | 85 ++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 tests/test_study_adaptive.py diff --git a/tests/test_study_adaptive.py b/tests/test_study_adaptive.py new file mode 100644 index 0000000..9c63dd0 --- /dev/null +++ b/tests/test_study_adaptive.py @@ -0,0 +1,85 @@ +"""Deterministic tests for the StudySprint 0.6.0 adaptive planner.""" +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +import pytest # noqa: E402 +from study_adaptive import build_adaptive_plan, replan_after_missed # noqa: E402 + +NOW = "2026-11-04T08:00:00Z" +WINDOWS = [ + {"start": "2026-11-04T09:00:00-08:00", + "end": "2026-11-04T11:00:00-08:00", "source": "student-calendar"}, + {"start": "2026-11-05T09:00:00-08:00", + "end": "2026-11-05T10:30:00-08:00", "source": "student-calendar"}, +] +TASKS = [ + {"task_id": "bio-quiz", "course": "BIO 101", "topic": "cells", + "estimated_minutes": 50, "due_at": "2026-11-05T17:00:00-08:00", + "priority": 4, "evidence_refs": ["syllabus:bio:w2"], + "evidence_verified": True, "student_confirmed": True}, + {"task_id": "history-reading", "course": "HIST 210", "topic": "WWI", + "estimated_minutes": 25, "priority": 2, + "evidence_refs": ["syllabus:hist:w1"], + "evidence_verified": True, "student_confirmed": True}, +] + + +def test_plan_is_deterministic_and_evidence_carrying(): + a = build_adaptive_plan(TASKS, WINDOWS, now=NOW) + b = build_adaptive_plan(TASKS, WINDOWS, now=NOW) + assert a == b + assert a["schema"] == "studysprint-adaptive-plan/v1" + assert all(s["evidence_refs"] for s in a["sessions"]) + assert a["scheduled_minutes"] == 75 + + +def test_due_first_and_session_chunks_respect_budgets(): + plan = build_adaptive_plan(TASKS, WINDOWS, now=NOW, + session_minutes=25, daily_budget_minutes=50) + assert plan["sessions"][0]["task_id"] == "bio-quiz" + assert all(s["duration_minutes"] <= 25 for s in plan["sessions"]) + assert all(v <= 50 for v in plan["daily_minutes"].values()) + assert sum(1 for s in plan["sessions"] if s["task_id"] == "bio-quiz") == 2 + + +def test_unconfirmed_or_unreferenced_tasks_never_enter_plan(): + tasks = [ + {**TASKS[0], "task_id": "unconfirmed", "student_confirmed": False}, + {**TASKS[0], "task_id": "unverified", "evidence_verified": False}, + {**TASKS[0], "task_id": "no-ref", "evidence_refs": []}, + ] + plan = build_adaptive_plan(tasks, WINDOWS, now=NOW) + assert plan["sessions"] == [] + assert {x["reason"] for x in plan["excluded"]} == { + "evidence-not-confirmed", "missing-evidence-reference"} + + +def test_insufficient_time_is_explicit_not_silently_dropped(): + plan = build_adaptive_plan( + [{**TASKS[0], "estimated_minutes": 300}], + WINDOWS, now=NOW, daily_budget_minutes=60) + assert plan["scheduled_minutes"] < 300 + assert plan["unscheduled"][0]["remaining_minutes"] > 0 + assert plan["unscheduled"][0]["reason"] + + +def test_rejects_naive_or_overlapping_availability(): + with pytest.raises(ValueError, match="explicit UTC offset"): + build_adaptive_plan(TASKS, [{"start": "2026-11-04T09:00:00", + "end": "2026-11-04T10:00:00"}], now=NOW) + with pytest.raises(ValueError, match="must not overlap"): + build_adaptive_plan(TASKS, [ + WINDOWS[0], + {"start": "2026-11-04T10:00:00-08:00", + "end": "2026-11-04T12:00:00-08:00"}], now=NOW) + + +def test_replan_restores_missed_open_task_and_records_policy(): + plan = replan_after_missed( + TASKS, WINDOWS, missed_task_ids=["history-reading"], now=NOW) + assert plan["recovery"]["missed_task_ids"] == ["history-reading"] + assert plan["recovery"]["policy"] == "recompute-from-current-availability" + assert "history-reading" in {s["task_id"] for s in plan["sessions"]} From fad2dcc0e243511e6129aebce52976033cd3aea0 Mon Sep 17 00:00:00 2001 From: "Founders' are 'found' to implement." Date: Thu, 5 Nov 2026 00:01:31 -0800 Subject: [PATCH 4/9] ci(studysprint): validate parallel 0.6.0 branch --- .github/workflows/studysprint-06-ci.yml | 66 +++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .github/workflows/studysprint-06-ci.yml diff --git a/.github/workflows/studysprint-06-ci.yml b/.github/workflows/studysprint-06-ci.yml new file mode 100644 index 0000000..92f7587 --- /dev/null +++ b/.github/workflows/studysprint-06-ci.yml @@ -0,0 +1,66 @@ +name: StudySprint 0.6.0 branch validation + +on: + push: + branches: + - experiment/0.6.0 + pull_request: + branches: + - main + paths: + - "study_adaptive.py" + - "tests/test_study_adaptive.py" + - ".github/workflows/studysprint-06-ci.yml" + +permissions: + contents: read + +concurrency: + group: studysprint-06-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements.txt pytest + - name: Run complete test suite + run: python -m pytest -q tests/ + - name: Run offline product demo + run: python demo.py + - name: Validate adaptive planner public API + run: | + python - <<'PY' + from study_adaptive import build_adaptive_plan + result = build_adaptive_plan( + [{ + "task_id": "release-smoke", + "estimated_minutes": 25, + "evidence_refs": ["fixture:confirmed"], + "evidence_verified": True, + "student_confirmed": True, + "priority": 5, + }], + [{ + "start": "2026-11-06T09:00:00-08:00", + "end": "2026-11-06T10:00:00-08:00", + "source": "smoke-fixture", + }], + now="2026-11-06T08:00:00-08:00", + ) + assert result["scheduled_minutes"] == 25, result + assert len(result["sessions"]) == 1, result + assert result["sessions"][0]["evidence_refs"] == ["fixture:confirmed"] + print("adaptive planner smoke test passed") + PY From 62ca58507041d523b244659876067e71592e07eb Mon Sep 17 00:00:00 2001 From: "Founders' are 'found' to implement." Date: Thu, 5 Nov 2026 00:02:25 -0800 Subject: [PATCH 5/9] test(studysprint): align web surface assertions with v0.5.2 --- tests/test_iteration2_memory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_iteration2_memory.py b/tests/test_iteration2_memory.py index 5931a39..043d7dd 100644 --- a/tests/test_iteration2_memory.py +++ b/tests/test_iteration2_memory.py @@ -197,7 +197,7 @@ def test_web_surface_ships_memory_view_and_leitner(): assert f"function {fn}" in js, fn state = json.loads( (APP_ROOT / "web" / "data" / "state.json").read_text()) - assert state["surface_version"] == "0.5.1" + assert state["surface_version"] == "0.5.2" assert state["memory"]["palace"]["placements"] From 79ff4e320cb01f1df57ed0284231ed9c696ece72 Mon Sep 17 00:00:00 2001 From: "Founders' are 'found' to implement." Date: Thu, 5 Nov 2026 00:02:27 -0800 Subject: [PATCH 6/9] test(studysprint): align web surface assertions with v0.5.2 --- tests/test_iteration3_courses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_iteration3_courses.py b/tests/test_iteration3_courses.py index dce2f4f..0749855 100644 --- a/tests/test_iteration3_courses.py +++ b/tests/test_iteration3_courses.py @@ -51,7 +51,7 @@ def _run_js(snippet: str): def test_web_state_carries_the_course_set(): from web.build_data import emit_state state = emit_state(APP_ROOT) - assert state["surface_version"] == "0.5.1" + assert state["surface_version"] == "0.5.2" names = [b["course"] for b in state["courses"]] assert names == [BIO, STAT] intake = state["courses_intake"] From 0592f93ec9692e7b679648bac8536809904fe917 Mon Sep 17 00:00:00 2001 From: "Founders' are 'found' to implement." Date: Thu, 5 Nov 2026 00:02:30 -0800 Subject: [PATCH 7/9] test(studysprint): align web surface assertions with v0.5.2 --- tests/test_iteration4_hardening_terms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_iteration4_hardening_terms.py b/tests/test_iteration4_hardening_terms.py index 26d1a48..787bc30 100644 --- a/tests/test_iteration4_hardening_terms.py +++ b/tests/test_iteration4_hardening_terms.py @@ -65,7 +65,7 @@ def test_single_file_ingest_reports_the_three_counts(): def test_state_carries_book_and_version(): state = json.loads((WEB / "data" / "state.json").read_text()) - assert state["surface_version"] == "0.5.1" + assert state["surface_version"] == "0.5.2" assert state["book"]["counts"] == EXPECTED_COUNTS From a0d9bde80be113642b7bc7de5eb33ea51616cb05 Mon Sep 17 00:00:00 2001 From: "Founders' are 'found' to implement." Date: Thu, 5 Nov 2026 00:02:34 -0800 Subject: [PATCH 8/9] test(studysprint): align web surface assertions with v0.5.2 --- tests/test_web_surface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_web_surface.py b/tests/test_web_surface.py index 7a9fe94..40e1c24 100644 --- a/tests/test_web_surface.py +++ b/tests/test_web_surface.py @@ -93,7 +93,7 @@ def test_state_carries_plan_start_and_surface_version(): from build_data import emit_state state = emit_state(ROOT) assert state["plan_start"] # #4214: Today advances from here - assert state["surface_version"] == "0.5.1" + assert state["surface_version"] == "0.5.2" def test_service_worker_cache_is_versioned_and_state_network_first(): From 9a74b7c882f6fa37eb9a76811f36933a568ed27f Mon Sep 17 00:00:00 2001 From: "Founders' are 'found' to implement." Date: Thu, 5 Nov 2026 00:02:45 -0800 Subject: [PATCH 9/9] ci(studysprint): install optional TUI test dependency --- .github/workflows/studysprint-06-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/studysprint-06-ci.yml b/.github/workflows/studysprint-06-ci.yml index 92f7587..5a2579c 100644 --- a/.github/workflows/studysprint-06-ci.yml +++ b/.github/workflows/studysprint-06-ci.yml @@ -34,7 +34,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install -r requirements.txt pytest + python -m pip install -r requirements.txt pytest textual - name: Run complete test suite run: python -m pytest -q tests/ - name: Run offline product demo