Skip to content

feat: study briefing — cohesive loop connecting content, review, and sessions - #4

Merged
NetDevAutomate merged 2 commits into
mainfrom
feat/study-briefing-loop
Apr 5, 2026
Merged

feat: study briefing — cohesive loop connecting content, review, and sessions#4
NetDevAutomate merged 2 commits into
mainfrom
feat/study-briefing-loop

Conversation

@NetDevAutomate

Copy link
Copy Markdown
Owner

Summary

Connects the three disconnected systems (content pipeline, review, study sessions) into a single feedback loop:

  • Topic resolverstudyctl study "Python Decorators" resolves to TopicConfig(name="Python") via 4-tier cascade (exact name → substring → tag → difflib fuzzy). Interactive picker on ambiguity.
  • Study briefing — review stats (due cards, struggling, mastered), content inventory (flashcards, chapters), and content gap hints injected into agent persona at session start
  • Post-session flashcards — wins/insights auto-generate flashcard JSON with hash dedup, picked up by studyctl web review on next load
  • Performance fixget_course_stats() correlated subquery replaced with CTE + window function (O(n²) → O(n))

Test plan

  • 602 tests passed (+60 new), 7 skipped
  • 24 topic resolver tests — exact, substring, tag, fuzzy, Unicode casefold, precedence, edge cases
  • 20 briefing logic tests — format sections, degradation, backlog, gatherer mocking
  • 16 flashcard writer tests — wins generate cards, dedup, skip short notes, JSON structure
  • All pre-commit hooks pass (ruff, pyright, detect-secrets)
  • Pre-push pytest passes

🤖 Generated with Claude Code

NetDevAutomate and others added 2 commits April 5, 2026 13:14
- session.py: _render_activity_feed() returned wrapper div causing
  duplicate id="activity-feed" in DOM when HTMX SSE swapped innerHTML.
  Now returns only inner content.
- services/review.py: restore flashcard_count key (was renamed to
  card_count breaking frontend app.js and web API tests)
- test_mcp_tools.py: match restored flashcard_count key
- test_e2e_session_demo.py: 8-test E2E suite covering full session
  lifecycle (dashboard, SSE feed, terminal proxy, pop-out, WS relay,
  LAN auth, cleanup). Doubles as recordable demo via --video=on.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…sessions

Topic resolver (logic/topic_resolver.py):
- Pure function: resolve_topic() -> ResolveResult with 4-tier cascade
  (exact name → name substring → tag match → difflib fuzzy)
- Uses str.casefold() for Unicode-correct matching
- Interactive picker in CLI shell when ambiguous (click.prompt)
- 24 tests, zero mocking

Study briefing (logic/briefing_logic.py + cli/_study.py):
- BriefingData + ReviewContext + ContentContext dataclasses (FCIS boundary)
- format_study_briefing() pure formatter → markdown for agent persona
- Shell-side gatherers with independent fault tolerance
- Struggling count derived from get_due(), not get_wrong()
- topic_slug + topic_config_name stored in session state

Post-session flashcard generation (services/flashcard_writer.py):
- write_session_flashcards() generates JSON from session wins/insights
- Question-form fronts, hash dedup, 15-char minimum filter
- Hooked into cleanup.py with logger.warning on failure

Performance fix:
- get_course_stats() correlated subquery → CTE + ROW_NUMBER()

Tests: 602 passed (+60 new), 7 skipped, lint clean

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 5, 2026 13:04
@NetDevAutomate
NetDevAutomate merged commit 2e1d7b9 into main Apr 5, 2026
6 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Connects content, review, and study sessions into a tighter feedback loop by resolving free-text topics to configured courses, injecting a structured “study briefing” at session start, and generating flashcards from session wins/insights on session end (plus a review DB stats performance improvement and web session feed rendering fix).

Changes:

  • Added a pure topic resolver (exact/substring/tag/fuzzy) and wired it into studyctl study with an interactive disambiguation picker.
  • Added study briefing formatting logic and session-start data gatherers to inject review + content inventory context into the agent persona.
  • Added post-session flashcard writer + session-end hook to emit deduped flashcard JSON; improved get_course_stats() mastered calculation via CTE/window function.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/studyctl/tests/test_web_session.py Updates assertions to match activity-feed fragment now being inner HTML only.
packages/studyctl/tests/test_topic_resolver.py New unit tests for resolver cascade behavior, precedence, and Unicode casefold handling.
packages/studyctl/tests/test_mcp_tools.py Updates expected key name to flashcard_count.
packages/studyctl/tests/test_flashcard_writer.py New tests for win/insight flashcard generation, dedup, and JSON structure.
packages/studyctl/tests/test_e2e_session_demo.py New Playwright-based E2E/demo test covering web UI + ttyd + WS proxy + LAN auth.
packages/studyctl/tests/test_briefing_logic.py New tests for briefing formatting and degradation behavior; light integration coverage via mocks.
packages/studyctl/src/studyctl/web/routes/session.py Fixes SSE activity-feed rendering to avoid duplicating the swap target element/id.
packages/studyctl/src/studyctl/session/cleanup.py Hooks session-end cleanup to generate flashcards from wins/insights.
packages/studyctl/src/studyctl/services/review.py Renames card_countflashcard_count in course summaries.
packages/studyctl/src/studyctl/services/flashcard_writer.py New flashcard writer service for post-session JSON generation with hash-based dedup.
packages/studyctl/src/studyctl/review_db.py Replaces correlated subquery with CTE + window function for mastered stats.
packages/studyctl/src/studyctl/logic/topic_resolver.py New pure resolver implementing the 4-tier match cascade.
packages/studyctl/src/studyctl/logic/briefing_logic.py New pure briefing formatter and data structures.
packages/studyctl/src/studyctl/cli/_study.py Wires topic resolution + briefing assembly into studyctl study startup flow.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 47 to 51
{
"name": name,
"card_count": fc_count,
"flashcard_count": fc_count,
"quiz_count": quiz_count,
"due_count": due,

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

list_course_summaries() now returns the key flashcard_count, but the function docstring still documents the return shape using card_count. This is likely to confuse API consumers and future maintainers—please update the docstring (and any related docs) to match the renamed field.

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +70
def write_session_flashcards(
content_base: Path,
topic_slug: str,
session_id: str,
topic_entries: list[TopicEntry],
) -> int:

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

session_id is documented as being used for source attribution, but it is currently unused and the generated card source field only includes the date. Either remove session_id or include it in the written payload/filename/source so the provenance matches the API contract.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +60
CONFIG_DIR = Path.home() / ".config" / "studyctl"
STATE_FILE = CONFIG_DIR / "session-state.json"
TOPICS_FILE = CONFIG_DIR / "session-topics.md"
PARKING_FILE = CONFIG_DIR / "session-parking.md"
ONELINE_FILE = CONFIG_DIR / "session-oneline.txt"
SESSIONS_DIR = CONFIG_DIR / "sessions"

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test hard-codes its working files under Path.home()/.config/studyctl. That means running the test locally can read/overwrite a developer’s real studyctl state and requires aggressive cleanup. Consider deriving these paths from an isolated temp HOME/XDG_CONFIG_HOME set in a fixture (and avoid module-level constants so the env override takes effect).

Suggested change
CONFIG_DIR = Path.home() / ".config" / "studyctl"
STATE_FILE = CONFIG_DIR / "session-state.json"
TOPICS_FILE = CONFIG_DIR / "session-topics.md"
PARKING_FILE = CONFIG_DIR / "session-parking.md"
ONELINE_FILE = CONFIG_DIR / "session-oneline.txt"
SESSIONS_DIR = CONFIG_DIR / "sessions"
def _config_dir() -> Path:
xdg_config_home = os.environ.get("XDG_CONFIG_HOME")
if xdg_config_home:
return Path(xdg_config_home) / "studyctl"
return Path.home() / ".config" / "studyctl"
def _state_file() -> Path:
return _config_dir() / "session-state.json"
def _topics_file() -> Path:
return _config_dir() / "session-topics.md"
def _parking_file() -> Path:
return _config_dir() / "session-parking.md"
def _oneline_file() -> Path:
return _config_dir() / "session-oneline.txt"
def _sessions_dir() -> Path:
return _config_dir() / "sessions"

Copilot uses AI. Check for mistakes.
Comment on lines +116 to +121
# Kill study tmux sessions
result = _tmux("list-sessions", "-F", "#{session_name}")
if result.returncode == 0:
for name in result.stdout.strip().splitlines():
if name.startswith("study-") or name.startswith("e2e-"):
_tmux("kill-session", "-t", name)

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_cleanup_all() kills any tmux session whose name starts with study- or e2e-, which can terminate unrelated user sessions if this test is run on a dev machine. Please scope cleanup to only the session(s) created by this test (e.g., exact study-e2e-demo... prefix or PID-based ownership) rather than broad prefixes.

Copilot uses AI. Check for mistakes.
Comment on lines +123 to +130
for pattern in (
"studyctl.tui.sidebar",
"mock-agent",
f"ttyd.*{TTYD_PORT}",
f"studyctl.*{WEB_PORT}",
):
with contextlib.suppress(Exception):
subprocess.run(["pkill", "-f", pattern], capture_output=True, check=False)

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using pkill -f with broad patterns (e.g. studyctl.*{WEB_PORT}) is risky in a local environment because it can match and kill unrelated processes. Prefer tracking PIDs you started (from Popen) and terminating those, or narrowing patterns with unique test identifiers.

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +66
WEB_PORT = 18567
TTYD_PORT = 17681
LAN_PASSWORD = "e2e-test-pass" # pragma: allowlist secret
STUDYCTL = f"uv run --project {PROJECT_DIR} studyctl"

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The E2E test uses fixed ports (18567 / 17681), which can make the test flaky if those ports are already in use on a developer machine or CI runner. Consider dynamically selecting free ports at runtime (and writing them into the temp config) to reduce collisions.

Copilot uses AI. Check for mistakes.
@NetDevAutomate
NetDevAutomate deleted the feat/study-briefing-loop branch May 4, 2026 22:05
NetDevAutomate added a commit that referenced this pull request Sep 3, 2026
Amendment #4 at the top of the plan. Closes out yesterday's TodoWrite
items PR-B-1 through PR-B-4 and PR-B-9; next handoff is §1.5 wiring.

Captures:
- Four sub-PRs shipped this session with commit SHAs
  (0e26d8c, 4f418ff, c35224f) plus the rename-hygiene commit b15fb5c
  that unblocked CI.
- Two real production bugs that PR-B-4's TDD flushed out of PR-B-2's
  shipped code: the process-global SIGCHLD registration that broke
  per-loop semantics, and the double-emit in cancel() that silently
  downgraded user cancellations to reason="exit". Both fixed in
  c35224f; documented here so §1.5 readers don't hit them cold.
- Test-suite state: 1974 passed, 145 deselected, 0 failures.
  +928 tests over Phase 1 start, driven mostly by the rename fixes
  repairing harness scripts that had been silently skipping earlier.
- Open follow-ups scoped for the next PR: §1.5 FastAPI wiring,
  §1.6-1.9 xterm.js UI, Origin-guard blocker B1 (lands with §1.5),
  and eventual removal of the legacy session_runtime/ module once
  the web route no longer reaches into it.

No code change — documentation only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NetDevAutomate added a commit that referenced this pull request Sep 5, 2026
feat: study briefing — cohesive loop connecting content, review, and sessions
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.

2 participants