diff --git a/.github/scripts/checkpoint.py b/.github/scripts/checkpoint.py deleted file mode 100644 index 2a7081f..0000000 --- a/.github/scripts/checkpoint.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python3 -"""Append a row to ``docs/checkpoints.md`` for major project iterations. - -Run by ``.github/workflows/sync-checkpoints.yaml``. Two paths: - -* **pull_request** closed + merged that closes a ``phase``-labeled issue → a row built from - the PR (branch = head ref, checkpoint = PR title). -* **push** whose head commit message is ``#-checkpoint: `` → a manual checkpoint - row built from that commit (checkpoint = ````). - -Columns: ``Date | Branch | Issue | Checkpoint``. The Issue cell is the leading number of the -branch name, which must be the GitHub issue number (e.g. ``1-foundations`` → ``1``), or empty -when the branch starts with no number. Appends are idempotent: an identical row is not added twice. -""" - -from __future__ import annotations - -import json -import os -import re -import subprocess -import sys -from datetime import date -from pathlib import Path - -DEFAULT_DOC = Path(__file__).resolve().parents[2] / "docs" / "checkpoints.md" -DOC = Path(os.environ.get("CHECKPOINTS_FILE", str(DEFAULT_DOC))) -# Checkpoint commits look like `#1-checkpoint: drafted store schema` (the leading `#` is -# optional; `#` auto-links the issue on GitHub). -CHECKPOINT_RE = re.compile(r"^#?\d+-checkpoint:\s*(?P.*)$", re.IGNORECASE) - - -def gh(*args: str) -> str: - """Run a ``gh`` command and return stdout.""" - return subprocess.run(["gh", *args], capture_output=True, text=True, check=True).stdout - - -def issue_from_branch(branch: str) -> str: - """Leading issue number of the branch name, or '' (e.g. ``1-foundations`` → ``1``).""" - match = re.match(r"(\d+)", branch) - return match.group(1) if match else "" - - -def pr_closes_phase_issue(repo: str, pr_number: str) -> bool: - """True if the PR closes at least one issue carrying the ``phase`` label.""" - refs = json.loads( - gh("pr", "view", pr_number, "--repo", repo, "--json", "closingIssuesReferences") - )["closingIssuesReferences"] - for ref in refs: - labels = json.loads( - gh("issue", "view", str(ref["number"]), "--repo", repo, "--json", "labels") - )["labels"] - if any(label["name"] == "phase" for label in labels): - return True - return False - - -def esc(value: str) -> str: - """Flatten and escape text so it stays inside one table cell.""" - return value.replace("|", "\\|").replace("\n", " ").strip() - - -def append_row(when: str, branch: str, issue: str, checkpoint: str) -> None: - row = f"| {when} | `{branch}` | {issue} | {esc(checkpoint)} |" - text = DOC.read_text() - if row in text: - print("Checkpoint row already present; skipping.") - return - if not text.endswith("\n"): - text += "\n" - DOC.write_text(text + row + "\n") - print(f"Appended checkpoint: {row}") - - -def main() -> int: - event = os.environ["GITHUB_EVENT_NAME"] - repo = os.environ["GITHUB_REPOSITORY"] - today = date.today().isoformat() - - if event == "pull_request": - # The workflow only invokes this for merged PRs. - pr_number = os.environ["PR_NUMBER"] - branch = os.environ["PR_HEAD_REF"] - title = os.environ["PR_TITLE"] - if not pr_closes_phase_issue(repo, pr_number): - print("Merged PR closes no phase issue; nothing to record.") - return 0 - append_row(today, branch, issue_from_branch(branch), title) - return 0 - - if event == "push": - branch = os.environ["BRANCH"] - lines = os.environ.get("HEAD_COMMIT_MESSAGE", "").splitlines() - first = lines[0] if lines else "" - match = CHECKPOINT_RE.match(first.strip()) - if not match: - print("Head commit is not a `#-checkpoint:` commit; nothing to record.") - return 0 - description = match.group("text").strip() - append_row(today, branch, issue_from_branch(branch), description) - return 0 - - print(f"Unhandled event: {event}") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/.github/scripts/sync_issues.py b/.github/scripts/sync_issues.py deleted file mode 100644 index bfc7b44..0000000 --- a/.github/scripts/sync_issues.py +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env python3 -"""Regenerate ``docs/issues.md`` from this repo's GitHub issues. - -GitHub is the source of truth. This is run by ``.github/workflows/sync-issues.yaml`` on -issue events, and can also be run locally (requires ``gh`` to be authenticated). Issue -bodies are not mirrored — only number, title, state, labels, and the sub-issue parent. -""" - -from __future__ import annotations - -import json -import os -import subprocess -from pathlib import Path - -OUT = Path(__file__).resolve().parents[2] / "docs" / "issues.md" - - -def gh(*args: str) -> str: - """Run a ``gh`` command and return stdout.""" - return subprocess.run(["gh", *args], capture_output=True, text=True, check=True).stdout - - -def repo_slug() -> str: - """``OWNER/REPO`` from the Actions env, falling back to ``gh`` for local runs.""" - slug = os.environ.get("GITHUB_REPOSITORY") - if slug: - return slug - return gh("repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner").strip() - - -def esc(value: str) -> str: - """Escape table-breaking pipes and trim surrounding whitespace.""" - return value.replace("|", "\\|").strip() - - -def main() -> None: - repo = repo_slug() - issues = json.loads( - gh( - "issue", - "list", - "--repo", - repo, - "--state", - "all", - "--limit", - "500", - "--json", - "number,title,state,labels", - ) - ) - - # Build a child-number -> parent-number map from the sub-issue graph. - parent_of: dict[int, int] = {} - for issue in issues: - number = issue["number"] - try: - children = json.loads( - gh("api", f"repos/{repo}/issues/{number}/sub_issues", "--jq", "[.[].number]") - ) - except subprocess.CalledProcessError: - children = [] # issue has no sub-issues (endpoint 404s) - for child in children: - parent_of[child] = number - - rows = [] - for issue in sorted(issues, key=lambda i: i["number"]): - number = issue["number"] - title = esc(issue["title"]) - state = issue["state"].lower() - labels = esc(", ".join(sorted(label["name"] for label in issue["labels"]))) - parent = parent_of.get(number, "") - rows.append(f"| {number} | {title} | {state} | {labels} | {parent} |") - - preamble = ( - "# Issues\n" - "\n" - "> Auto-generated — do not edit by hand. This table mirrors the GitHub issues for\n" - f"> [`{repo}`](https://github.com/{repo}/issues) and is regenerated by the `sync-issues`\n" - "> workflow (`.github/workflows/sync-issues.yaml`) on every issue event. GitHub is the\n" - "> source of truth — manage issues there, not here. Issue bodies are not mirrored.\n" - "\n" - "| # | Title | State | Labels | Parent |\n" - "| --- | ----- | ----- | ------ | ------ |\n" - ) - OUT.write_text(preamble + "\n".join(rows) + "\n") - print(f"Wrote {len(rows)} issues to {OUT}") - - -if __name__ == "__main__": - main() diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 46017df..36a6cc2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,6 +2,7 @@ name: CI on: push: + branches: [main, dev] pull_request: # Builds and tests only — no deploy (ADR 0003: local single-user scope). @@ -13,7 +14,7 @@ jobs: - uses: actions/checkout@v5 - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v7 with: python-version: "3.12" enable-cache: true @@ -32,3 +33,23 @@ jobs: - name: Pytest run: uv run pytest + + docs: + name: Docs build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + python-version: "3.12" + enable-cache: true + + - name: Sync workspace + run: uv sync + + # Build-only (no deploy) so every push/PR is checked the same way as lint/mypy/pytest. + # -W turns Sphinx warnings into errors. Deployment to Pages stays in docs-deploy.yaml. + - name: Build docs + run: uv run sphinx-build -b html docs site -W diff --git a/.github/workflows/docs-deploy.yaml b/.github/workflows/docs-deploy.yaml new file mode 100644 index 0000000..52716ef --- /dev/null +++ b/.github/workflows/docs-deploy.yaml @@ -0,0 +1,54 @@ +name: Deploy docs + +# Build the Sphinx site and publish it to GitHub Pages on push to main or dev. Kept as its own +# workflow (not a job in ci.yaml) so CI stays build-and-test only, with no deploy. +on: + push: + branches: [main, dev] + +permissions: + contents: read + pages: write + id-token: write + +# Never cancel an in-flight Pages deploy; let queued runs finish in order. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + # Build and upload the Pages artifact in its own job so it is produced exactly once. If the deploy + # job below fails transiently and is re-run, it does not re-upload — which would otherwise leave two + # "github-pages" artifacts in the run and make actions/deploy-pages error. + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + python-version: "3.12" + enable-cache: true + + - name: Sync workspace + run: uv sync + + - name: Build site + run: uv run sphinx-build -b html docs site -W + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v5 + with: + path: site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/sync-checkpoints.yaml b/.github/workflows/sync-checkpoints.yaml deleted file mode 100644 index ac86370..0000000 --- a/.github/workflows/sync-checkpoints.yaml +++ /dev/null @@ -1,59 +0,0 @@ -name: Sync checkpoints log - -# Append a row to docs/checkpoints.md for major iterations: -# - a merged PR that closes a `phase`-labeled issue, or -# - a push whose head commit message is `#-checkpoint: `. -on: - pull_request: - types: [closed] - push: - -permissions: - contents: write - issues: read - pull-requests: read - -# Shared group with sync-issues so the two doc-sync workflows never push to the default -# branch at the same time (merging a phase PR fires both `issues` and `pull_request`). -concurrency: - group: repo-docs-sync - cancel-in-progress: false - -jobs: - checkpoint: - runs-on: ubuntu-latest - # Only act on a merged PR, or a push whose head commit looks like `#-checkpoint:`. - # `contains` is a coarse gate; checkpoint.py validates the exact `#-checkpoint:` shape. - if: >- - (github.event_name == 'pull_request' && github.event.pull_request.merged == true) || - (github.event_name == 'push' && contains(github.event.head_commit.message, '-checkpoint:')) - steps: - - uses: actions/checkout@v5 - with: - # Always commit the log to the default branch (dev), regardless of trigger. - # The row's Branch column still records the work branch (PR head / pushed ref). - ref: ${{ github.event.repository.default_branch }} - - - name: Append checkpoint row (if applicable) - env: - GH_TOKEN: ${{ github.token }} - GITHUB_EVENT_NAME: ${{ github.event_name }} - GITHUB_REPOSITORY: ${{ github.repository }} - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} - PR_TITLE: ${{ github.event.pull_request.title }} - BRANCH: ${{ github.ref_name }} - HEAD_COMMIT_MESSAGE: ${{ github.event.head_commit.message }} - run: python3 .github/scripts/checkpoint.py - - - name: Commit if changed - run: | - if git diff --quiet -- docs/checkpoints.md; then - echo "No checkpoint change." - exit 0 - fi - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add docs/checkpoints.md - git commit -m "chore: append checkpoint to docs/checkpoints.md [skip ci]" - git push diff --git a/.github/workflows/sync-issues.yaml b/.github/workflows/sync-issues.yaml deleted file mode 100644 index 9aa6384..0000000 --- a/.github/workflows/sync-issues.yaml +++ /dev/null @@ -1,43 +0,0 @@ -name: Sync issues mirror - -# Regenerate docs/issues.md from GitHub issues whenever issues change. GitHub is the -# source of truth; this keeps the in-repo mirror in step and commits it back. -on: - issues: - types: [opened, closed, reopened, edited, labeled, unlabeled, deleted, transferred] - -permissions: - contents: write - issues: read - -# Shared group with sync-checkpoints so the two doc-sync workflows never push to the -# default branch at the same time (e.g. when merging a phase PR fires both at once). -concurrency: - group: repo-docs-sync - cancel-in-progress: false - -jobs: - sync: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - with: - # Always operate on the default branch (dev) — that is where the mirror lives. - ref: ${{ github.event.repository.default_branch }} - - - name: Regenerate docs/issues.md - env: - GH_TOKEN: ${{ github.token }} - run: python3 .github/scripts/sync_issues.py - - - name: Commit if changed - run: | - if git diff --quiet -- docs/issues.md; then - echo "docs/issues.md already up to date." - exit 0 - fi - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add docs/issues.md - git commit -m "chore: sync docs/issues.md from GitHub issues [skip ci]" - git push diff --git a/.gitignore b/.gitignore index e7316eb..1f5c0d4 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,9 @@ pgdata/ *.log logs/ +# ---- Docs site (sphinx-build output) ---- +site/ + # ---- Editors / OS ---- .idea/ .vscode/ diff --git a/AGENTS.md b/AGENTS.md index e6a564d..74f3960 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,87 +1,89 @@ # Study Assistant — Agent Context -A local, single-user RAG study assistant over course materials (slides, papers, notes). It -answers questions with synthesis from Claude, grounded in retrieved sources, with citations back to -the exact source slide/page. Ingestion and retrieval run entirely on-machine; only generation calls -the Anthropic API. - -Start here: [`docs/architecture.md`](docs/architecture.md) is the full design. Locked decisions -live as ADRs in [`docs/decisions/`](docs/decisions/). Work is tracked as phases in -[`docs/issues.md`](docs/issues.md). - -## Resuming work (fresh agent) - -To pick up where the project was left off — whether between phases or mid-phase — orient in this -order. Docs (1–3) state intent; repo metadata (4) is the ground truth and is what reveals progress -made within or between phases. If they disagree, trust the tree and git, then update the docs. - -1. General context — read [`docs/architecture.md`](docs/architecture.md) (target design) and - [`docs/decisions/`](docs/decisions/) (ADRs — locked decisions and constraints) to understand the - system and why it is the way it is. -2. `README.md` → Status and [`docs/checkpoints.md`](docs/checkpoints.md) — the quickest read - of the current phase, plus an append-only table of every major iteration (branch, phase, - checkpoint) showing how the project got here. -3. [`docs/issues.md`](docs/issues.md) — the phase roadmap: closed phases are done, the - lowest-numbered open phase is next up, and child rows flag in-flight improvements. -4. Repo metadata (ground truth) — confirm the docs against reality; this also exposes work done - within or between phases: - - Branch name (e.g. `4-embeddings`) — usually names the active phase. - - `git log` plus staged/unstaged changes — the most recent real work and anything in - progress. - - Actual tree — which packages/modules exist versus the architecture's target layout. - - `uv sync` then `uv run pytest` — install the workspace and run the tests to see what is - actually implemented and passing. - -[`docs/checkpoints.md`](docs/checkpoints.md) is appended automatically — by the `sync-checkpoints` -workflow on phase-PR merges and `checkpoint:` commits (see `CONTRIBUTING.md`); do not hand-edit it. -At every major iteration still update the manual living docs so the next agent can resume cleanly: -the `README.md` Status blurb and the "Workspace layout" section below. Name -branches starting with their GitHub issue number (e.g. `1-foundations` for issue #1) so checkpoints -attribute correctly. +A local, single-user RAG study assistant over course materials (slides, papers, notes). It answers +questions with synthesis from Claude, grounded in retrieved sources, with citations back to the +exact source slide/page. Ingestion and retrieval run entirely on-machine; only generation calls the +Anthropic API. + +## Getting Oriented + +- Read [`docs/architecture.md`](docs/architecture.md), it is the full design. +- Read ADRs under [`docs/decisions/`](docs/decisions/), locked decisions live there. +- Read the [GitHub issues](https://github.com/sid-ak/study_assistant/issues), work is tracked as phases there. +- Read [`README.md`](README.md), Status blurb states intent. +- Compare against repo, which is the ground truth. + - Scan with `ls` and compare with directory structure in `architecture.md` to gauge progress. + - Use `git log` to reveal further current progress whether in between phases or mid-phase. +- Fetch the current issue being worked on from the GitHub repo, read its full content. + - Also read the full content of the current issue's sub issues (if any). ## Governance model Canonical context lives in `AGENTS.md` files (tool-neutral). Each `CLAUDE.md` is a one-line `@AGENTS.md` import stub so Claude Code's directory-walk loading picks up the same content. Edit `AGENTS.md`, never the stub. Each package/service carries its own scoped `AGENTS.md` describing its -surface; this one is the root entry point. - -## Primary conventions (enforced) - -- Retrieval lives only in `rag_core`. The MCP server and API are consumers, never reimplementers - ([ADR 0001](docs/decisions/0001-rag-retrieval-boundary.md)). -- Lazy model loading: no `torch` import at module top-level outside `embed/` and `rerank/` - ([ADR 0002](docs/decisions/0002-local-embedding-and-reranking.md)). -- Embedder/reranker behind a small interface so the system is agnostic to the concrete model; - the only hard lock-in is the pgvector embedding dimension (`vector(N)`). -- Secrets via `.env` (documented in `.env.example`), single local user, no auth - ([ADR 0003](docs/decisions/0003-local-single-user-scope.md)). -- Never commit; only stage. Do not run `git commit`. At the end of a set of changes, `git add` - the relevant files and leave them staged for the human to review and commit — but only if there - are currently 0 staged files. If anything is already staged, do not stage at all; leave the - working tree as-is for the human. - -## Stack & tooling - -- Python 3.12, `uv` workspace. Lint/format (`ruff`) and typecheck (`mypy`) run in pre-commit and - CI. -- PostgreSQL + pgvector is the single store, stood up via `docker compose`. -- Models: Claude via the Anthropic SDK (`claude-opus-4-8`); `bge-m3` embeddings and - `bge-reranker-v2-m3` reranking run in-process. - -## Workspace layout - -The repo is built phase by phase (see `docs/issues.md`). Today the workspace contains the root and -`packages/rag_core/` (the shared retrieval library — the foundation everything else depends on). -`cli/`, `services/`, and `apps/web/` are added in their respective phases; the target tree is -documented in `docs/architecture.md`. - -## Common commands - -```sh -uv sync # resolve + install the workspace -docker compose up -d # start Postgres + pgvector -pre-commit install # enable hooks -pre-commit run --all-files # lint + format + typecheck -uv run pytest # run tests -``` +surface; agents read the nearest file in the tree, so the closest one wins. This file is the root +entry point — keep package-specific detail in the package's own `AGENTS.md`. + +## Dev environment tips + +- Python 3.12 on a `uv` workspace. Run `uv sync` once to resolve and install every workspace member + (root + `packages/*`) into a single `.venv`. +- Run tools through the workspace venv with `uv run ` (e.g. `uv run pytest`); do not call a + global `python`/`pip`. +- Add a runtime dependency by editing the owning package's `pyproject.toml` `dependencies`, then + `uv sync`. Add a dev/test-only tool to the root `[dependency-groups] dev` instead. +- Bring up the store with `docker compose up -d` (PostgreSQL + pgvector). Copy `.env.example` to + `.env` first — the connection string and secrets live there. The DB is required for the store + layer and its tests. +- New workspace members go under `packages/` (libraries) or top-level (`cli/`, `services/`, + `apps/web/`) per `docs/architecture.md`; each gets its own scoped `AGENTS.md` plus a one-line + `CLAUDE.md` stub (`@AGENTS.md`). +- Install hooks once with `pre-commit install`; `pre-commit run --all-files` runs lint + format + + typecheck across the tree. + +## Testing instructions + +- The CI plan is in `.github/workflows/ci.yaml`: a `checks` job (ruff lint, ruff format check, mypy, + pytest against a pgvector service) and a `docs` job that builds the Sphinx site + (`uv run sphinx-build -b html docs site -W`, build-only — deployment lives in `docs-deploy.yaml`). +- Run the whole suite with `uv run pytest`. Integration tests need the database — run + `docker compose up -d` first or they fail to connect. +- Integration tests are marked `@pytest.mark.integration`. Scope a run with + `uv run pytest -m "not integration"` (fast, no DB) or `-m integration` (DB-backed); target one + test with `uv run pytest -k ""`. +- Lint, format, and typecheck must also be green: + `uv run ruff check . && uv run ruff format --check . && uv run mypy`. Fix every error and type + failure until the whole suite is green before you merge. +- Docs must build clean: `uv run sphinx-build -b html docs site -W` (CI runs this too — `-W` turns + Sphinx warnings into errors, catching broken toctrees, anchors, and autodoc import failures). +- Add or update tests for the code you change, even if nobody asked. +- Schema changes need a fresh DB: `initialize_schema()` is `CREATE ... IF NOT EXISTS` and will not + retrofit constraints, so run `docker compose down -v && docker compose up -d` after editing + `store/schema.py`. + +## Code style + +- `ruff` formats and lints (line length 100, rule set `E,F,I,UP,B`); `mypy` runs in `strict` mode. + Both must pass — do not silence them without cause. +- Type every function signature; `mypy --strict` rejects untyped defs. Prefer explicit, narrow types + over `Any`. + +## Binding decisions (ADRs) + +The locked architectural decisions are recorded as ADRs, indexed in +[`docs/decisions/`](docs/decisions/). They are binding and the source of truth — read the relevant +ADR before changing what it governs, and do not restate its rules here. + +## PR instructions + +- Branch from `dev`, naming the branch with its GitHub issue number first (e.g. `4-embeddings` for + issue #4) so the branch links back to its issue. +- PR title format: `[] ` — e.g. `[rag_core/store] Add schema + CRUD`. +- Run the full gate green before handing off (with `docker compose up -d`): + `uv run ruff check . && uv run ruff format --check . && uv run mypy && uv run pytest && uv run sphinx-build -b html docs site -W`. +- Update the `README.md` Status blurb in the same change — current status lives there (and in the + GitHub issues), not in this file. +- Never commit; only stage. Do not run `git commit`. At the end of a set of changes, `git add` the + relevant files and leave them staged for the human to review — but only if 0 files are currently + staged. If anything is already staged, do not stage at all; leave the working tree as-is. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 37eba39..83635e8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,33 +12,35 @@ pre-commit install # installs the git hooks - One language, one workspace. Everything is a `uv` workspace member under `packages/` (and, in later phases, `cli/`, `services/`, `apps/web/`). Run `uv sync` from the repo root. -- Quality gates run in pre-commit and CI: `ruff` (lint + format) and `mypy` (typecheck). Run - them all locally before pushing: +- Quality gates run in pre-commit and CI: `ruff` (lint + format) and `mypy` (typecheck). Run them + all locally before pushing: ```sh pre-commit run --all-files uv run pytest + uv run sphinx-build -b html docs site -W ``` -- CI (`.github/workflows/ci.yaml`) runs the same checks on every push and pull request. It builds - and tests, but does not deploy (see [ADR 0003](docs/decisions/0003-local-single-user-scope.md)). +- CI (`.github/workflows/ci.yaml`) runs the same checks on every push and pull request, plus a docs + build (`sphinx-build ... -W`). It builds and tests, but does not deploy (see + [ADR 0003](docs/decisions/0003-local-single-user-scope.md)); the docs site is deployed separately + by `.github/workflows/docs-deploy.yaml`. ## Branches & Commits -- Branch names and commit messages must start with the GitHub issue number they address — the - issue number, not the phase number. - - Branch: the bare number, e.g. `1-foundations` (issue #1), `4-embeddings` (issue #4). (`#` - isn't valid in branch names.) +- Branch names and commit messages must start with the GitHub issue number they address — the issue + number, not the phase number. + - Branch: the bare number, e.g. `1-foundations` (issue #1), `4-embeddings` (issue #4). (`#` isn't + valid in branch names.) - Commit message: prefix with `#<number>` so GitHub auto-links the issue, e.g. `#1 add pgvector schema`. - - Checkpoint commit messages: prefix with `#<number>-checkpoint:`. ## Governance (`AGENTS.md`) Architectural invariants are encoded as scoped `AGENTS.md` files placed where the work happens. Canonical content lives in `AGENTS.md`; each `CLAUDE.md` is a one-line `@AGENTS.md` import stub — -edit `AGENTS.md`, not the stub. Honor the primary conventions (retrieval lives only in -`rag_core`; lazy `torch` loading; embedder/reranker behind an interface). +edit `AGENTS.md`, not the stub. Honor the primary conventions (retrieval lives only in `rag_core`; +lazy `torch` loading; embedder/reranker behind an interface). ## Architectural decisions @@ -48,7 +50,5 @@ changing direction. ## Issues / roadmap -Work is tracked as phases on GitHub Issues, which are the source of truth. `docs/issues.md` is -an auto-generated mirror: the `sync-issues` workflow (`.github/workflows/sync-issues.yaml`) -regenerates and commits it whenever an issue is opened, closed, edited, or relabeled. Manage issues -on GitHub — don't hand-edit `docs/issues.md`. Issue bodies are not mirrored. +Work is tracked as phases on [GitHub Issues](https://github.com/sid-ak/study_assistant/issues), +which are the source of truth — manage them there. diff --git a/README.md b/README.md index d4fe3a8..87024f1 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # Study Assistant -A local, single-user RAG study assistant over course materials (lecture slides, papers, notes). -It answers questions with synthesis from Claude, grounded in retrieved sources, with citations back -to the exact source slide or page. Ingestion and retrieval run entirely on-machine, offline, and for +A local, single-user RAG study assistant over course materials (lecture slides, papers, notes). It +answers questions with synthesis from Claude, grounded in retrieved sources, with citations back to +the exact source slide or page. Ingestion and retrieval run entirely on-machine, offline, and for free; only answer generation calls the Anthropic API. - Design: [`docs/architecture.md`](docs/architecture.md) - Decisions: [`docs/decisions/`](docs/decisions/) (ADRs) -- Roadmap: [`docs/issues.md`](docs/issues.md) (phased) +- Roadmap: [GitHub issues](https://github.com/sid-ak/study_assistant/issues) (phased) ## Status diff --git a/docs/architecture.md b/docs/architecture.md index 9bd8a39..d71c7ef 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -17,9 +17,9 @@ Records (ADRs) in [`decisions/`](decisions/). This is a local, single-user project, and that scope is deliberate. The primary goal is to unwind the Retrieval-Augmented Generation (RAG) stack end-to-end, which drives the decision to run -everything on machine, offline, and for free. Embeddings and the cross-encoder run in-process, -so there are no per-call API costs and no network dependency for retrieval and because nothing -transits to a vendor, the corpus stays private. +everything on machine, offline, and for free. Embeddings and the cross-encoder run in-process, so +there are no per-call API costs and no network dependency for retrieval and because nothing transits +to a vendor, the corpus stays private. > NOTE > "Offline and for free" scopes to ingestion and retrieval, which run entirely on-machine. @@ -36,23 +36,22 @@ live or how they're scored so retrieval stays a clean, replaceable concern. ## Architecture -The system is a retrieval-augmented pipeline organized into three lanes: Ingestion, -Retrieval, and Generation over a single PostgreSQL/pgvector store, with a React frontend on -top. +The system is a retrieval-augmented pipeline organized into three lanes: Ingestion, Retrieval, and +Generation over a single PostgreSQL/pgvector store, with a React frontend on top. ![RAG pipeline architecture](./assets/architecture.svg) ### Ingestion -Ingestion is driven by the `study` CLI: it points at a folder of course materials, parses -slides, papers, and notes (pptx/pdf/md), semantically chunks them, embeds each chunk with `bge-m3`, -and upserts into pgvector. It is idempotent and re-runnable, so the corpus grows as courses progress +Ingestion is driven by the `study` CLI: it points at a folder of course materials, parses slides, +papers, and notes (pptx/pdf/md), semantically chunks them, embeds each chunk with `bge-m3`, and +upserts into pgvector. It is idempotent and re-runnable, so the corpus grows as courses progress without reprocessing what's already stored. ### Retrieval -Retrieval is the heart of the system and lives entirely in `rag_core`. A query runs hybrid -search — dense vector similarity against pgvector plus BM25 lexical search — fused with RRF, then a +Retrieval is the heart of the system and lives entirely in `rag_core`. A query runs hybrid search — +dense vector similarity against pgvector plus BM25 lexical search — fused with RRF, then a `bge-reranker-v2-m3` cross-encoder re-scores the top candidates into the final top-k. The reranker is the single highest-leverage quality lever, reading query and document together to catch relevance that bi-encoder embeddings miss. Retrieval quality is validated by an eval loop: a golden-set @@ -61,11 +60,10 @@ against regressions and demonstrates that reranking measurably improves results. ### Generation -Generation runs in the FastAPI backend, where a LangGraph state machine orchestrates the agent -loop — reaching retrieval through the MCP server's tools, pausing at HITL checkpoints, and -synthesizing a grounded answer with Claude (`claude-opus-4-8`, adaptive thinking, streaming). -Responses stream to the React frontend over SSE, with citations resolving back to the exact source -slide or page. +Generation runs in the FastAPI backend, where a LangGraph state machine orchestrates the agent loop +— reaching retrieval through the MCP server's tools, pausing at HITL checkpoints, and synthesizing a +grounded answer with Claude (`claude-opus-4-8`, adaptive thinking, streaming). Responses stream to +the React frontend over SSE, with citations resolving back to the exact source slide or page. ## Governance and Conventions @@ -73,10 +71,10 @@ The governance context model is a layered set of AGENTS.md files that encode the architectural invariants as enforceable rules, because key properties (like decoupling) now rest on discipline rather than physical service boundaries. -A root AGENTS.md holds the project one-liner and entry point, while each package and service -carries its own scoped AGENTS.md describing its surface and relationship to the rest, each paired -with a CLAUDE.md stub. This puts focused context exactly where work happens. For example, an -agent editing the MCP server is told it is a consumer of rag_core, never a reimplementer. +A root AGENTS.md holds the project one-liner and entry point, while each package and service carries +its own scoped AGENTS.md describing its surface and relationship to the rest, each paired with a +CLAUDE.md stub. This puts focused context exactly where work happens. For example, an agent editing +the MCP server is told it is a consumer of rag_core, never a reimplementer. This governance principle ensures that intentions become constraints that can be asserted by another coder or coding agent in the future. @@ -93,8 +91,8 @@ coder or coding agent in the future. - Retrieval lives only in `rag_core`. MCP server and API are consumers, never reimplementers. - Lazy model loading: no `torch` import at module top-level outside `embed/` and `rerank/`. -- Embedder/reranker behind a small interface so the rest of the system is agnostic to the - concrete model; the only real lock-in is the pgvector embedding dimension (`vector(N)`). +- Embedder/reranker behind a small interface so the rest of the system is agnostic to the concrete + model; the only real lock-in is the pgvector embedding dimension (`vector(N)`). ### Other Conventions @@ -173,16 +171,20 @@ study_assistant/ └── postgres/ # pgvector init.sql, model-weights volume notes ``` +> NOTE +> The Python packages are documented on a Sphinx + MyST + Furo site (`docs/conf.py`, deployed by +> `.github/workflows/docs-deploy.yaml`; see [ADR 0005](decisions/0005-documentation-tooling.md)). + ## Future Scope ### Model Agnostic -A hybrid local/Claude generation approach would extend the model-agnostic goal through the -reasoning layer and make a fully free, offline run possible end-to-end. The generation node would -sit behind a small interface — the same pattern already used for the embedder and reranker — so -config selects the model per run: a local model served through an OpenAI-compatible runtime (Ollama, -llama.cpp, or vLLM) as the free default, with Claude reserved for harder questions where synthesis -quality matters most. +A hybrid local/Claude generation approach would extend the model-agnostic goal through the reasoning +layer and make a fully free, offline run possible end-to-end. The generation node would sit behind a +small interface — the same pattern already used for the embedder and reranker — so config selects +the model per run: a local model served through an OpenAI-compatible runtime (Ollama, llama.cpp, or +vLLM) as the free default, with Claude reserved for harder questions where synthesis quality matters +most. ### Cloud Deployable @@ -194,7 +196,7 @@ pgvector `vector(N)` dimension lock-in and therefore a re-embed plus schema migr ### Multi User Auth -To avoid a future rewrite, the schema carries a `user_id` seam — a `user_id` column on the -relevant tables, defaulted for the single local user — so authentication and per-user isolation can -be layered on later by populating the seam and adding an auth layer, rather than reshaping the data +To avoid a future rewrite, the schema carries a `user_id` seam — a `user_id` column on the relevant +tables, defaulted for the single local user — so authentication and per-user isolation can be +layered on later by populating the seam and adding an auth layer, rather than reshaping the data model. diff --git a/docs/checkpoints.md b/docs/checkpoints.md deleted file mode 100644 index 9c241e3..0000000 --- a/docs/checkpoints.md +++ /dev/null @@ -1,18 +0,0 @@ -# Checkpoints - -An append-only log of project checkpoints — one row per major iteration. It gives a human or a -fresh agent a quick history of how the project advanced and where it was left off. - -Rows are appended automatically by the `sync-checkpoints` workflow -(`.github/workflows/sync-checkpoints.yaml`); don't hand-edit this file. A row is added when: - -- a pull request that closes a `phase`-labeled issue is merged (checkpoint = the PR title), or -- a commit is pushed whose message is `#<number>-checkpoint: <text>` (checkpoint = `<text>`). - -The Issue column is the leading number of the branch name, which must be the GitHub issue number -(e.g. `1-foundations` → `1`), or blank when the branch starts with no number (see `CONTRIBUTING.md`). - -| Date | Branch | Issue | Checkpoint | -| ---------- | --------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -| 2026-06-26 | `1-foundations` | 1 | Repo skeleton + governance files, `uv` workspace with `packages/rag_core`, Postgres + pgvector dev stack via `docker compose`, and pre-commit + CI. | -| 2026-06-27 | `1-foundations` | 1 | #1 Phase 0 — Foundations: uv workspace, pgvector stack, and CI | diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..de1c0ea --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,48 @@ +"""Sphinx configuration for the Study Assistant documentation site. + +Builds one static site from the all-Markdown docs/ tree (via myst-parser) plus an autodoc API +reference for rag_core (see ADR 0005). Build with: + + uv run sphinx-build -b html docs site -W +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +# autodoc imports the package to read its docstrings/type hints, so put rag_core's src-layout +# source on sys.path (the workspace install works too; this keeps the build self-contained). +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "packages" / "rag_core" / "src")) + +project = "Study Assistant" +author = "Sid Anandkumar" +copyright = "2026, Sid Anandkumar" + +extensions = [ + "myst_parser", # parse the existing .md docs as-is + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", # understand Google/NumPy docstring sections + "sphinx_autodoc_typehints", # render the mypy --strict type hints into the reference +] + +source_suffix = {".md": "markdown"} + +# Generate GitHub-style slug anchors for headings (h1-h3) so the in-page "Outline" links in +# architecture.md (e.g. [Goals](#goals)) resolve as real in-site links instead of dead anchors. +myst_heading_anchors = 3 + +html_theme = "furo" +html_title = "Study Assistant" + +# Fold type hints into the parameter descriptions rather than repeating them in the signature. +autodoc_typehints = "description" +autodoc_member_order = "bysource" + +# The narrative docs (README, architecture.md, ADRs) are authored to render on GitHub, so they carry +# repo-relative links (e.g. docs/architecture.md, decisions/) that are not in-site targets. The +# README is included with :relative-docs: so its file links rewrite correctly; the residual +# directory-style links are the only remaining source of missing-xref warnings. Suppress just that +# one class so -W still fails on everything else (autodoc import errors, bad toctrees, broken +# anchors) — the same narrowly-scoped relaxation the old mkdocs.yml used for link validation. +suppress_warnings = ["myst.xref_missing"] diff --git a/docs/decisions/0001-rag-retrieval-boundary.md b/docs/decisions/0001-rag-retrieval-boundary.md index 44b7f78..613e47d 100644 --- a/docs/decisions/0001-rag-retrieval-boundary.md +++ b/docs/decisions/0001-rag-retrieval-boundary.md @@ -13,13 +13,13 @@ ingestion/admin paths), and the CLI (for batch ingestion). The question is where the retrieval logic should _physically_ live. Three options were considered: -1. Shared library, MCP + API both import it. Retrieval lives in a standalone package; the MCP - server wraps it as tools, the API uses it directly. -2. MCP server owns retrieval. All RAG logic lives behind the MCP server; the API/LangGraph layer - is a pure MCP client. Maximal decoupling, but ingestion/admin must also route through MCP or +1. Shared library, MCP + API both import it. Retrieval lives in a standalone package; the MCP server + wraps it as tools, the API uses it directly. +2. MCP server owns retrieval. All RAG logic lives behind the MCP server; the API/LangGraph layer is + a pure MCP client. Maximal decoupling, but ingestion/admin must also route through MCP or duplicate database access. -3. API owns retrieval, MCP is a façade. The backend owns the pipeline; the MCP server is a thin - HTTP proxy. Adds a network hop for little gain and weakens the decoupling story. +3. API owns retrieval, MCP is a façade. The backend owns the pipeline; the MCP server is a thin HTTP + proxy. Adds a network hop for little gain and weakens the decoupling story. ## Decision @@ -33,10 +33,10 @@ depends on it. - Single home for retrieval. There is exactly one implementation; the MCP server and API are consumers, never reimplementers. This is enforced as a golden rule in `AGENTS.md`. - No service-to-service hop for retrieval, and no duplicated database access for ingestion. -- `rag_core` is a shared dependency (a `uv` workspace path dependency), so its public surface - must be treated as an internal API and versioned with care. -- The embedder and reranker sit behind a small interface in `rag_core`, so the rest of the - system is agnostic to the concrete models. The only hard lock-in is the pgvector embedding - dimension (`vector(N)`) — see [ADR 0002](0002-local-embedding-and-reranking.md). +- `rag_core` is a shared dependency (a `uv` workspace path dependency), so its public surface must + be treated as an internal API and versioned with care. +- The embedder and reranker sit behind a small interface in `rag_core`, so the rest of the system is + agnostic to the concrete models. The only hard lock-in is the pgvector embedding dimension + (`vector(N)`) — see [ADR 0002](0002-local-embedding-and-reranking.md). - The "decoupling" property now depends on discipline (keeping the MCP server thin) rather than a physical service boundary; the golden rule in governance is what preserves it. diff --git a/docs/decisions/0002-local-embedding-and-reranking.md b/docs/decisions/0002-local-embedding-and-reranking.md index 4a976c4..21543a2 100644 --- a/docs/decisions/0002-local-embedding-and-reranking.md +++ b/docs/decisions/0002-local-embedding-and-reranking.md @@ -10,21 +10,21 @@ two distinct stages: - Embedding — turns each chunk and each query into a vector stored in pgvector; powers the dense half of hybrid search. -- Reranking — a slower cross-encoder that re-scores a candidate set (e.g. top-50) and reorders - it to the final top-k. It reads query and document together, catching relevance that bi-encoder +- Reranking — a slower cross-encoder that re-scores a candidate set (e.g. top-50) and reorders it to + the final top-k. It reads query and document together, catching relevance that bi-encoder embeddings miss. This is the "not naive top-k" requirement and the single highest-leverage quality lever in the pipeline. Three stacks were considered: -1. Voyage AI (`voyage-3` + `rerank-2.5`) — Anthropic's recommended pairing. Best quality with - least tuning, trivial infra, but hosted: requires an API key, network access, and chunks transit - to a vendor. -2. Local open-source (BGE embeddings + a BGE cross-encoder reranker) — runs in-process. No - per-call cost, fully offline and private, but heavier containers and slower CPU reranking, and - the eval/model-selection work is owned in-house. -3. OpenAI + Cohere — solid hosted option, but spans three vendors and three keys to do what - Voyage does in one. +1. Voyage AI (`voyage-3` + `rerank-2.5`) — Anthropic's recommended pairing. Best quality with least + tuning, trivial infra, but hosted: requires an API key, network access, and chunks transit to a + vendor. +2. Local open-source (BGE embeddings + a BGE cross-encoder reranker) — runs in-process. No per-call + cost, fully offline and private, but heavier containers and slower CPU reranking, and the + eval/model-selection work is owned in-house. +3. OpenAI + Cohere — solid hosted option, but spans three vendors and three keys to do what Voyage + does in one. At this project's scale (a personal course corpus, ~10K–50K chunks) cost is negligible for every option and all three clear the quality bar for course Q&A. The deciding axis is therefore @@ -32,8 +32,8 @@ offline/privacy and learning value vs. operational simplicity, not cost or quali ## Decision -Adopt the local open-source stack: `bge-m3` for embeddings and `bge-reranker-v2-m3` -(cross-encoder) for reranking, both running in-process within `rag_core`. +Adopt the local open-source stack: `bge-m3` for embeddings and `bge-reranker-v2-m3` (cross-encoder) +for reranking, both running in-process within `rag_core`. This is driven by the explicit goals of no per-call cost, keeping everything on-machine, and learning the RAG stack end-to-end (model loading, the eval loop, CPU/MPS performance). @@ -45,14 +45,13 @@ learning the RAG stack end-to-end (model loading, the eval loop, CPU/MPS perform multi-GB image). Model loading is isolated to the `embed/` and `rerank/` modules — no `torch` import at module top-level elsewhere — and weights are cached in a named Docker volume rather than baked into images, so the download happens once. -- Reranking is the slow step on CPU: a cross-encoder scoring ~50 candidates per query is the - cost of "not naive top-k." It uses MPS on macOS and CPU in Linux containers/CI. The rerank - candidate count is configurable to trade latency for quality, and model loads stay out of the - request path. +- Reranking is the slow step on CPU: a cross-encoder scoring ~50 candidates per query is the cost of + "not naive top-k." It uses MPS on macOS and CPU in Linux containers/CI. The rerank candidate count + is configurable to trade latency for quality, and model loads stay out of the request path. - We own the eval loop — a golden-set harness (labeled query → expected-doc) introduced in the embeddings phase — to validate retrieval quality and demonstrate that reranking improves it. - The embedding model's output dimension is baked into the pgvector `vector(N)` column and its index. Changing embedders later means a re-embed plus a schema migration; this is the main lock-in and the reason the choice is fixed early. -- Query and document vectors must come from the same model, so the embedder is pinned in config, - not chosen per call. +- Query and document vectors must come from the same model, so the embedder is pinned in config, not + chosen per call. diff --git a/docs/decisions/0003-local-single-user-scope.md b/docs/decisions/0003-local-single-user-scope.md index 16db454..d84fa68 100644 --- a/docs/decisions/0003-local-single-user-scope.md +++ b/docs/decisions/0003-local-single-user-scope.md @@ -14,9 +14,9 @@ authentication and multi-tenancy exist as concerns at all. Three options were co (managed Postgres, registry push, prod vs. local config). More infra surface — and the local `bge` models are heavy to host in the cloud anyway (see [ADR 0002](0002-local-embedding-and-reranking.md)). -3. Multi-user with auth — per-user corpora and authentication from day one. The most - infrastructure: an auth layer, per-user data isolation, and user migrations. Overkill for a - personal tool unless it will be shared. +3. Multi-user with auth — per-user corpora and authentication from day one. The most infrastructure: + an auth layer, per-user data isolation, and user migrations. Overkill for a personal tool unless + it will be shared. ## Decision @@ -29,13 +29,13 @@ be layered on later without restructuring the data model — but none of that is ## Consequences -- Simplest possible footprint: no auth layer, no session/JWT handling, no per-user isolation - logic. Development effort goes into retrieval and orchestration instead. -- Secrets live in `.env`, documented via `.env.example`. This is acceptable for a single-user - local tool and is not a model for a shared deployment. -- CI builds and tests images but does not deploy — there is no cloud target, registry push, or - prod configuration to maintain. -- The `user_id` seam preserves an upgrade path to authenticated multi-user use; adopting it - later would mean adding an auth layer and populating the seam, not reshaping the schema. +- Simplest possible footprint: no auth layer, no session/JWT handling, no per-user isolation logic. + Development effort goes into retrieval and orchestration instead. +- Secrets live in `.env`, documented via `.env.example`. This is acceptable for a single-user local + tool and is not a model for a shared deployment. +- CI builds and tests images but does not deploy — there is no cloud target, registry push, or prod + configuration to maintain. +- The `user_id` seam preserves an upgrade path to authenticated multi-user use; adopting it later + would mean adding an auth layer and populating the seam, not reshaping the schema. - This decision aligns the deployment story with [ADR 0002](0002-local-embedding-and-reranking.md): on-machine models and an on-machine, single-user runtime reinforce the same all-local goal. diff --git a/docs/decisions/0004-cli-batch-ingestion.md b/docs/decisions/0004-cli-batch-ingestion.md index 0150ffb..0eff923 100644 --- a/docs/decisions/0004-cli-batch-ingestion.md +++ b/docs/decisions/0004-cli-batch-ingestion.md @@ -9,28 +9,28 @@ Course materials (slides, papers, notes) need to get into the system: parsed, se embedded, and stored in pgvector. How documents enter shapes early architecture. Two primary approaches were considered: -1. CLI batch — a command points at a folder and runs the pipeline. Best fit for a personal - corpus the user controls on disk; re-runnable and scriptable, and it needs no upload UI or job - queue, keeping early phases lean. -2. UI upload + async worker — files are uploaded through the React app and processed by a - background worker. Nicer UX, but it adds a job queue, a worker service, and upload/status - endpoints up front — more moving parts before retrieval even works. +1. CLI batch — a command points at a folder and runs the pipeline. Best fit for a personal corpus + the user controls on disk; re-runnable and scriptable, and it needs no upload UI or job queue, + keeping early phases lean. +2. UI upload + async worker — files are uploaded through the React app and processed by a background + worker. Nicer UX, but it adds a job queue, a worker service, and upload/status endpoints up front + — more moving parts before retrieval even works. A third "both, CLI now / UI later" framing was also considered; in practice that reduces to "build the CLI engine first," which is exactly option 1 with the UI route deferred to a later phase. ## Decision -Adopt CLI batch ingestion: a `study ingest <folder>` command parses, semantically chunks, -embeds, and upserts into pgvector. It is idempotent and re-runnable. +Adopt CLI batch ingestion: a `study ingest <folder>` command parses, semantically chunks, embeds, +and upserts into pgvector. It is idempotent and re-runnable. -The CLI is its own top-level package (`cli/`, exposing the `study` console-script) that depends -on `rag_core` as a workspace dependency — it is a thin entry point over the library, kept separate -from it rather than bundled inside. +The CLI is its own top-level package (`cli/`, exposing the `study` console-script) that depends on +`rag_core` as a workspace dependency — it is a thin entry point over the library, kept separate from +it rather than bundled inside. -A light UI ingestion path is the planned follow-on, not a rejected option: it is deferred to a -later phase rather than declined, and will reuse the same `rag_core` pipeline rather than introduce -a second one. CLI-first is the order of work, not a decision to remain CLI-only. +A light UI ingestion path is the planned follow-on, not a rejected option: it is deferred to a later +phase rather than declined, and will reuse the same `rag_core` pipeline rather than introduce a +second one. CLI-first is the order of work, not a decision to remain CLI-only. ## Consequences @@ -38,7 +38,7 @@ a second one. CLI-first is the order of work, not a decision to remain CLI-only. first working phases stay lean and focused on retrieval. - Ingestion is idempotent and re-runnable, which suits a corpus that grows as courses progress. - Because all ingestion logic lives in `rag_core` (per [ADR 0001](0001-rag-retrieval-boundary.md)), - the planned light UI upload route in a later phase reuses the exact same pipeline — the - CLI-first choice sets up that follow-on rather than foreclosing it. -- The CLI being a separate package keeps the library's surface clean and lets the ingestion - entry point evolve (flags, batching, progress) without touching `rag_core`. + the planned light UI upload route in a later phase reuses the exact same pipeline — the CLI-first + choice sets up that follow-on rather than foreclosing it. +- The CLI being a separate package keeps the library's surface clean and lets the ingestion entry + point evolve (flags, batching, progress) without touching `rag_core`. diff --git a/docs/decisions/0005-documentation-tooling.md b/docs/decisions/0005-documentation-tooling.md new file mode 100644 index 0000000..786abc0 --- /dev/null +++ b/docs/decisions/0005-documentation-tooling.md @@ -0,0 +1,93 @@ +# 5. Documentation site tooling: Sphinx, MyST, and Furo + +- Status: Accepted +- Date: 2026-07-01 + +## Context + +The narrative docs (`architecture.md` and the ADRs) are already a Markdown-only, docs-as-code tree. +Once `rag_core` carries real typed modules (`config.py`, `store/`, and later `ingest/`, `embed/`, +`rerank/`, `retrieve/`), a second need shows up: a browsable API reference generated from docstrings +and the `mypy --strict` type hints already required by `AGENTS.md`, rather than hand-written and +prone to drifting from the code. Both halves — narrative and generated reference — are wanted in one +static site, deployed by a GitHub Actions workflow like the rest of the project's automation. + +An initial pass adopted MkDocs + `mkdocstrings` + Material for MkDocs and got as far as a working +`mkdocs.yml`, a generated API reference page, and a deploy workflow. That work surfaced a governance +problem rather than a technical one: MkDocs 2.0 (announced by a maintainer who took over the project +mid-2024) removes the plugin system entirely, has no migration path for existing projects, and is +currently unlicensed. Since `mkdocstrings` — the entire reason to use this stack — is itself a +plugin, pinning `mkdocs<2` only defers the exposure; it does not remove it. For a project being +written from scratch with no legacy MkDocs investment to protect, that is a reason to pick a +foundation without that single point of failure, not to work around it. + +Several tool families were considered: + +1. **MkDocs + Material + `mkdocstrings`** — the original pick. Excellent ergonomics and theme, but + its core dependency (MkDocs itself) is mid-fork, with the maintainer removing the plugin system + that `mkdocstrings` depends on and no announced compatibility story for existing users. +2. **Zensical** — a from-scratch static site generator built by the Material for MkDocs team, + explicitly aimed at being a drop-in MkDocs 1.x replacement, and already listing `mkdocstrings` as + a supported (Tier 1) plugin. Rejected for now: versioned `0.0.x`, and its own compatibility page + states the plugin/module system's public API is deliberately being held back until no breaking + changes are expected. Worth revisiting once it reaches a stable public API — migration should be + closer to a drop-in swap than a rewrite by the team's own design goal. +3. **Decoupled: `pdoc` (API reference only) + plain Markdown narrative docs.** `pdoc` has no plugin + system to fracture — it is a single, focused tool with nothing to depend on breaking. Rejected as + the primary choice because it would abandon a unified site (nav, cross-linking, search across + both narrative and reference) in exchange for a risk this project can avoid without that + trade-off. +4. **Node-based site generators — Starlight (Astro), VitePress, Docusaurus.** All are mature, fast, + and have no relation to the MkDocs governance problem. Rejected for this project specifically: + none has a first-class Python docstring/type-hint introspection story equivalent to + `autodoc`/`mkdocstrings` — generating the API reference would mean running a separate Python tool + to emit Markdown and gluing it into the site, plus introducing a second runtime (Node) alongside + the `uv` Python workspace this project otherwise keeps to one language end-to-end (per + [ADR 0000](0000-stack.md)). Docusaurus in particular adds versioning/i18n machinery this + single-user, single-corpus project (per [ADR 0003](0003-local-single-user-scope.md)) has no use + for. +5. **Quarto.** A strong fit on paper given the project's ML/RAG content — it can execute Python code + blocks live, which could embed real retrieval/generation output in the docs. Rejected for now: it + is a separate non-Python binary/runtime (not a `uv` dev dependency), its docstring-to-reference + story (`quartodoc`) is newer and far less mature than `autodoc`, and the live-execution + capability is a genuine future enhancement rather than a current need. Worth reconsidering under + a documentation "Future Scope" once retrieval/generation exist to demonstrate. +6. **Sphinx, with a modern theme (PyData Sphinx Theme, Sphinx Book Theme, or Furo).** The + long-standing Python documentation standard, multi-maintainer governed with no comparable fork + history, and `autodoc` integrates directly with docstrings and type hints. Historically dated + default styling, but that is a theme problem, not a Sphinx problem — solved by pairing it with a + modern theme. + +## Decision + +Adopt **Sphinx** with **`myst-parser`** (so the existing all-Markdown `docs/` tree needs no +rewrite), **`autodoc`** plus **`sphinx-autodoc-typehints`** for the generated API reference, and +**Furo** as the theme. Furo over PyData Sphinx Theme or Sphinx Book Theme specifically: PyData's +version-switcher and multi-project chrome targets scientific-package ecosystems this project doesn't +have, and Sphinx Book Theme leans toward long-form, Jupyter-book-style scientific text; Furo is +minimal, actively maintained, and closest in spirit to the clean single-project developer-tool look +the MkDocs + Material setup was chosen for in the first place. + +The MkDocs-based setup (`mkdocs.yml`, the `mkdocstrings` reference page, the mkdocs deploy workflow, +and the corresponding dev dependencies) is removed rather than kept as a fallback — there is no +production site depending on it yet, so this is the cheapest point at which to switch. + +## Consequences + +- No second point of failure from a single maintainer's roadmap: Sphinx has a core-developer team + with shared commit access, and no history comparable to the MkDocs 2.0 situation. +- Stays inside the `uv` Python workspace as a dev dependency, consistent with keeping the ML core + and its tooling in one language and one workspace ([ADR 0000](0000-stack.md)) — no Node runtime + (as Starlight/VitePress/Docusaurus would require) or separate binary (as Quarto would require) + enters the toolchain. +- `myst-parser` means `architecture.md` and the ADRs are wired into a Sphinx `toctree` as-is; no + content is rewritten into reStructuredText. +- Gives up things this project doesn't currently need: Docusaurus-grade versioning/i18n, Starlight's + zero-client-JS search speed, and Quarto's live code execution. Quarto in particular is a candidate + to revisit later, under a documentation "Future Scope," once retrieval/generation output exists to + embed as living examples. +- Some Sphinx configuration and cross-referencing (`conf.py`, roles like `:py:class:`) is still + reST-flavored under the hood even though page content is authored in Markdown — an accepted + friction in exchange for governance stability and native `autodoc`/type-hint integration. +- Deployment is a GitHub Actions workflow that builds the site and publishes to GitHub Pages, kept + as its own workflow rather than a job in `ci.yaml`. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index d1e5d3e..b9937a9 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -14,3 +14,4 @@ phased build plan is tracked as | [0002](0002-local-embedding-and-reranking.md) | Embedding and reranking stack: local open-source models | Accepted | | [0003](0003-local-single-user-scope.md) | Deployment scope: local single-user | Accepted | | [0004](0004-cli-batch-ingestion.md) | Document ingestion: CLI batch | Accepted | +| [0005](0005-documentation-tooling.md) | Documentation site tooling: Sphinx, MyST, and Furo | Accepted | diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..81088b0 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,34 @@ +```{include} ../README.md +:relative-docs: docs/ +:relative-images: +``` + +```{toctree} +:hidden: +:maxdepth: 2 +:caption: Project + +architecture.md +``` + +```{toctree} +:hidden: +:maxdepth: 1 +:caption: Decisions (ADRs) + +Overview <decisions/README.md> +decisions/0000-stack.md +decisions/0001-rag-retrieval-boundary.md +decisions/0002-local-embedding-and-reranking.md +decisions/0003-local-single-user-scope.md +decisions/0004-cli-batch-ingestion.md +decisions/0005-documentation-tooling.md +``` + +```{toctree} +:hidden: +:maxdepth: 2 +:caption: API Reference + +reference/index.md +``` diff --git a/docs/issues.md b/docs/issues.md deleted file mode 100644 index 08d13c3..0000000 --- a/docs/issues.md +++ /dev/null @@ -1,28 +0,0 @@ -# Issues - -> Auto-generated — do not edit by hand. This table mirrors the GitHub issues for -> [`sid-ak/study_assistant`](https://github.com/sid-ak/study_assistant/issues) and is regenerated by the `sync-issues` -> workflow (`.github/workflows/sync-issues.yaml`) on every issue event. GitHub is the -> source of truth — manage issues there, not here. Issue bodies are not mirrored. - -| # | Title | State | Labels | Parent | -| --- | ----- | ----- | ------ | ------ | -| 1 | Phase 0 — Foundations | closed | phase | | -| 2 | Phase 1 — Storage + schema (rag_core/store) | open | phase | | -| 3 | Phase 2 — Ingestion + CLI (rag_core/ingest + cli/) | open | phase | | -| 4 | Phase 3 — Embeddings + dense retrieval (rag_core/embed) | open | phase | | -| 5 | Phase 4 — Hybrid retrieval + reranking (rag_core/retrieve + rerank) | open | phase | | -| 6 | Phase 5 — MCP server | open | phase | | -| 7 | Phase 6 — FastAPI + LangGraph + streaming | open | phase | | -| 8 | Phase 7 — Human-in-the-loop checkpoints | open | phase | | -| 9 | Phase 8 — React frontend | open | phase | | -| 10 | Phase 9 — CI/CD hardening + full-stack e2e | open | phase | | -| 11 | Cap per-turn retrieval calls and cache rerank scores | open | improvement, priority: high | 7 | -| 12 | Document ts_rank as BM25 approximation and validate before pg_bm25 | open | improvement, priority: medium | 5 | -| 13 | Define per-content-type chunking strategy | open | improvement, priority: medium | 3 | -| 14 | Add manifest table for idempotent ingestion | open | improvement, priority: low | 3 | -| 15 | Record embedder model/dimension and stub re-embed migration | open | improvement, priority: medium | 4 | -| 16 | Isolate Anthropic SDK calls in a single generation module | open | improvement, priority: low | 7 | -| 17 | Define explicit SSE event types for HITL streaming | open | improvement, priority: medium | 8 | -| 18 | Add /health endpoint reporting model load status | open | improvement, priority: low | 6 | -| 19 | Augment golden eval set with synthetic query variations | open | improvement, priority: low | 4 | diff --git a/docs/reference/index.md b/docs/reference/index.md new file mode 100644 index 0000000..30d7f7d --- /dev/null +++ b/docs/reference/index.md @@ -0,0 +1,24 @@ +# API Reference + +Generated from docstrings and the `mypy --strict` type hints by Sphinx autodoc (see +[ADR 0005](../decisions/0005-documentation-tooling.md)). One directive block per module, so the +reference stays navigable rather than one giant page. + +## rag_core + +```{eval-rst} +.. automodule:: rag_core + :members: + :undoc-members: + :show-inheritance: +``` + +## Modules added as phases land + +The following modules are not yet in the tree; add one `automodule` block each (identical in shape +to the `rag_core` block above) as they land: + +- `rag_core.config` — settings +- `rag_core.store.client` — pgvector access +- `rag_core.store.schema` — table/index DDL +- later phases: `rag_core.ingest`, `rag_core.embed`, `rag_core.rerank`, `rag_core.retrieve` diff --git a/packages/rag_core/AGENTS.md b/packages/rag_core/AGENTS.md index b2fd1ce..ac2a566 100644 --- a/packages/rag_core/AGENTS.md +++ b/packages/rag_core/AGENTS.md @@ -1,13 +1,13 @@ # rag_core — Agent Context -`rag_core` is the shared retrieval library: the single, authoritative implementation of -chunking, embedding, hybrid retrieval, reranking, and pgvector access. See the root +`rag_core` is the shared retrieval library: the single, authoritative implementation of chunking, +embedding, hybrid retrieval, reranking, and pgvector access. See the root [`AGENTS.md`](../../AGENTS.md) for project-wide context. ## Golden rule -Retrieval lives only here. The MCP server, the API, and the CLI are _consumers_ of `rag_core`, -never reimplementers. If retrieval logic is being written anywhere else, it belongs here instead +Retrieval lives only here. The MCP server, the API, and the CLI are _consumers_ of `rag_core`, never +reimplementers. If retrieval logic is being written anywhere else, it belongs here instead ([ADR 0001](../../docs/decisions/0001-rag-retrieval-boundary.md)). The public surface is an internal API — treat it as versioned and change it deliberately. @@ -16,12 +16,12 @@ API — treat it as versioned and change it deliberately. - Lazy model loading. No `torch` import at module top-level outside `embed/` and `rerank/`. Importing `rag_core` (or any non-model module) must not pull in PyTorch or load weights ([ADR 0002](../../docs/decisions/0002-local-embedding-and-reranking.md)). -- Embedder and reranker sit behind a small interface so the rest of the system is agnostic to - the concrete model. The one hard lock-in is the pgvector embedding dimension (`vector(N)`): query - and document vectors must come from the same model, pinned in config, not chosen per call. - Changing embedders later means a re-embed plus schema migration. -- `user_id` seam. Schema carries a defaulted `user_id` column on relevant tables so multi-user - auth can be layered on later without reshaping the data model +- Embedder and reranker sit behind a small interface so the rest of the system is agnostic to the + concrete model. The one hard lock-in is the pgvector embedding dimension (`vector(N)`): query and + document vectors must come from the same model, pinned in config, not chosen per call. Changing + embedders later means a re-embed plus schema migration. +- `user_id` seam. Schema carries a defaulted `user_id` column on relevant tables so multi-user auth + can be layered on later without reshaping the data model ([ADR 0003](../../docs/decisions/0003-local-single-user-scope.md)). ## Layout (built out across phases) diff --git a/pyproject.toml b/pyproject.toml index ac43019..646ceaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,10 @@ dev = [ "ruff>=0.6", "mypy>=1.11", "pytest>=8.0", + "sphinx>=8.0", + "myst-parser>=4.0", + "sphinx-autodoc-typehints>=2.0", + "furo>=2024.8", ] [tool.ruff] diff --git a/uv.lock b/uv.lock index d3f3e85..93a1353 100644 --- a/uv.lock +++ b/uv.lock @@ -8,6 +8,27 @@ members = [ "study-assistant", ] +[[package]] +name = "accessible-pygments" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + [[package]] name = "ast-serialize" version = "0.5.0" @@ -32,6 +53,62 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/19/cc8bd127d28a43da249aa955cfd164cf8fd534e79e42cea96c4854d72fd0/ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642", size = 1081181, upload-time = "2026-05-17T17:48:28.122Z" }, ] +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -41,6 +118,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "furo" +version = "2025.12.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accessible-pygments" }, + { name = "beautifulsoup4" }, + { name = "pygments" }, + { name = "sphinx" }, + { name = "sphinx-basic-ng" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/20/5f5ad4da6a5a27c80f2ed2ee9aee3f9e36c66e56e21c00fde467b2f8f88f/furo-2025.12.19.tar.gz", hash = "sha256:188d1f942037d8b37cd3985b955839fea62baa1730087dc29d157677c857e2a7", size = 1661473, upload-time = "2025-12-19T17:34:40.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/b2/50e9b292b5cac13e9e81272c7171301abc753a60460d21505b606e15cf21/furo-2025.12.19-py3-none-any.whl", hash = "sha256:bb0ead5309f9500130665a26bee87693c41ce4dbdff864dbfb6b0dae4673d24f", size = 339262, upload-time = "2025-12-19T17:34:38.905Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "imagesize" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -50,6 +170,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + [[package]] name = "librt" version = "0.11.0" @@ -71,6 +203,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "mypy" version = "2.1.0" @@ -103,6 +287,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "myst-parser" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "jinja2" }, + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/dc/603751677fff302f34396e206b610f556a59d7fe58b9a2145f54e96b48e8/myst_parser-5.1.0.tar.gz", hash = "sha256:ab69322dc6719dcc7f296479dbb70181b66df6ed315064f92dbc85c0e1bf2f02", size = 101182, upload-time = "2026-05-13T09:38:19.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl", hash = "sha256:9c91c52b3cdb4d94a6506e4fab4e2f296c7623a0da0dcbe6de1565c3dad67a8a", size = 85817, upload-time = "2026-05-13T09:38:17.904Z" }, +] + [[package]] name = "packaging" version = "26.2" @@ -155,11 +356,53 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, +] + [[package]] name = "rag-core" version = "0.0.0" source = { editable = "packages/rag_core" } +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + [[package]] name = "ruff" version = "0.15.19" @@ -185,6 +428,130 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/30/66/9a73695e31eaee04f35d8475998bf8ab354465f9c638936d76111603dcc5/ruff-0.15.19-py3-none-win_arm64.whl", hash = "sha256:6c6b607466e47349332eb1d9be52fb1467423fc07c217341af41cd0f3f0573be", size = 11376779, upload-time = "2026-06-24T01:10:34.465Z" }, ] +[[package]] +name = "snowballstemmer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/f8/0a71edf031f03c40db17503cb8ca78a69a171254e568e7db241b0ab57ea1/snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260", size = 123314, upload-time = "2026-06-03T00:56:40.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752", size = 104164, upload-time = "2026-06-03T00:56:38.614Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, +] + +[[package]] +name = "sphinx-autodoc-typehints" +version = "3.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/f2/d99787d34d881b2352c04ce02bcdf0a61adf54b389c86d438b7a8ac3ae20/sphinx_autodoc_typehints-3.12.0.tar.gz", hash = "sha256:6571eb33c72cdc616a9945730fcb43c5bcf3685e79f1e8aea144735e43d5230d", size = 83874, upload-time = "2026-06-25T15:44:47.786Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl", hash = "sha256:1a639b7cf71be13c4d8a4f0b552dcfcdf32ff39ac33ecb47398acb85863c2faf", size = 42548, upload-time = "2026-06-25T15:44:46.306Z" }, +] + +[[package]] +name = "sphinx-basic-ng" +version = "1.0.0b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736, upload-time = "2023-07-08T18:40:54.166Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496, upload-time = "2023-07-08T18:40:52.659Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + [[package]] name = "study-assistant" version = "0.0.0" @@ -192,20 +559,28 @@ source = { virtual = "." } [package.dev-dependencies] dev = [ + { name = "furo" }, { name = "mypy" }, + { name = "myst-parser" }, { name = "pytest" }, { name = "rag-core" }, { name = "ruff" }, + { name = "sphinx" }, + { name = "sphinx-autodoc-typehints" }, ] [package.metadata] [package.metadata.requires-dev] dev = [ + { name = "furo", specifier = ">=2024.8" }, { name = "mypy", specifier = ">=1.11" }, + { name = "myst-parser", specifier = ">=4.0" }, { name = "pytest", specifier = ">=8.0" }, { name = "rag-core", editable = "packages/rag_core" }, { name = "ruff", specifier = ">=0.6" }, + { name = "sphinx", specifier = ">=8.0" }, + { name = "sphinx-autodoc-typehints", specifier = ">=2.0" }, ] [[package]] @@ -216,3 +591,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac8 wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +]