diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2e8bbc73..fc7039dd 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -33,7 +33,7 @@ updates: update-types: ["minor", "patch"] ignore: # 1.1.0 pins `syrupy==4.8.0`, which caps `pytest<9` and drags pytest - # back to 8.4.2 — PYSEC-2026-1845. Drop this once it allows syrupy 5. + # back to 8.4.2: PYSEC-2026-1845. Drop this once it allows syrupy 5. - dependency-name: "pytest-textual-snapshot" versions: ["1.1.0"] open-pull-requests-limit: 5 diff --git a/.github/scripts/aggregate_junit.py b/.github/scripts/aggregate_junit.py index 870d1c73..ceabb991 100644 --- a/.github/scripts/aggregate_junit.py +++ b/.github/scripts/aggregate_junit.py @@ -2,7 +2,7 @@ Reads the junit reports one per (OS, run) and prints a markdown summary. A test that fails in some runs and passes in others is a flake; one that -fails in every run of an OS is a real, platform-specific failure — the +fails in every run of an OS is a real, platform-specific failure: the distinction the workflow exists to make. """ @@ -68,7 +68,7 @@ def main(root: Path, expect_oses: list[str] | None = None, expect_runs: int = 0) detail.setdefault(nodeid, _detail(case)) total = sum(len(v) for v in runs.values()) - out = [f"## Flake hunt — {total} suite runs\n"] + out = [f"## Flake hunt: {total} suite runs\n"] # A job that dies before uploading leaves no directory at all, so a smaller # count is the ONLY trace of it. Say so loudly: a table that silently @@ -83,7 +83,7 @@ def main(root: Path, expect_oses: list[str] | None = None, expect_runs: int = 0) if absent: out.append( f"> **{len(absent)} of {len(expect_oses) * expect_runs} suites reported " - f"nothing** — {', '.join(absent)}. Still running, or the job died before " + f"nothing**: {', '.join(absent)}. Still running, or the job died before " "uploading. Everything below describes only the suites that reported.\n" ) out.append("| OS | runs | runs with a failure |") @@ -94,7 +94,7 @@ def main(root: Path, expect_oses: list[str] | None = None, expect_runs: int = 0) out.append("") if missing: - out.append(f"> No report from: {', '.join(missing)} — job died before pytest wrote one.\n") + out.append(f"> No report from: {', '.join(missing)}, job died before pytest wrote one.\n") if not fails: out.append("**Every run green.** No test failed in any run.") @@ -129,7 +129,7 @@ def _json_list(arg: str) -> list[str]: if __name__ == "__main__": - # argv: [oses-json] [indices-json] — the matrix that was ASKED + # argv: [oses-json] [indices-json]; the matrix that was ASKED # for, so a suite that never reported is named rather than silently missing. _dir = Path(sys.argv[1] if len(sys.argv) > 1 else "reports") _oses = _json_list(sys.argv[2]) if len(sys.argv) > 2 else None diff --git a/.github/scripts/check_ruff_sync.py b/.github/scripts/check_ruff_sync.py index ada6c5f3..6118b4c6 100644 --- a/.github/scripts/check_ruff_sync.py +++ b/.github/scripts/check_ruff_sync.py @@ -37,7 +37,7 @@ def main() -> int: if hook != locked: print( - f"::error::ruff drift — .pre-commit-config.yaml pins v{hook}, uv.lock resolves {locked}" + f"::error::ruff drift, .pre-commit-config.yaml pins v{hook}, uv.lock resolves {locked}" ) print( "\nThe hook and CI must run the same ruff. Drift means the hook " diff --git a/.github/scripts/check_runtime_pins.py b/.github/scripts/check_runtime_pins.py index 54ca5c05..541ba472 100644 --- a/.github/scripts/check_runtime_pins.py +++ b/.github/scripts/check_runtime_pins.py @@ -41,7 +41,7 @@ def main() -> int: f"\n{len(bad)} user-facing dependency specifier(s) are not pinned.\n" "uv.lock reaches no installer, so these specifiers are what every user\n" "resolves. A widened range ships versions CI never ran.\n" - "Dependabot widens by default — correct the specifier before merging." + "Dependabot widens by default: correct the specifier before merging." ) return 1 diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index a6fc7670..233ab244 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -52,7 +52,7 @@ jobs: # --refresh so a cached wheel cannot mask a broken publish. out=$(uvx --refresh --from "fndr==$VERSION" fnd version 2>&1) printf '%s\n' "$out" - # Whole line, not substring — 0.0.50 contains 0.0.5. + # Whole line, not substring: 0.0.50 contains 0.0.5. printf '%s\n' "$out" | grep -qFx "$VERSION" \ || { echo "::error::PyPI install did not report exactly $VERSION"; exit 1; } @@ -109,7 +109,7 @@ jobs: body="The weekly canary failed installing the published fndr \`$VERSION\`: $RUN_URL This means users cannot install, or are installing the wrong version. - The run shows which channel — PyPI, Homebrew, or a specific OS. + The run shows which channel: PyPI, Homebrew, or a specific OS. This is user-facing and outranks anything else in the queue." existing=$(gh issue list --search "$title in:title" --state open \ diff --git a/.github/workflows/deps-latest.yml b/.github/workflows/deps-latest.yml index 9a9a4de7..fe0945bc 100644 --- a/.github/workflows/deps-latest.yml +++ b/.github/workflows/deps-latest.yml @@ -52,7 +52,7 @@ jobs: echo "### Newest allowed vs committed uv.lock (${{ matrix.os }})" echo '```diff' if diff -u /tmp/before.txt /tmp/after.txt | tail -n +3 | grep -E '^[-+]'; then :; else - echo "(nothing moved — the lock is already at the newest allowed versions)" + echo "(nothing moved; the lock is already at the newest allowed versions)" fi echo '```' } >> "$GITHUB_STEP_SUMMARY" @@ -90,7 +90,7 @@ jobs: summary lists exactly which versions moved. Do not raise the affected pin in \`pyproject.toml\` until this is green again. - Users are unaffected while the pins stay as they are — that is what + Users are unaffected while the pins stay as they are: that is what the three-component \`~=\` ranges buy." # Comment on the standing issue rather than opening a fresh one each # Monday, so a long-lived upstream break is one thread, not twelve. diff --git a/.github/workflows/flake-hunt.yml b/.github/workflows/flake-hunt.yml index 01ec4631..1f925c32 100644 --- a/.github/workflows/flake-hunt.yml +++ b/.github/workflows/flake-hunt.yml @@ -2,7 +2,7 @@ name: flake-hunt # Runs the suite N times per OS and reports which tests failed and how # often. A single CI run cannot tell a flake from a fix; this can. -# Manual dispatch only — it costs N times a normal run. +# Manual dispatch only: it costs N times a normal run. on: # `workflow_dispatch` only fires for workflows already on the default diff --git a/.github/workflows/quarterly-review.yml b/.github/workflows/quarterly-review.yml index ccf66684..17bd95c5 100644 --- a/.github/workflows/quarterly-review.yml +++ b/.github/workflows/quarterly-review.yml @@ -52,14 +52,14 @@ jobs: run: | set -euo pipefail gh issue create \ - --title "Quarterly dependency review — $(date -u '+%Y-%m')" \ + --title "Quarterly dependency review: $(date -u '+%Y-%m')" \ --body "Per SECURITY.md, review the security-critical parsers this quarter. For each of **pymupdf, python-docx, python-pptx, tantivy, markdown-it-py**: - [ ] Read the changelog since the pinned version for disclosed security fixes - [ ] \`uv lock --upgrade-package \` - - [ ] Raise the \`~=X.Y.Z\` pin in \`pyproject.toml\` to match — the pin, not the lock, is what users get + - [ ] Raise the \`~=X.Y.Z\` pin in \`pyproject.toml\` to match: the pin, not the lock, is what users get - [ ] Confirm \`deps-latest\` and the full suite are green at the new version - [ ] Ship as a patch release if anything security-relevant was disclosed @@ -70,4 +70,4 @@ jobs: \`\`\` Note this diff only shows movement *allowed by the pins* (patches). A - new minor will not appear here — check the upstream changelogs directly." + new minor will not appear here: check the upstream changelogs directly." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7f8e1182..17f3bc01 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,7 @@ name: release # Triggered on any `v*` tag. Builds sdist + wheel, emits SBOM and a # SLSA-style build-provenance attestation, publishes to PyPI via a -# Trusted Publisher (OIDC — no API token in repo secrets), and opens +# Trusted Publisher (OIDC: no API token in repo secrets), and opens # a bump-PR against the Homebrew tap with the new sdist SHA-256. on: @@ -34,7 +34,7 @@ jobs: with: fetch-depth: 0 - - name: Guard — release tag matches pyproject version + - name: Guard (release tag matches pyproject version) # Fail fast (before build/publish) if the `vX.Y.Z` tag and the # declared version have drifted. pyproject is the single source; # this just stops a mismatched tag from shipping. @@ -42,7 +42,7 @@ jobs: set -euo pipefail tag="${GITHUB_REF_NAME#v}" ver=$(python3 -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])") - [ "$tag" = "$ver" ] || { echo "::error::tag v$tag != pyproject version $ver — bump pyproject.toml before tagging"; exit 1; } + [ "$tag" = "$ver" ] || { echo "::error::tag v$tag != pyproject version $ver, bump pyproject.toml before tagging"; exit 1; } echo "tag and pyproject version agree: $ver" - name: Install uv @@ -82,7 +82,7 @@ jobs: sha=$(shasum -a 256 "$sdist" | awk '{print $1}') # PEP 625 sdist name is `{name}-{version}.tar.gz` and the name # part carries no `-` (normalised to `_`), so the first `-` - # splits name from version — name-agnostic (fndr, fnd_x, …). + # splits name from version: name-agnostic (fndr, fnd_x, …). base=$(basename "$sdist" .tar.gz) ver="${base#*-}" echo "sdist=$(basename "$sdist")" >> "$GITHUB_OUTPUT" @@ -176,7 +176,7 @@ jobs: env: # HOMEBREW_TAP_PAT: fine-scoped PAT with PR-write to the tap # (the default GITHUB_TOKEN can't push cross-repo). Both live - # on THIS repo, not the tap — the workflow runs here. + # on THIS repo, not the tap: the workflow runs here. # HOMEBREW_TAP_REPO = ben-dev-au/homebrew-tap. GH_TOKEN: ${{ secrets.HOMEBREW_TAP_PAT }} TAP_REPO: ${{ vars.HOMEBREW_TAP_REPO }} @@ -184,7 +184,7 @@ jobs: if: ${{ vars.HOMEBREW_TAP_REPO != '' }} run: | set -euo pipefail - # Resolve url + sha256 from PyPI's JSON API — one source of + # Resolve url + sha256 from PyPI's JSON API: one source of # truth, immune to name normalisation and the hashed storage # path. The index can lag the publish by a few seconds, so retry. meta="" @@ -212,7 +212,7 @@ jobs: existing_url=$(sed -nE 's/^ url "([^"]+)"/\1/p' "$formula" | head -n1) existing_sha=$(sed -nE 's/^ sha256 "([^"]+)"/\1/p' "$formula" | head -n1) if [ "$existing_url" = "$new_url" ] && [ "$existing_sha" = "$new_sha" ]; then - echo "Homebrew formula already at $VERSION — leaving its bottle block alone" + echo "Homebrew formula already at $VERSION, leaving its bottle block alone" exit 0 fi sed -i.bak "s|^ url \".*\"| url \"$new_url\"|" "$formula" @@ -220,7 +220,7 @@ jobs: # Drop the PREVIOUS version's `bottle do` block. Homebrew derives the # bottle filename from the formula version, so a bumped url + a stale # root_url makes every `brew upgrade fnd` 404 until the tap's bottle - # workflow runs. With no block, brew builds from source instead — + # workflow runs. With no block, brew builds from source instead: # slower, but never broken. The bottle job re-adds it via # `brew bottle --merge --write`. No-op when the block is absent. sed -i.bak "/^ bottle do$/,/^ end$/d" "$formula" @@ -233,7 +233,7 @@ jobs: if git diff --cached --quiet; then # Re-run for a version whose formula is already current: `git commit` # would exit 1 on an empty commit and fail the job before the PR. - echo "Homebrew formula already at $VERSION — nothing to bump" + echo "Homebrew formula already at $VERSION: nothing to bump" exit 0 fi git -c user.email="actions@github.com" -c user.name="fnd-release-bot" \ @@ -244,7 +244,7 @@ jobs: --body "Automated bump from ben-dev-au/fnd@${GITHUB_REF##*/}. sha256 \`$new_sha\`.") # Merge it here rather than leaving it for a human. The diff is # machine-generated from PyPI's own metadata and the tap runs no PR - # checks, so the manual gate bought nothing — and forgetting it is + # checks, so the manual gate bought nothing, and forgetting it is # what left 0.0.5's formula serving a 404 for twelve hours. Merging # is also what triggers the tap's bottle build. gh pr merge "$pr" --squash --delete-branch @@ -283,7 +283,7 @@ jobs: # a bare grep. [ "$installed" -eq 1 ] \ || { echo "::error::fndr $VERSION never became installable"; exit 1; } - # Whole line, not substring — 0.0.50 contains 0.0.5. + # Whole line, not substring: 0.0.50 contains 0.0.5. printf '%s\n' "$out" | grep -qFx "$VERSION" \ || { echo "::error::\`fnd version\` did not report exactly $VERSION"; exit 1; } echo "PyPI install OK: fndr $VERSION" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 66128506..6ad7c060 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -6,7 +6,7 @@ on: pull_request: branches: [main] schedule: - # Weekly Monday 09:00 UTC — catches CVEs published since the last + # Weekly Monday 09:00 UTC, catches CVEs published since the last # human-driven push. - cron: "0 9 * * 1" @@ -36,8 +36,8 @@ jobs: # `--no-emit-project` drops the local `fnd` package (which uv # emits as `-e .`); pip-audit can't look up an unpublished # editable install and would fail the whole audit. Once Phase - # 0 of the publish runbook lands `fnd` on PyPI, the flag stays - # — we don't need to audit ourselves, only our deps. + # 0 of the publish runbook lands `fnd` on PyPI, the flag stays: + # we don't need to audit ourselves, only our deps. run: | uv export \ --format requirements-txt \ diff --git a/.gitleaksignore b/.gitleaksignore index 87238a26..c81ab092 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -4,6 +4,6 @@ # scans the full commit range, not just HEAD. # 2. The note above used to quote that fixture verbatim, so this file # tripped the very rule it suppresses. Reworded, but the original -# commit is immutable — hence the second fingerprint. +# commit is immutable, hence the second fingerprint. ac26e70dc720c7a3b44a5a09497a395814514b4f:tests/test_extraction_cache.py:generic-api-key:84 113b88e9f185faf64456691a7e90d860862d232a:.gitleaksignore:generic-api-key:1 diff --git a/fnd/_perms.py b/fnd/_perms.py index a10583a6..1261b3f7 100644 --- a/fnd/_perms.py +++ b/fnd/_perms.py @@ -4,7 +4,7 @@ By default ``Path.mkdir(parents=True)`` honours the process umask (0o022 on macOS), which leaves directories world-readable. fnd's ``config.toml`` and on-disk state contain absolute paths to the user's -private documents — readable by any other local account on a shared +private documents: readable by any other local account on a shared Mac. These helpers normalise everything we create to 0o700 dirs and 0o600 files so the published binary doesn't bake in the "single-user Mac" assumption. @@ -16,7 +16,7 @@ Platform note: POSIX modes are only meaningful on macOS/Linux. On Windows ``os.chmod`` merely toggles the read-only bit (the 0o700/0o600 modes are a -no-op there), so this hardening is a POSIX-only guarantee — but the Windows +no-op there), so this hardening is a POSIX-only guarantee, but the Windows per-user profile root (``%LOCALAPPDATA%``) is already ACL-scoped to its owner, which provides the equivalent protection.""" @@ -31,7 +31,7 @@ def _chmod_quiet(p: Path, mode: int) -> None: """Best-effort chmod. Some filesystems (smbfs, exfat, some external drives) don't honour POSIX modes; swallowing the error is the right - call — we tried.""" + call: we tried.""" with contextlib.suppress(OSError): os.chmod(p, mode) @@ -43,7 +43,7 @@ def secure_mkdir(path: Path, *, anchor: Path | None = None) -> Path: Returns ``path`` so callers can chain. """ if anchor is None: - # Lazy import — ``fnd.config`` imports back from this module. + # Lazy import: ``fnd.config`` imports back from this module. from fnd.config import app_data_dir anchor = app_data_dir() @@ -53,7 +53,7 @@ def secure_mkdir(path: Path, *, anchor: Path | None = None) -> Path: try: rel = path.relative_to(anchor) except ValueError: - # Caller asked for a dir outside our anchor — only chmod the leaf. + # Caller asked for a dir outside our anchor: only chmod the leaf. _chmod_quiet(path, 0o700) return path cur = anchor diff --git a/fnd/apps.py b/fnd/apps.py index 706cd588..cb9a1a5d 100644 --- a/fnd/apps.py +++ b/fnd/apps.py @@ -1,13 +1,13 @@ -"""Apps registry — resolves a (kind, source) hit to an app and dispatches. +"""Apps registry: resolves a (kind, source) hit to an app and dispatches. Three layers: -* ``BUILTIN_APPS`` — frozen registry of ship-default apps (``system``, +* ``BUILTIN_APPS``: frozen registry of ship-default apps (``system``, ``preview``, ``skim``, ``pdf_expert``, ``obsidian``, ``vscode``). Each entry has its own ``handler`` closure that constructs an argv or URL, validates inputs, and dispatches through :mod:`fnd.launcher` (never a shell; output discarded, never raising). -* :func:`load_user_apps` — turns ``[apps.]`` TOML tables into ``App`` +* :func:`load_user_apps`, turns ``[apps.]`` TOML tables into ``App`` records whose handlers do pure :py:meth:`str.format` substitution into the user's argv or URL template. Mutually-exclusive ``argv``/``url`` is enforced; ids and ``handles`` are constrained to safe character sets. @@ -70,7 +70,7 @@ class App: positional: bool notes: str = "" # False for entries that act on the file without opening it (``reveal``). - # They belong in the Open-with picker but not in the default-app pickers — + # They belong in the Open-with picker but not in the default-app pickers: # choosing one as a kind's default would stop Open opening that kind at all. selectable_default: bool = True @@ -200,7 +200,7 @@ def _reset_ax_cache() -> None: # pyright: ignore[reportUnusedFunction] def _probe_ax_trusted() -> bool: - """Real AX probe — runs a no-op System Events osascript. Treats a + """Real AX probe: runs a no-op System Events osascript. Treats a non-zero exit code as "not trusted". Slow (~150-300 ms), so the result is cached for the lifetime of the process via :func:`ax_trusted`. @@ -241,7 +241,7 @@ def set_notice_sink(sink: Callable[[str], None] | None) -> None: def _emit_notice(message: str) -> None: - """One-shot user-facing notice — dedup'd by message text so the same + """One-shot user-facing notice: dedup'd by message text so the same fallback warning never spams the log. Routed through :func:`set_notice_sink` when a TUI sink is registered.""" if message in _notice_seen: @@ -345,23 +345,23 @@ def _handle_skim(req: OpenRequest) -> int: # Embedded AppleScript that page-jumps an ALREADY-OPEN Preview document. # -# It does NOT open the file — _handle_preview opens it first via +# It does NOT open the file: _handle_preview opens it first via # ``open -a Preview`` (LaunchServices). Opening from osascript # (``open POSIX file``) is attributed to the parent terminal's TCC identity # and gets denied for files carrying ``com.apple.macl`` on Desktop/Documents # ("you don't have permission to view it"); LaunchServices opens under # Preview's own identity and isn't. So this script only drives the open -# document — it never touches the filesystem. +# document: it never touches the filesystem. # # Correctness invariants (a regression here paged the WRONG document): -# 1. Confirm by EXACT POSIX path, not substring — `front document` must +# 1. Confirm by EXACT POSIX path, not substring: `front document` must # equal pdfPath ("report.pdf" must not match "old_report.pdf"). # 2. `open -a Preview` already opens AND fronts the target, so we just wait # for the front document to BE the target rather than re-fronting a # window by name (which is ambiguous when two open PDFs share a filename, # e.g. the same paper on the Desktop and under Documents). # 3. Fail safe: if the target isn't confirmed front within the retry budget, -# send NO keystrokes — the pdf still opened on page 1. +# send NO keystrokes; the pdf still opened on page 1. _PREVIEW_PAGE_JUMP_SCRIPT: Final[str] = r""" on run argv set pdfPath to item 1 of argv @@ -384,7 +384,7 @@ def _handle_skim(req: OpenRequest) -> int: set tries to tries + 1 end repeat -- Fail-safe: only page when our document is confirmed front. Otherwise - -- the PDF still opened (on page 1) — better than paging the wrong one. + -- the PDF still opened (on page 1): better than paging the wrong one. if matched then tell application "Preview" to activate delay 0.15 @@ -418,7 +418,7 @@ def _handle_preview(req: OpenRequest) -> int: ``open POSIX file`` from osascript is attributed to the parent terminal and gets denied for files on Desktop/Documents, whereas LaunchServices opens under Preview's own identity. With a page locator AND Accessibility - granted, a second osascript call then page-jumps the now-open document — + granted, a second osascript call then page-jumps the now-open document: it never touches the filesystem. Without AX (or no page), the PDF is left on page 1 and a one-shot Accessibility notice is emitted. @@ -445,7 +445,7 @@ def _handle_preview(req: OpenRequest) -> int: def _handle_pdf_expert(req: OpenRequest) -> int: """Open the PDF in PDF Expert. - Earlier I tried a ``pdf-expert-7://`` URL — that scheme isn't + Earlier I tried a ``pdf-expert-7://`` URL; that scheme isn't registered (the bundle's Info.plist exposes ``pdfexpert://`` only) and any URL it does accept isn't publicly documented in a stable form. ``open -a "PDF Expert" `` is the reliable invocation @@ -464,7 +464,7 @@ def _heading_path_to_anchor(heading_path: str) -> str: nested-heading anchor format ("A#B#C"). Obsidian's wiki-link syntax uses ``#`` between successive heading - levels — ``[[Note#A#B#C]]`` navigates to the C heading inside B + levels: ``[[Note#A#B#C]]`` navigates to the C heading inside B inside A. The chained form is more specific than the leaf alone: if the file has multiple headings literally named "C", only the one nested under A→B will match. @@ -481,7 +481,7 @@ def _advanced_uri_available(source_path: Path | None) -> bool: contains ``source_path``. Walks up from ``source_path`` looking for a ``.obsidian/plugins/`` - directory — the same detection pattern :func:`detect_obsidian_vault` + directory: the same detection pattern :func:`detect_obsidian_vault` uses for the vault root, just one level deeper. Returns ``False`` when ``source_path`` is unset, isn't under any vault, or the plugin folder is missing. Cheap (~one ``stat`` per parent walked). @@ -515,13 +515,13 @@ def _resolve_match_line( Used by deep-link handlers that support line jumps (Obsidian Advanced URI, VS Code) so the user lands on the matched word rather than the chunk's heading. The chunk's ``line`` is the - heading line — a long section can drop the user 50+ lines above + heading line: a long section can drop the user 50+ lines above their match without this resolution step. Conservative on fuzzy: only literal whole-word query tokens are matched, so a query like ``template~2`` matching the file's ``templating`` falls back to the chunk start. That's - intentional — the goal is to *improve precision when we can*, + intentional: the goal is to *improve precision when we can*, not guarantee it. """ if not query or from_line < 1: @@ -554,26 +554,26 @@ def _handle_obsidian(req: OpenRequest) -> int: the only path that lands the cursor on the *matched line* rather than at the top of the section. Built-in URL forms can only target headings, so a match deep inside a long section drops the user - lines above their match — see :func:`_advanced_uri_available`. + lines above their match: see :func:`_advanced_uri_available`. * **Vault set, no plugin (or no line)**: ``obsidian://open?vault=X &file=Y#A#B#C``. Heading anchor uses Obsidian's chained-heading syntax via :func:`_heading_path_to_anchor`. * **No vault**: ``obsidian://open?path=[#A#B#C]``. Obsidian routes the path to whichever vault contains it. We never fall - back to ``open `` — the user explicitly picked Obsidian and + back to ``open ``: the user explicitly picked Obsidian and a silent app-swap is misleading. """ use_advanced = req.line > 0 and _advanced_uri_available(req.source_path) if req.vault and use_advanced: filepath = req.file_in_vault or str(req.path) # Resolve the chunk's heading line forward to the actual matched - # line when a query is in play — Advanced URI's ``line=N`` jumps + # line when a query is in play: Advanced URI's ``line=N`` jumps # the cursor to exactly that line, so we get word-precision # instead of section-precision. target_line = _resolve_match_line(req.path, req.query, req.line) # ``openmode=tab`` opens the match in a NEW tab instead of navigating # the active one (which silently loses whatever the user had open). - # Current Advanced URI param — supersedes the deprecated ``newpane``. + # Current Advanced URI param: supersedes the deprecated ``newpane``. url = ( "obsidian://advanced-uri" f"?vault={urllib.parse.quote(req.vault, safe=_PCT_SAFE)}" @@ -606,7 +606,7 @@ def _handle_vscode(req: OpenRequest) -> int: def _handle_zathura(req: OpenRequest) -> int: - """``zathura --page N `` — page-jump on Linux (``--page`` is 1-based).""" + """``zathura --page N ``: page-jump on Linux (``--page`` is 1-based).""" argv = ( ["zathura", "--page", str(req.page), str(req.path)] if req.page > 0 @@ -616,7 +616,7 @@ def _handle_zathura(req: OpenRequest) -> int: def _handle_okular(req: OpenRequest) -> int: - """``okular --page N `` — page-jump on Linux/KDE (``--page`` is 1-based).""" + """``okular --page N ``: page-jump on Linux/KDE (``--page`` is 1-based).""" argv = ( ["okular", "--page", str(req.page), str(req.path)] if req.page > 0 @@ -626,7 +626,7 @@ def _handle_okular(req: OpenRequest) -> int: def _handle_sumatra(req: OpenRequest) -> int: - """``SumatraPDF.exe -page N `` — page-jump on Windows.""" + """``SumatraPDF.exe -page N ``: page-jump on Windows.""" exe = _sumatra_exe() if exe is None: # available() gates this; defensive fall-through only return launcher.open_path(req.path) @@ -696,7 +696,7 @@ def _handle_sumatra(req: OpenRequest) -> int: positional=True, notes="`code -g ::1`; requires the `code` CLI in PATH.", ), - # Cross-platform PDF viewers — each self-gates via ``available`` (a + # Cross-platform PDF viewers: each self-gates via ``available`` (a # ``which`` / install-path probe), so it appears only on the OS where it # is installed. Registering them all in one table keeps the registry # OS-agnostic; the probes do the filtering. @@ -729,7 +729,7 @@ def _handle_sumatra(req: OpenRequest) -> int: ), # Last so the picker lists the apps that actually open the file first. # Resolved eagerly rather than via an ``os_labels`` placeholder: unlike help - # text, ``display_name`` feeds logic — ``letter_shortcuts`` derives the + # text, ``display_name`` feeds logic, ``letter_shortcuts`` derives the # picker's access key from its first free letter. "reveal": App( id="reveal", @@ -838,7 +838,7 @@ def _run_url(req: OpenRequest) -> int: def load_user_apps(raw_apps: dict[str, dict[str, Any]] | None) -> dict[str, App]: """Build an ``App`` per ``[apps.]`` table. Raises ``ValueError`` on - the first invalid entry — partial registries hide bugs.""" + the first invalid entry: partial registries hide bugs.""" out: dict[str, App] = {} if not raw_apps: return out @@ -849,7 +849,7 @@ def load_user_apps(raw_apps: dict[str, dict[str, Any]] | None) -> dict[str, App] display_name=spec.display_name, handles=spec.handles, handler=_make_user_handler(spec), - available=lambda: True, # user-declared — assume present + available=lambda: True, # user-declared: assume present positional="{page}" in (spec.url or "") or any("{line}" in t or "{page}" in t for t in (spec.argv or ())), ) @@ -858,7 +858,7 @@ def load_user_apps(raw_apps: dict[str, dict[str, Any]] | None) -> dict[str, App] def build_registry(cfg: Any) -> dict[str, App]: """Combine BUILTIN_APPS with the user-defined apps on ``cfg``. User - entries with the same id as a built-in win — gives users a way to + entries with the same id as a built-in win: gives users a way to override a built-in's template without touching code.""" user_raw = getattr(cfg, "apps", None) or {} user_built = load_user_apps( @@ -918,7 +918,7 @@ def detect_obsidian_vault_path(path: Path) -> Path | None: root, or ``None`` if no vault is found before the filesystem root. Used by the opener to compute ``file_in_vault`` relative to the - actual vault root — which Obsidian's Advanced URI expects — even + actual vault root, which Obsidian's Advanced URI expects, even when the source's ``path`` is a subdirectory of the vault (a common layout for collections that index only one project's notes out of a larger vault). diff --git a/fnd/cache.py b/fnd/cache.py index cacdef9f..7ffdf8f2 100644 --- a/fnd/cache.py +++ b/fnd/cache.py @@ -11,7 +11,7 @@ (~10ms for a 5MB file on M1 Max) is rounding error. History: directory was previously ``$XDG_CACHE_HOME/fnd/extraction/``; -class previously ``ExtractionCache``. Both renamed for clarity — this +class previously ``ExtractionCache``. Both renamed for clarity: this is fnd's PDF structure cache, not a generic extractor's. Old directory is migrated on first launch by :func:`_migrate_legacy_cache_dir` and ``ExtractionCache`` remains as a deprecated alias for one release. @@ -90,9 +90,9 @@ def _safe_mtime(path: Path) -> float: # Windows rejects <>:"/\|?* in filenames; a cache key embeds an extractor # signature that can contain ``|`` (see fnd.extract.pdf.extractor_signature). # Percent-encode each illegal char there. This is: -# * reversible & collision-free — each illegal char maps to a unique token, +# * reversible & collision-free, each illegal char maps to a unique token, # so distinct keys never alias (``a|b`` → ``a%7Cb`` ≠ ``a_b``); -# * idempotent — the ``%XX`` tokens contain no illegal char, so re-encoding a +# * idempotent; the ``%XX`` tokens contain no illegal char, so re-encoding a # sanitised stem is a no-op, which the glob→entry_path round-trip relies on. # macOS/Linux keep the raw key so existing caches aren't orphaned. _WIN_ILLEGAL_STEM = str.maketrans( @@ -127,13 +127,13 @@ class PdfStructureCache: Writes are atomic via ``os.replace`` from a tmpfile in the same directory, so a Ctrl+C during the write either leaves the previous entry intact (most likely) or no entry at all (worst - case the next extraction recomputes — still correct). + case the next extraction recomputes: still correct). """ def __init__(self, root: Path | None = None) -> None: self.root = root or default_cache_dir() # Per-instance counters for indexer-runner progress reporting. - # Not thread-safe — caller should snapshot before reading from + # Not thread-safe: caller should snapshot before reading from # another task. self.hits = 0 self.misses = 0 @@ -165,7 +165,7 @@ def get(self, key: str, *, accept: AcceptEntry | None = None) -> list[Chunk] | N and overwrites on next put(). ``accept`` refuses an entry on what it holds and the engine recorded with - it — policy the cache has no business knowing. A refusal is a miss. + it: policy the cache has no business knowing. A refusal is a miss. """ path = self.entry_path(key) if not path.exists(): @@ -206,7 +206,7 @@ def get_any_for_content( shard_dir = self.root / content_sha256[:2] candidates = ( # Guard stat(): an entry can be deleted (cache prune / manual - # cleanup) between glob and stat — missing → sorts oldest, the + # cleanup) between glob and stat: missing → sorts oldest, the # subsequent get() then misses cleanly rather than crashing. sorted(shard_dir.glob(f"{content_sha256}--*.json"), key=_safe_mtime, reverse=True) if shard_dir.exists() @@ -254,11 +254,11 @@ def count_orphans(self, live_content_shas: set[str]) -> int: ) def prune_orphans(self, live_content_shas: set[str]) -> int: - """Delete entries whose content hash is NOT in ``live_content_shas`` - — texturings for files no longer present under any collection's - sources (removed, renamed to different bytes, or de-configured). - Content-addressed, so the caller passes the shas of files that - still exist. Returns the number of entries removed.""" + """Delete entries whose content hash is NOT in ``live_content_shas``: + texturings for files no longer present under any collection's + sources (removed, renamed to different bytes, or de-configured). + Content-addressed, so the caller passes the shas of files that + still exist. Returns the number of entries removed.""" if not self.root.exists(): return 0 removed = 0 @@ -283,7 +283,7 @@ def promote_current_engine_entries( configs are left untouched so they correctly read as outdated. Idempotent. Returns ``(migrated, failed)``. The caller should only - mark the migration done (write its sentinel) when ``failed == 0`` — + mark the migration done (write its sentinel) when ``failed == 0``, otherwise a partial promotion would permanently misclassify the un-promoted current-engine entries as outdated. The signature strings are passed in so this module stays independent of @@ -385,7 +385,7 @@ def _chunk_from_dict(d: dict[str, Any]) -> Chunk: body=d.get("body", ""), body_struct=blocks, body_md=d.get("body_md", ""), - # .get() not [] — the cache is durable and entries written before + # .get() not []: the cache is durable and entries written before # these fields existed must still load. created=d.get("created", 0), inode_changed=d.get("inode_changed", 0), diff --git a/fnd/cascade.py b/fnd/cascade.py index 8a5340fd..332675ca 100644 --- a/fnd/cascade.py +++ b/fnd/cascade.py @@ -3,9 +3,9 @@ Three widening passes are tried in order: 0. literal: query as the user typed it (already stem-aware via the index analyser) - 1. fuzzy — Lucene-style "rewrite" fuzzy: enumerate the F_BODY term + 1. fuzzy: Lucene-style "rewrite" fuzzy: enumerate the F_BODY term dictionary for indexed stems within the per-term auto-distance - (0 for ≤2 chars, 1 for 3-5, 2 for ≥6 — same shape as Lucene's + (0 for ≤2 chars, 1 for 3-5, 2 for ≥6, same shape as Lucene's ``fuzziness=AUTO``), then issue an OR of regular ``term_query`` for each matching stem. This is the same rewrite Lucene applies to ``MultiTermQuery`` so the matched docs land back on BM25 @@ -13,7 +13,7 @@ ``fuzzy_term_query`` returns. A single-character prefix anchors the dictionary scan so a 6-char stem with distance 2 doesn't bring in unrelated short words. - 2. synonym — terms in any synonym group expand to (term OR sym1 OR sym2) + 2. synonym: terms in any synonym group expand to (term OR sym1 OR sym2) A pass runs only if the cumulative deduplicated hit count is below the caller-supplied threshold. Hits surfaced first by an earlier pass keep @@ -63,7 +63,7 @@ def _carries_precision_intent(query: str) -> bool: * a quoted phrase, ``{N}`` proximity, or ``NEAR/N``; * a ``*``/``?`` wildcard or a ``/regex/`` (the fuzzy pass strips these and - fuzzy-matches the bare stem — ``crypto*`` would re-admit ``cryptid``); + fuzzy-matches the bare stem: ``crypto*`` would re-admit ``cryptid``); * an explicit exclusion (``NOT x`` / ``-x``): the fuzzy pass strips the operator and would re-admit the excluded docs. """ @@ -87,7 +87,7 @@ def _fuzzy_stands_down(query: str) -> bool: _FUZZY_TOKEN_RE = re.compile(r"^(\w+)(~(\d*))?$") # Strip ``~N`` (and bare trailing ``~``) only when preceded by a word -# char — preserves phrase-proximity ``"a b"~3`` (~ after a quote). +# char: preserves phrase-proximity ``"a b"~3`` (~ after a quote). _STRIP_FUZZY_MOD_RE = re.compile(r"(?<=\w)~\d*") @@ -98,7 +98,7 @@ def _terms_with_fuzzy(query: str) -> list[tuple[str, int | None]]: Returns ``(term, explicit_distance | None)`` tuples, None for no modifier. Bare ``~`` is the automatic distance, as the compiled query searches it. ``~N`` is clamped to ``{1, 2}``. - Quoted phrases are stripped — proximity (``"a b"~3``) isn't fuzzy. + Quoted phrases are stripped: proximity (``"a b"~3``) isn't fuzzy. """ if not query: return [] @@ -482,7 +482,7 @@ def _trace( def _with_pass(h: Hit, pass_index: int) -> Hit: """Return a copy of ``h`` tagged with ``pass_index``. Hits are frozen - dataclasses, so we copy rather than mutate — via ``dataclasses.replace``, + dataclasses, so we copy rather than mutate, via ``dataclasses.replace``, which cannot drop a field the way an enumerated rebuild does (see :func:`fnd.fusion._with_pass_index`).""" return dataclasses.replace(h, pass_index=pass_index) diff --git a/fnd/cli.py b/fnd/cli.py index 4ac9f87b..edcc0994 100644 --- a/fnd/cli.py +++ b/fnd/cli.py @@ -3,11 +3,11 @@ Phases 1-3 surface: - ``fnd version`` -- ``fnd index `` — quick ad-hoc index of a single root into the +- ``fnd index ``: quick ad-hoc index of a single root into the default collection -- ``fnd search ""`` — query the index -- ``fnd config show|edit|validate`` — manage ~/Library/Application Support/fnd/config.toml -- ``fnd collection list|add|rm|reindex`` — manage collections +- ``fnd search ""``, query the index +- ``fnd config show|edit|validate``, manage ~/Library/Application Support/fnd/config.toml +- ``fnd collection list|add|rm|reindex``, manage collections """ from __future__ import annotations @@ -215,7 +215,7 @@ def parse_filter_flags( resolve_or_exit(found) # Expand category ids (e.g. "code" → its member language kinds) and de-dupe, - # so BOTH `search` and `tui` seed the index-compatible fine-grained ids — + # so BOTH `search` and `tui` seed the index-compatible fine-grained ids: # F_KIND never stores a category, so a raw ``kind:code`` clause matches # nothing. Centralised here so the two command surfaces can't drift. kind_values: list[str] = [] @@ -294,7 +294,7 @@ def tui( raise typer.Exit(code=1) from e # Ahead of the rebuild prompt and the worker pool, and well ahead of - # Textual taking the screen — the prompt has to land on a plain terminal. + # Textual taking the screen: the prompt has to land on a plain terminal. _check_query_filters(initial_query, cfg, issues) collection = resolve_launch_collection(collection, cfg, issues) resolve_or_exit(issues) @@ -422,7 +422,7 @@ def search( # ``flags.kinds`` is already category-expanded + de-duped by # parse_filter_flags. All values collapse into ONE kind:(a b …) OR-group # (F_KIND stores fine-grained ids) so multiple --kind flags match ANY of the - # kinds — matching the TUI. Emitting one clause per flag would AND them and + # kinds, matching the TUI. Emitting one clause per flag would AND them and # match nothing (a chunk has a single kind). if flags.kinds: kv = list(flags.kinds) @@ -436,7 +436,7 @@ def search( prompt_and_rebuild_or_exit(index_dir=default_index_dir(), config=cfg) - # Tags never enter the query string — see fnd/tag_query.py. They are + # Tags never enter the query string: see fnd/tag_query.py. They are # normalised the same way indexed tags were, then passed as typed state. tag_filter = None if flags.tags or flags.not_tags: @@ -521,7 +521,7 @@ def search( def _check_query_filters(query: str, cfg: Config, issues: FilterIssues) -> None: """Record any ``c:`` / ``kind:`` value typed inside the query itself. - These are reported but never rewritten — see ``fnd.cli_scope``. The + These are reported but never rewritten: see ``fnd.cli_scope``. The ``all`` pseudo-name isn't valid in the DSL (it would be a literal collection name), so it isn't special-cased here. @@ -632,7 +632,7 @@ def config_validate() -> None: f"✓ {path} valid; {len(cfg.collections)} collection(s): " f"{', '.join(sorted(cfg.collections)) or '(none)'}" ) - # Not a schema error — the file parses — but a default naming a collection + # Not a schema error (the file parses) but a default naming a collection # that no longer exists silently widens every fresh launch to everything. want = cfg.defaults.collection if want and not is_all_collections(want, known=set(cfg.collections)): @@ -836,7 +836,7 @@ def collection_reindex( resolve_or_exit(issues) # Reached only when the user accepted the proposal. Start a fresh issue # set: the accepted one is still in `issues`, and resolving `raw` below - # runs resolve_or_exit again — which would re-offer what they just said + # runs resolve_or_exit again, which would re-offer what they just said # yes to. issues = FilterIssues() raw = "all" @@ -1035,7 +1035,7 @@ def extras_uninstall( raise typer.Exit(code=2) _print_uninstall_disclosure(extra) - # --dry-run is a planning preview — show the full plan as if + # --dry-run is a planning preview: show the full plan as if # everything were installed. The real uninstall (below) uses the # filtered version that skips already-removed packages. if dry_run: diff --git a/fnd/cli_scope.py b/fnd/cli_scope.py index 84230718..dcbed8e4 100644 --- a/fnd/cli_scope.py +++ b/fnd/cli_scope.py @@ -9,7 +9,7 @@ The rule that makes this safe: :meth:`FilterIssues.resolve` hands back the correction it would apply, and :func:`resolve_or_exit` only lets a command proceed when every recorded problem was correctable *and* confirmed. So a -caller can use the returned values directly — by the time it runs, they're +caller can use the returned values directly: by the time it runs, they're either the user's own or ones the user accepted. """ @@ -75,12 +75,12 @@ def resolve(self, vocab: Vocabulary, raw: str, *, flag: str | None = None) -> st return err.correction or raw def add(self, err: UnknownFilterValueError) -> None: - """Record an issue the caller built itself — a scope that is missing + """Record an issue the caller built itself: a scope that is missing rather than misspelt, say. Reported and offered like any other.""" self._issues.append(err) def check(self, vocab: Vocabulary, raw: str, *, flag: str | None = None) -> None: - """Record ``raw`` if it isn't legal, handing nothing back — for values + """Record ``raw`` if it isn't legal, handing nothing back: for values the CLI reports but has nothing to carry forward from.""" if vocab.match(raw) is None: self._issues.append(vocab.unknown(raw, flag=flag)) @@ -102,7 +102,7 @@ def split_resolve(self, vocab: Vocabulary, raw: str, *, flag: str | None = None) if not known and not unknown: # Separator-only input ("," / ", ,") names nothing at all. Callers # read an empty list as "unscoped", so staying quiet here would - # widen the search to every collection — the exact failure this + # widen the search to every collection: the exact failure this # module exists to prevent. self._issues.append(vocab.unknown(raw, flag=flag)) return list(dict.fromkeys(known)) @@ -120,8 +120,8 @@ def _collection_names( A config with no collections skips the *name* check: there's nothing to compare against, and an ad-hoc ``fnd index --collection`` name would be indistinguishable from a typo. It still has to name something, and it is - still comma-split — an unvalidated value is not an unparsed one. (The TUI - can't scope to such a name either way — see the note in + still comma-split: an unvalidated value is not an unparsed one. (The TUI + can't scope to such a name either way: see the note in ScopeController._valid_collection_names.) """ if raw is None: @@ -134,7 +134,7 @@ def _collection_names( if not names: # `-c ""` / `-c " "` (usually an unset shell variable) or `-c ","`. # Names nothing, so treating it as "no flag" would silently search - # everything — the failure this module exists to prevent. + # everything: the failure this module exists to prevent. issues.check(collection_vocabulary(config), raw, flag=flag) return names @@ -158,7 +158,7 @@ def resolve_launch_collection( *, flag: str = "--collection", ) -> str | None: - """The same value as the TUI wants it — one string. + """The same value as the TUI wants it: one string. ``all`` passes through untouched: the sidebar expands it against the live config, and collapsing it to None here would read as "no flag given" and @@ -184,7 +184,7 @@ def resolve_or_exit(issues: FilterIssues, *, is_tty: bool | None = None) -> None # A query token is reported but never rewritten: silently editing text the # user typed is more surprising than correcting a flag, and the message # already names the exact edit. One unfixable problem also cancels the - # offer for the rest — accepting would still leave a broken command. + # offer for the rest: accepting would still leave a broken command. fixable = [e for e in problems if e.flag is not None and e.correction is not None] all_fixable = len(fixable) == len(problems) if not all_fixable or not _interactive(is_tty): @@ -226,5 +226,5 @@ def _describe(err: UnknownFilterValueError) -> str: def _interactive(is_tty: bool | None) -> bool: if is_tty is not None: return is_tty - # Allow tests to force the TTY path — same escape hatch as fnd.migrate. + # Allow tests to force the TTY path: same escape hatch as fnd.migrate. return True if os.environ.get("_FND_FORCE_TTY") == "1" else sys.stdin.isatty() diff --git a/fnd/cloud_files.py b/fnd/cloud_files.py index e8ad242c..ac1d496f 100644 --- a/fnd/cloud_files.py +++ b/fnd/cloud_files.py @@ -1,4 +1,4 @@ -"""Cloud-backed placeholder files — is this file's data actually here? +"""Cloud-backed placeholder files: is this file's data actually here? Fourth platform seam, alongside :mod:`fnd.paths` (where files live), :mod:`fnd.launcher` (how to open them) and :mod:`fnd.os_labels` (what the @@ -7,12 +7,12 @@ Every sync client (iCloud Drive, OneDrive, Dropbox, Google Drive) can leave a *placeholder*: a normal-looking directory entry whose contents are not on -disk. Opening one blocks until the provider materialises it — seconds for a +disk. Opening one blocks until the provider materialises it: seconds for a note, minutes for a large PDF. The indexer needs to know that up front so it can tell the user what it is waiting for instead of looking wedged. Detection is a filesystem-attribute question, never a path question. On -macOS an evicted file carries ``SF_DATALESS`` wherever it lives — inside +macOS an evicted file carries ``SF_DATALESS`` wherever it lives: inside ``~/Library/Mobile Documents``, under "Desktop & Documents Folders" sync, or in a third-party provider mounted at ``~/Library/CloudStorage``. Attribution (*which* service) is the only path-dependent part, and it is cosmetic: it @@ -59,7 +59,7 @@ class _CancelSignal(Protocol): - """Anything with ``is_set()`` — an ``asyncio.Event`` read from a worker + """Anything with ``is_set()``: an ``asyncio.Event`` read from a worker thread (a plain bool read, safe) or a ``threading.Event``.""" def is_set(self) -> bool: ... @@ -94,7 +94,7 @@ class Materialisation(StrEnum): # Bound once so a test can swap the stat call without patching the shared # ``os`` module, which would hand a fake back to every caller in the -# interpreter — pytest's own bookkeeping included. +# interpreter: pytest's own bookkeeping included. _stat = os.stat # Path separators, both spellings: a Windows path inspected on POSIX (or a @@ -103,7 +103,7 @@ class Materialisation(StrEnum): _GENERIC_PROVIDER: Final = "cloud storage" -# Provider names, matched against the START of a path component — never as +# Provider names, matched against the START of a path component: never as # a bare substring, which made "box" claim ~/Documents/Inbox and Sandbox. # A prefix still catches the real spellings, which append an account: # "OneDrive-Personal", "OneDrive - Contoso", "GoogleDrive-user@host". @@ -123,7 +123,7 @@ class Materialisation(StrEnum): def materialisation(path: Path) -> Materialisation: """Whether ``path``'s bytes are local, remote, or unknowable here. - Never raises and never opens the file — a stat is enough, and opening + Never raises and never opens the file: a stat is enough, and opening is the very thing callers are trying to decide about. """ try: @@ -151,7 +151,7 @@ def materialisation(path: Path) -> Materialisation: def is_placeholder(path: Path) -> bool: """True only when the platform positively says the bytes are remote. - ``UNKNOWN`` reads as "assume local" — the alternative would have Linux + ``UNKNOWN`` reads as "assume local": the alternative would have Linux treat every file as cloud-backed. """ return materialisation(path) is Materialisation.PLACEHOLDER @@ -233,7 +233,7 @@ def fetch[T]( timeout_s: float, cancel: _CancelSignal | None = None, ) -> T: - """Run ``call`` — which will materialise ``path`` — under a deadline. + """Run ``call``, which will materialise ``path``, under a deadline. Publishes a :class:`FetchWait` for the duration so the UI can name what it is waiting on, and raises :class:`CloudFetchError` if the provider has @@ -241,7 +241,7 @@ def fetch[T]( ``cancel`` is polled while waiting, so a Cancel lands in well under a second instead of being held for the remainder of the timeout by a - stalled provider. Raises :class:`CloudFetchCancelledError` when it fires — + stalled provider. Raises :class:`CloudFetchCancelledError` when it fires; the run is ending, so the file is not a failure. The worker is a throwaway daemon thread rather than a pooled one: a diff --git a/fnd/data/synonyms_default.toml b/fnd/data/synonyms_default.toml index 47b21fa9..4e171c3f 100644 --- a/fnd/data/synonyms_default.toml +++ b/fnd/data/synonyms_default.toml @@ -1,10 +1,10 @@ -# Default synonym table — curated security/tech acronyms and tight synonyms. +# Default synonym table: curated security/tech acronyms and tight synonyms. # Query-time expansion only (no reindex). Groups are bidirectional: any one # form expands to the rest. Multi-word forms are auto-quoted as phrases. # # Scope is deliberately narrow: high-value, low-noise initialisms and exact # expansions sourced from public glossaries (NIST CSRC, CISA, MITRE, OWASP). -# This is NOT a general thesaurus — broad English synonyms hurt precision in +# This is NOT a general thesaurus: broad English synonyms hurt precision in # technical search. Users extend this via their own synonyms.toml. [synonyms.identity] diff --git a/fnd/explain.py b/fnd/explain.py index df5c830f..a175bce8 100644 --- a/fnd/explain.py +++ b/fnd/explain.py @@ -4,7 +4,7 @@ Off the hot path: trace objects are only built when ``with_trace=True`` flows through :func:`fnd.layered.search_layered`. The data inside is the same data that would otherwise be discarded after fusion / cascade -ran — no extra computation, just retention of internal state. +ran: no extra computation, just retention of internal state. """ from __future__ import annotations diff --git a/fnd/extract/_docling_daemon.py b/fnd/extract/_docling_daemon.py index 6b74303c..ad99e441 100644 --- a/fnd/extract/_docling_daemon.py +++ b/fnd/extract/_docling_daemon.py @@ -76,7 +76,7 @@ def shutdown(cls) -> None: def extract_page(self, pdf_path: Path, page_index: int, *, timeout_s: float = 45.0) -> str: """Send one (pdf, page) request; return the Markdown response. - Returns "" on any failure — caller decides how to handle + Returns "" on any failure: caller decides how to handle (typically: keep the pymupdf4llm output it already has). The blocking stdout.readline() is bounded by a timeout (read in a @@ -98,7 +98,7 @@ def extract_page(self, pdf_path: Path, page_index: int, *, timeout_s: float = 45 return "" # Read the response with a timeout. ``select()`` only accepts sockets # on Windows, so bound the blocking ``readline()`` with a throwaway - # daemon thread + a size-1 queue instead — portable across OSes. + # daemon thread + a size-1 queue instead: portable across OSes. result_q: queue.Queue[str | None] = queue.Queue(maxsize=1) stdout = self._proc.stdout @@ -148,7 +148,7 @@ def _docling_python() -> Path | None: def _spawn_helper(python: Path, ready_timeout_s: float = 60.0) -> subprocess.Popen[str] | None: # -I = isolated mode: don't add the script's dir to sys.path[0] and # ignore PYTHONPATH. The helper lives in fnd/extract/, which contains - # fnd's own pptx.py extractor — without isolated mode, docling's + # fnd's own pptx.py extractor, without isolated mode, docling's # `import pptx` resolves to that file instead of the docling-slim # venv's python-pptx package, breaking the helper at import time. proc = subprocess.Popen( diff --git a/fnd/extract/_docling_helper.py b/fnd/extract/_docling_helper.py index 8ff63080..ac8563b2 100644 --- a/fnd/extract/_docling_helper.py +++ b/fnd/extract/_docling_helper.py @@ -1,10 +1,10 @@ -"""Docling helper subprocess — runs under docling-slim's own tool venv. +"""Docling helper subprocess: runs under docling-slim's own tool venv. Reads PDF page requests from stdin as JSON lines; writes one JSON response per request to stdout. Loads the docling model once at startup. Used by `fnd.extract._docling_daemon.DoclingDaemon`. -NOT executed in fnd's project venv. fnd never imports this directly — +NOT executed in fnd's project venv. fnd never imports this directly: it's launched as a subprocess via `python `. Protocol: @@ -47,7 +47,7 @@ def _main() -> None: # whole page on Apple Silicon. Pass the device as a string so we # don't depend on the AcceleratorDevice enum's import path. If # AcceleratorOptions can't be imported at all (docling moved it - # again), skip forcing CPU rather than failing to boot — the + # again), skip forcing CPU rather than failing to boot: the # PYTORCH_ENABLE_MPS_FALLBACK env above still routes unsupported # ops to CPU. with contextlib.suppress(Exception): diff --git a/fnd/extract/_html.py b/fnd/extract/_html.py index 1e6804a9..bc6a0533 100644 --- a/fnd/extract/_html.py +++ b/fnd/extract/_html.py @@ -1,6 +1,6 @@ """XHTML/HTML → structure walker, shared by the epub and web extractors. -Parses with ``lxml.html`` (libxml2's forgiving HTML parser — it lowercases +Parses with ``lxml.html`` (libxml2's forgiving HTML parser: it lowercases tags and ignores XML namespaces, so EPUB XHTML and loose web HTML both come through as clean lowercase tags), then walks the document in reading order and feeds a :class:`~fnd.extract._sectioner.HeadingSectioner` so headings, lists, diff --git a/fnd/extract/_limits.py b/fnd/extract/_limits.py index cf5a70bb..631ed838 100644 --- a/fnd/extract/_limits.py +++ b/fnd/extract/_limits.py @@ -1,13 +1,13 @@ """Hard limits applied to untrusted documents before they reach a parser. Centralised so the same numbers cover DOCX, PPTX, frontmatter, and the -query layer — and so they're easy to tune from one place if a real-world +query layer, and so they're easy to tune from one place if a real-world file legitimately exceeds them. Numbers are conservative: a legitimate office document doesn't approach 500 MB uncompressed, doesn't have a single entry that expands 200× from its compressed size, and doesn't carry a 64 KB YAML frontmatter block. -A document that does trip a limit is either pathological or hostile — +A document that does trip a limit is either pathological or hostile, either way we'd rather refuse and tell the user than feed the parser. """ @@ -34,6 +34,6 @@ # TUI, but once we add Spotlight / URL-handler / --query-from-file the # query becomes attacker-controllable and these bounds matter. Tantivy's # parser will compile arbitrarily-deep boolean trees and arbitrarily- -# long phrase queries — the limits below cap both before parsing. +# long phrase queries: the limits below cap both before parsing. LIMIT_QUERY_BYTES: Final = 8 * 1024 # 8 KB raw query string LIMIT_QUERY_BOOLEAN_TOKENS: Final = 64 # AND/OR/NOT count diff --git a/fnd/extract/_worker.py b/fnd/extract/_worker.py index e10da0ed..b0cb8cb9 100644 --- a/fnd/extract/_worker.py +++ b/fnd/extract/_worker.py @@ -138,7 +138,7 @@ def shutdown_pool() -> None: finalisers actually close the pipe FDs. The previous ``wait=False`` plus no GC raced the next ``_get_pool()`` and left stale FDs visible to ``_posixsubprocess.fork_exec`` in the new worker spawn - ("bad value(s) in fds_to_keep") — the exact symptom of the + ("bad value(s) in fds_to_keep"): the exact symptom of the chained-extraction bug. """ import gc @@ -161,7 +161,7 @@ def _kill_pool_workers(pool: ProcessPoolExecutor) -> None: # a quirky look-up is much smaller than the cost of writing our # own pool. ``getattr`` with default keeps the call site safe if # the attribute name ever changes upstream. ``Process.kill`` is - # cross-platform (SIGKILL on POSIX, TerminateProcess on Windows) — + # cross-platform (SIGKILL on POSIX, TerminateProcess on Windows): # ``os.kill(pid, SIGKILL)`` would raise/no-op on Windows. procs = getattr(pool, "_processes", {}) or {} for proc in list(procs.values()): diff --git a/fnd/extract/base.py b/fnd/extract/base.py index 81bc1caf..1f0c35c8 100644 --- a/fnd/extract/base.py +++ b/fnd/extract/base.py @@ -5,7 +5,7 @@ ``parent_id`` so the query layer can group hits back into per-file results. :class:`ExtractError` is the single failure type that propagates out of -``extract``. It wraps both *rejections* (we refused to parse — encrypted, +``extract``. It wraps both *rejections* (we refused to parse: encrypted, decompression-bomb threshold tripped) and *crashes* (the parser raised something unhelpful). The indexer catches it and continues to the next file, so a single hostile or corrupt document can't deny indexing of @@ -98,7 +98,7 @@ class Chunk: # serialised-to-markdown content (set by the relevant extractor). # Empty string for formats without a structural renderer (pdf, txt). body_md: str = "" - # ``page`` is the *PDF page index* (1-based, sequential from cover) — + # ``page`` is the *PDF page index* (1-based, sequential from cover): # used by the opener to deep-link via Skim. ``page_label`` is the # *printed* page label as set in the PDF (e.g. "292" or "iv"); empty # when the PDF has no labels. The two diverge for any book with diff --git a/fnd/extract/data.py b/fnd/extract/data.py index f7d659d4..27f81721 100644 --- a/fnd/extract/data.py +++ b/fnd/extract/data.py @@ -1,9 +1,9 @@ """Data / config extractor. Two shapes, keyed by kind: -* ``csv`` / ``tsv`` — parsed with the stdlib ``csv`` module and rendered as GFM +* ``csv`` / ``tsv``: parsed with the stdlib ``csv`` module and rendered as GFM pipe tables (previewed as a DataTable), chunked by row groups. -* ``json`` / ``yaml`` / ``toml`` / ``xml`` / ``ini`` — **not interpreted**; the +* ``json`` / ``yaml`` / ``toml`` / ``xml`` / ``ini``, **not interpreted**; the raw text is line-windowed and fenced with its language for a syntax-highlighted preview. Keeping them uninterpreted is deliberate: it avoids pulling in a parser per format and still indexes + previews the content faithfully. diff --git a/fnd/extract/docx.py b/fnd/extract/docx.py index 370b890a..82dd3141 100644 --- a/fnd/extract/docx.py +++ b/fnd/extract/docx.py @@ -1,6 +1,6 @@ """DOCX extractor: one chunk per heading section. -Walks the body in document order — interleaving paragraphs and tables — +Walks the body in document order, interleaving paragraphs and tables, and accumulates body until a Heading 1/2/3 paragraph appears, at which point it flushes the current section as a chunk and starts a new one. Tracks an ancestor stack so each chunk ships its full ``heading_path`` @@ -93,7 +93,7 @@ def _list_info(para: Paragraph) -> tuple[str, int] | None: Three signal sources are consulted (in priority order): - 1. The paragraph style name — ``List Bullet``, ``List Bullet 2``… + 1. The paragraph style name: ``List Bullet``, ``List Bullet 2``… map to bullet at increasing depths; ``List Number`` etc. map to numbered. 2. The underlying ``w:pPr/w:numPr`` element (XML) for any paragraph @@ -214,7 +214,7 @@ def _extract_inner(path: Path, parent_id: str) -> Iterator[Chunk]: doc = Document(str(path)) except PackageNotFoundError as e: # `python-docx` raises this for encrypted / password-protected - # packages too — we can't reliably tell them apart, so the + # packages too: we can't reliably tell them apart, so the # umbrella message is the right shape. raise ExtractError(str(path), f"unreadable docx: {e}") from e yield from _walk_docx_body(path=path, parent_id=parent_id, times=times, doc=doc) @@ -222,7 +222,7 @@ def _extract_inner(path: Path, parent_id: str) -> Iterator[Chunk]: raise except Exception as e: # Parser libs raise everything from RuntimeError to opaque C - # extension errors. Broad except is intentional — the goal is + # extension errors. Broad except is intentional: the goal is # "don't let one bad doc stop the index build." raise ExtractError(str(path), f"{type(e).__name__}: {e}") from e diff --git a/fnd/extract/heading_fold.py b/fnd/extract/heading_fold.py index 36990d0a..8adb856a 100644 --- a/fnd/extract/heading_fold.py +++ b/fnd/extract/heading_fold.py @@ -2,7 +2,7 @@ A PDF page's heading is usually derived from the document TOC rather than read off the page, so it often isn't present in the page's own text. Baking -it into ``body`` makes the section findable by its heading — but only the +it into ``body`` makes the section findable by its heading, but only the chunk that *starts* the section may do so. ``_toc_heading_for_page`` returns the same heading for every page of a section, so an unconditional fold made every continuation page match a heading it neither owns nor renders: search @@ -10,11 +10,11 @@ The rule is therefore twofold: -* **Ownership** — a chunk owns its heading only when its ``heading_path`` +* **Ownership**: a chunk owns its heading only when its ``heading_path`` differs from the previously yielded chunk's. Continuation pages and the ``carry_heading`` sections that inherit attribution across a page break are left untouched; their heading stays searchable through ``F_HEADING_PATH``. -* **Parity** — when a chunk does own its heading, the text goes into all three +* **Parity**, when a chunk does own its heading, the text goes into all three representations (``body`` for search, ``body_struct`` and ``body_md`` for the two preview substrates), so anything the index can match, the preview can paint. @@ -60,7 +60,7 @@ def _leading_heading_key(body_md: str) -> str: class HeadingFolder: """Folds each chunk's own heading in, once, across all representations. - One instance per document — ownership is decided by comparing against the + One instance per document: ownership is decided by comparing against the previous chunk, so a shared instance would leak attribution between files. """ diff --git a/fnd/extract/markdown.py b/fnd/extract/markdown.py index 9fa42b5b..8a234413 100644 --- a/fnd/extract/markdown.py +++ b/fnd/extract/markdown.py @@ -5,7 +5,7 @@ Sampling``. Each chunk also carries the verbatim markdown source for its section in -``body_md`` — the TUI's structural preview renderer (Textual Markdown +``body_md``: the TUI's structural preview renderer (Textual Markdown widget) consumes this. ``body_struct`` continues to carry a flat list of plain-text Blocks for the snippet pipeline; the two are intentionally separate so the snippet path doesn't end up showing markdown markers like @@ -38,7 +38,7 @@ _md = MarkdownIt("commonmark").enable("table") # Token types that carry "real" content. A section is worth flushing as a -# chunk when *any* of these appeared inside it — even if no `inline` +# chunk when *any* of these appeared inside it, even if no `inline` # tokens did. Pre-fix the extractor silently dropped sections that # contained only a fenced code block (``code`` kind, no inline children), # losing them from the index entirely. @@ -75,7 +75,7 @@ def _flush_section( heading_path = " > ".join(heading_stack) # F_BODY = chunk's own visible text (own heading + paragraphs). # Ancestor heading_path is searched via F_HEADING_PATH as a boost, - # NOT baked into body — inheriting parent-heading text into every + # NOT baked into body: inheriting parent-heading text into every # descendant's body inflates scores and surfaces chunks whose # visible content has no match. return Chunk( @@ -169,8 +169,8 @@ def _section_source(source_lines: list[str], start_line: int, end_line: int) -> ``start_line`` is the line of the section's heading_open (inclusive), ``end_line`` is the line of the *next* heading_open (exclusive), or ``len(source_lines)`` for the final section. The slice is verbatim - so tables, fenced code, lists, blockquotes — everything markdown-it - parses — round-trips into the renderer untouched. + so tables, fenced code, lists, blockquotes, everything markdown-it + parses, round-trips into the renderer untouched. """ start = max(start_line, 0) end = max(min(end_line, len(source_lines)), start) @@ -195,7 +195,7 @@ def _blank_frontmatter(source: str) -> str: can be mistaken for a heading. Blanked rather than removed so every downstream line number still matches - the file on disk — F_LINE feeds deep links like ``code -g {path}:{line}``. + the file on disk: F_LINE feeds deep links like ``code -g {path}:{line}``. """ if not source.startswith("---"): return source @@ -240,7 +240,7 @@ def _extract_inner(path: Path) -> Iterator[Chunk]: tok = tokens[i] if tok.type == "heading_open": - # Source line of the new heading — defines the boundary + # Source line of the new heading: defines the boundary # for the *previous* section's slice. heading_line = tok.map[0] if tok.map else section_start_line # Flush the section we were accumulating, slicing source up @@ -325,7 +325,7 @@ def _extract_inner(path: Path) -> Iterator[Chunk]: i += 1 continue - # Other token types (lists, hr, tables, blockquotes) — we don't + # Other token types (lists, hr, tables, blockquotes): we don't # expand them into the legacy Block list (the inline children that # DO appear already cover the searchable text), but flag the section # as having content so a table-only section flushes as its own chunk. @@ -333,7 +333,7 @@ def _extract_inner(path: Path) -> Iterator[Chunk]: section_has_content = True i += 1 - # Final flush — slice to end of file. + # Final flush: slice to end of file. chunk = _flush_section( path=path, parent_id=parent_id, diff --git a/fnd/extract/notebook.py b/fnd/extract/notebook.py index a4554c2c..c40ffb1b 100644 --- a/fnd/extract/notebook.py +++ b/fnd/extract/notebook.py @@ -3,7 +3,7 @@ Markdown cells keep their source verbatim on ``body_md`` (rendered rich); code cells become a language fence with their text outputs appended. A running top-level markdown heading becomes each cell's ``heading_path`` so result -labels read sensibly. Parsed with the stdlib ``json`` module — no nbformat. +labels read sensibly. Parsed with the stdlib ``json`` module: no nbformat. """ from __future__ import annotations diff --git a/fnd/extract/odf.py b/fnd/extract/odf.py index 9d46e1c9..4c0e0d05 100644 --- a/fnd/extract/odf.py +++ b/fnd/extract/odf.py @@ -3,11 +3,11 @@ ODF files are ZIPs whose ``content.xml`` holds the body. We parse it with stdlib ``zipfile`` + ``lxml.etree`` (no odfpy) and, per subtype: -* ``.odt`` — walk ``office:text`` into the shared ``HeadingSectioner`` (headings +* ``.odt``: walk ``office:text`` into the shared ``HeadingSectioner`` (headings from ``text:h``/``text:outline-level``, paragraphs, lists, tables), the same structural shape as docx. -* ``.odp`` — one chunk per ``draw:page`` slide, like pptx. -* ``.ods`` — one chunk per ``table:table`` sheet rendered as a GFM table, +* ``.odp``: one chunk per ``draw:page`` slide, like pptx. +* ``.ods``: one chunk per ``table:table`` sheet rendered as a GFM table, honouring ``number-columns/rows-repeated`` so repeated empty cells don't explode into huge phantom ranges. """ diff --git a/fnd/extract/pdf.py b/fnd/extract/pdf.py index 646db137..366f5d74 100644 --- a/fnd/extract/pdf.py +++ b/fnd/extract/pdf.py @@ -2,7 +2,7 @@ Flow: -1. Try ``doc.get_toc()`` first — if the PDF has an embedded outline, that's +1. Try ``doc.get_toc()`` first, if the PDF has an embedded outline, that's ~100% accurate; map each page to the nearest preceding TOC entry. 2. Else fall back to local font-size clustering (``_font_clustering_heading``). 3. Apply sanity gates and bail to ``heading_path = ""`` when: @@ -54,14 +54,14 @@ from fnd.fsmeta import read_file_times # Lazy availability of the pdf-structure extra (`pymupdf4llm`). Computed -# at module load — cheap; just a spec lookup. The actual import happens +# at module load: cheap; just a spec lookup. The actual import happens # inside _extract_page_md() to keep import-time cost zero for users # who haven't opted in (NF7 invariant). # # Note: ``find_spec`` only tests importability, not functional # readiness. A half-uninstalled pymupdf4llm whose namespace directory # survives but whose ``to_markdown`` is gone will still pass this -# check. ``_extract_page_md`` is defensive — its outer try/except +# check. ``_extract_page_md`` is defensive: its outer try/except # catches the AttributeError that surfaces from such a state and # returns "" (the page falls through to flat extraction). The TUI's # Status row uses the stricter ``fnd.extras.is_package_installed`` @@ -71,7 +71,7 @@ # Runtime override for the structured-extraction path. The indexer sets # this to True for a single Update index run when the user has toggled -# "Update cache at index time" Off — extract() still consults the cache +# "Update cache at index time" Off: extract() still consults the cache # on hit, but on miss it runs only the flat path (no pymupdf4llm, # no docling) and skips the cache write. Used for fast battery-friendly # flat-text refreshes. Default False keeps existing behaviour. @@ -93,12 +93,12 @@ def set_skip_structure_extraction(skip: bool) -> None: # extraction-logic change). Minor refactors, flag documentation, and # patch-level dependency bumps must NOT bump it. Rebuild keys off this # version: entries below it read as outdated and re-texturise. -# v2: invisible-text scanned-page recovery (Bug E) — the layered recovery +# v2: invisible-text scanned-page recovery (Bug E); the layered recovery # pipeline meaningfully changes texturised output corpus-wide, so prior # textures read as outdated and re-texturise under the new engine. -# v3: two recovery tiers added. Ligature repair — the layout engine emits +# v3: two recovery tiers added. Ligature repair: the layout engine emits # U+FFFD for ff/fi/fl glyphs in ToUnicode-less fonts; the repair tier -# rewrites them from the flat layer. Flat-fallback — the layout parser drops +# rewrites them from the flat layer. Flat-fallback: the layout parser drops # whole prose blocks on graphically-complex born-digital pages # (infographic/multi-column reports); the fallback tier backfills them from # the flat layer. Both change texturised output corpus-wide, so affected @@ -114,8 +114,8 @@ def texture_signature() -> str: # Run-scoped "Rebuild" flag. When True, extract() bypasses the texture -# cache entirely — it re-extracts every PDF fresh and overwrites the cache -# entry — so a Rebuild genuinely re-texturises all PDFs under the current +# cache entirely (it re-extracts every PDF fresh and overwrites the cache +# entry) so a Rebuild genuinely re-texturises all PDFs under the current # engine, not just the ones missing a current-signature entry. When False # (default), extract() reuses a current-signature entry and falls back to # any prior entry for the same content, making texturising durable across @@ -158,7 +158,7 @@ def _margin_integers(page: pymupdf.Page) -> list[int]: cross-page resolver in :func:`_resolve_page_labels` decides which candidate (if any) is actually the printed page number. - We're permissive on purpose — a header like ``"Chapter 5 291"`` + We're permissive on purpose: a header like ``"Chapter 5 291"`` yields both ``5`` and ``291`` here, and the cross-page sequence check then picks ``291`` because that's the integer that ticks up by 1 across consecutive pages. The same logic naturally rejects @@ -206,7 +206,7 @@ def _resolve_page_labels( Two-stage: - 1. Pages whose ``meta_labels[i]`` is non-empty keep that — the + 1. Pages whose ``meta_labels[i]`` is non-empty keep that: the PDF declared an explicit label, which is exact. 2. For the rest we scan ``margin_candidates`` for the longest *consecutive* sequence: pages ``i, i+1, ..., i+k`` whose @@ -365,7 +365,7 @@ def _md_segments(body_md: str) -> list[str]: def _word_sequence(text: str) -> list[str]: - """Lowercased words in order — the comparable form of a line.""" + """Lowercased words in order: the comparable form of a line.""" return _WORD_RE.findall(text.lower()) @@ -388,7 +388,7 @@ def _mirror_body_into_md(chunk: Chunk) -> Chunk: slices that markdown at ATX headings, so everything recovered lands in whichever slice happens to be last while the other sections keep the impoverished text the layout parser gave them. On a book-index page that - left a chunk rendering 51 characters of a 628-character page — searchable in + left a chunk rendering 51 characters of a 628-character page: searchable in full, all but invisible in the preview. Applied at YIELD time, like the heading fold, so it also repairs chunks @@ -397,7 +397,7 @@ def _mirror_body_into_md(chunk: Chunk) -> Chunk: A line counts as present only when its words appear CONTIGUOUSLY in the markdown. Asking whether its tokens appear anywhere let dispersed terms - stand in for a phrase — ``body`` of "virtual memory" against a ``body_md`` + stand in for a phrase: ``body`` of "virtual memory" against a ``body_md`` holding "virtual address" and "physical memory" appended nothing, and a phrase query then matched the chunk with no contiguous rendered text to highlight. That is the very failure this repair exists to prevent. @@ -426,7 +426,7 @@ def _mirror_body_into_md(chunk: Chunk) -> Chunk: def _largest_font_headings(page: pymupdf.Page) -> set[str]: """Stripped text of every visual line at the page's single largest font, or ``set()`` when the font-clustering sanity gates trip (≤1 - distinct size, or >30% of lines are heading-sized — both signal that + distinct size, or >30% of lines are heading-sized: both signal that clustering is unreliable). Span text is clean here (no layout-mode ligature loss), so it's safe to match against the page's rendered reading-order text.""" @@ -454,7 +454,7 @@ def _largest_font_headings(page: pymupdf.Page) -> set[str]: # The split tier must sit *above* the modal body size. When the page's # largest font IS the body font (e.g. only footnotes are smaller), # every body line would otherwise read as a heading and shatter the - # page — the cause of the worst over-fragmentation case observed. + # page: the cause of the worst over-fragmentation case observed. if largest <= body_size: return set() heads = [ @@ -481,7 +481,7 @@ def _split_page_sections( Detection authority is font-size clustering on clean span text (:func:`_largest_font_headings`); the page's sorted (reading-order) text is then partitioned at those headings so each section's - searchable ``body`` holds only its own prose — the consecutive-heading + searchable ``body`` holds only its own prose; the consecutive-heading merge bug was the whole page sharing the *first* heading. When ``body_md`` carries one ATX heading per detected section its slices feed the preview (keeps tables / bold); otherwise a plain markdown @@ -489,8 +489,8 @@ def _split_page_sections( Splits only when ≥2 headings are detected, so single-heading and headingless pages stay one chunk and prose PDFs don't fragment. The - leading (pre-first-heading) section inherits ``carry_heading`` — the - deepest heading carried over from the previous page — so a section + leading (pre-first-heading) section inherits ``carry_heading`` (the + deepest heading carried over from the previous page) so a section that continues across a page break keeps its attribution. """ heads = _largest_font_headings(page) @@ -575,7 +575,7 @@ def _mute_fd(fd: int) -> Generator[None]: def _try_docling_fallback(pdf_path: str, page_index: int) -> str: """Try to extract one page via the docling daemon. Returns "" on - any failure — caller keeps whatever pymupdf4llm produced.""" + any failure: caller keeps whatever pymupdf4llm produced.""" try: from fnd.extract._docling_daemon import DoclingDaemon except Exception: @@ -614,7 +614,7 @@ def _splice_docling_tables(pymupdf_md: str, docling_md: str) -> str: a `==> picture omitted <==` marker where it couldn't decode an image-rendered table; docling recovers the table but drops the formatting. docling only earns its place when pymupdf4llm *missed* a - table — so: + table, so: - docling found no table (the marker is a genuine figure/chart): keep pymupdf4llm verbatim. No table to gain, so don't pay the @@ -623,7 +623,7 @@ def _splice_docling_tables(pymupdf_md: str, docling_md: str) -> str: keep pymupdf4llm. The marker is a redundant image of a table it already has; splicing docling's copy would duplicate it. (Rare limitation: a page where pymupdf got table A but missed table B - behind the marker keeps only A — measured as not occurring.) + behind the marker keeps only A, measured as not occurring.) - pymupdf4llm has no table and docling found one per marker: replace each marker line (and the ``**`` wrapper pymupdf4llm puts it in) with its table, in reading order. Line-based, so no index @@ -649,7 +649,7 @@ def _splice_docling_tables(pymupdf_md: str, docling_md: str) -> str: def _strip_picture_markers(md: str) -> str: """Drop pymupdf4llm's `==> picture [W x H] intentionally omitted <==` placeholders from the user-facing preview. Run only AFTER the docling - splice — markers it used to place tables are already replaced, so this + splice: markers it used to place tables are already replaced, so this removes just the leftovers on dedup / figure / non-routed pages and never disturbs a recovered table.""" kept: list[str] = [] @@ -672,10 +672,10 @@ def _needs_docling_fallback(page: pymupdf.Page, pymupdf_md: str) -> bool: Triggers in two cases: 1. Sum of "picture intentionally omitted" rectangles exceeds - `_FALLBACK_AREA_RATIO` of the page area — most likely a big + `_FALLBACK_AREA_RATIO` of the page area: most likely a big borderless / image-rendered table. 2. A picture-omitted marker appears alongside a "TABLE" / "Table" - label on the same page — catches small image-rendered tables + label on the same page: catches small image-rendered tables (the HBR p99 case: a 324x70 pt = ~5% region that's clearly a table per its adjacent "TABLE 5-2" heading). """ @@ -726,7 +726,7 @@ def _extract_page_md(doc: pymupdf.Document, page_index: int) -> str: except Exception: # pymupdf4llm has stricter input validation than vanilla # pymupdf and can reject pages that the rest of the pipeline - # accepts. Fall through to empty md — caller treats this as + # accepts. Fall through to empty md: caller treats this as # "structured mode unavailable for this page" and the chunk # still ships its flat body. return "" @@ -758,7 +758,7 @@ def _extract_invisible_md(doc: pymupdf.Document, page_index: int) -> str: mode, recovering invisible OCR text the layout parser discards. Toggles the process-global ``use_layout`` False for the call and - restores its prior value in ``finally`` — a worker process handles + restores its prior value in ``finally``: a worker process handles one file's pages sequentially, so the toggle never races. Mutes both stdout and stderr (the non-layout path is chattier than production). """ @@ -799,7 +799,7 @@ def _extract_invisible_md(doc: pymupdf.Document, page_index: int) -> str: def _flat_text_blocks(page: pymupdf.Page) -> list[str]: - """Flat text blocks in reading order — the gap-fill source for the + """Flat text blocks in reading order: the gap-fill source for the flat-fallback tier. ``sort=True`` orders top-to-bottom, left-to-right; image blocks (type 1) are dropped, leaving the page's text runs.""" blocks = cast(list[tuple[Any, ...]], page.get_text("blocks", sort=True)) @@ -835,9 +835,9 @@ def _finalise_body_md(ctx: ExtractionContext, body_md: str) -> str: """Post-recovery assembly for one structured page, in order: 1. born-digital docling fallback (image-rendered tables pymupdf4llm - missed) — unchanged detection/splice logic; + missed): unchanged detection/splice logic; 2. strip leftover picture-omitted markers; - 3. flat-fallback LAST — backfill prose still missing after every + 3. flat-fallback LAST: backfill prose still missing after every structured step, so nothing downstream can discard it and the docling decision in step 1 never sees flat-appended text. """ @@ -946,7 +946,7 @@ def texture_reusable( def _has_docling() -> bool: - """Cached check for docling CLI presence — used by signature only.""" + """Cached check for docling CLI presence: used by signature only.""" import shutil return shutil.which("docling") is not None @@ -978,7 +978,7 @@ def extract( On miss: runs the normal extraction, then writes the result back to the cache before yielding (so a Ctrl+C *after* the put is - safe — next reindex will hit). + safe, next reindex will hit). ``on_heartbeat`` receives per-page ("page", index) tuples plus a ("total", N) opener and ("file-start", path) reset. Used by the @@ -998,7 +998,7 @@ def extract( key = cache.build_key(content_sha256=content_sha, extractor_signature=texture_signature()) # Rebuild (force_fresh) literally deletes this content's saved # texturing first, so the entry is genuinely gone (not overwritten) and - # the re-extraction below leaves a single fresh entry — no stale + # the re-extraction below leaves a single fresh entry: no stale # variants linger. Otherwise reuse the current-signature entry, then # fall back to any prior entry for the same content (durable reuse: a # TEXTURE_VERSION bump leaves the current key empty but the older @@ -1014,7 +1014,7 @@ def extract( if cached is not None: # Cache entries are keyed by content hash, so two different files # with identical bytes share an entry. The chunks were captured - # with the original file's parent_id / path / mtime — overlay + # with the original file's parent_id / path / mtime: overlay # the *current* file's identity so downstream indexer code # routes chunks to the right Tantivy parent_id. parent_id_now = file_parent_id(path) @@ -1071,9 +1071,9 @@ def extract( raise ExtractError(str(path), f"{type(e).__name__}: {e}") from e # Best-effort cache write; if it fails (disk full, perms) we still - # yield the freshly-extracted chunks — caller should never lose work + # yield the freshly-extracted chunks: caller should never lose work # because the cache had a bad day. Skipped when the run-scoped - # battery-saver flag is on (no point caching flat extractions — + # battery-saver flag is on (no point caching flat extractions: # they're already cheap to recompute and cache hits assume # structured chunks). if not _skip_structure_extraction: @@ -1085,7 +1085,7 @@ def extract( def _get_cache() -> ExtractionCache: - """Cached singleton — building the path each call is cheap but + """Cached singleton: building the path each call is cheap but creating the directory tree once at first use is cleaner. The prefetcher and indexer can both reach this from background diff --git a/fnd/extract/plain.py b/fnd/extract/plain.py index 6413532b..99d766fd 100644 --- a/fnd/extract/plain.py +++ b/fnd/extract/plain.py @@ -34,7 +34,7 @@ def extract(path: Path) -> Iterator[Chunk]: continue # 1-based line of the chunk's first character. Counting newlines # in ``text[:start]`` is O(start) per chunk and O(len(text)^2 / - # step) total — fine for the ≤ few-MB plain-text files this + # step) total, fine for the ≤ few-MB plain-text files this # extractor sees in practice; revisit if huge logs land in the # corpus. start_line = text.count("\n", 0, start) + 1 diff --git a/fnd/extract/pptx.py b/fnd/extract/pptx.py index 05f9206e..7772098c 100644 --- a/fnd/extract/pptx.py +++ b/fnd/extract/pptx.py @@ -108,7 +108,7 @@ def _shape_md_lines(shape: BaseShape) -> list[str]: def _table_md(shape: BaseShape) -> str: - """Serialise a pptx table shape to a GFM pipe table — same shape as + """Serialise a pptx table shape to a GFM pipe table: same shape as the docx extractor produces.""" if not shape.has_table: return "" @@ -144,7 +144,7 @@ def extract(path: Path) -> Iterator[Chunk]: raise except Exception as e: # Parser libs raise everything from RuntimeError to opaque C - # extension errors. Broad except is intentional — the goal is + # extension errors. Broad except is intentional: the goal is # "don't let one bad doc stop the index build." raise ExtractError(str(path), f"{type(e).__name__}: {e}") from e diff --git a/fnd/extract/recovery/evaluators.py b/fnd/extract/recovery/evaluators.py index 2d4c5582..fd22ddb4 100644 --- a/fnd/extract/recovery/evaluators.py +++ b/fnd/extract/recovery/evaluators.py @@ -17,7 +17,7 @@ # prose as garbled). def _find_system_dict() -> Path: """First installed Unix word list (macOS/Linux/BSD locations). Windows - ships none — the caller then abstains from the legibility heuristic + ships none: the caller then abstains from the legibility heuristic rather than failing.""" for candidate in ( Path("/usr/share/dict/words"), @@ -58,11 +58,11 @@ def alpha_tokens(text: str) -> set[str]: class CoverageEvaluator: """Scores how much of the flat text layer a Markdown extraction - captured — the signal that distinguishes a dropped invisible-text + captured: the signal that distinguishes a dropped invisible-text page (coverage ~0.08) from a faithfully extracted one (~0.95).""" def flat_token_count(self, flat: str) -> int: - """Distinct flat-layer tokens — the gate's token floor input.""" + """Distinct flat-layer tokens: the gate's token floor input.""" return len(alpha_tokens(flat)) def coverage(self, markdown: str, flat: str) -> float: @@ -84,7 +84,7 @@ def _is_camel(token: str) -> bool: class LegibilityEvaluator: - """Scores how readable a recovered page's prose is — the signal that + """Scores how readable a recovered page's prose is: the signal that separates clean baked OCR from a garbled scan. Operates on prose only (code fences stripped, since identifiers aren't dictionary words). @@ -97,7 +97,7 @@ def __init__(self, dictionary: set[str] | None = None) -> None: self._loaded = dictionary is not None # True only when the lazy system-dict load was attempted and the # word list was absent (Windows / minimal containers). An explicit - # empty dict is NOT "unavailable" — it deliberately isolates the + # empty dict is NOT "unavailable": it deliberately isolates the # CamelCase rule, so we still score against it. self._dict_unavailable = False @@ -128,7 +128,7 @@ def prose_legr(self, markdown: str) -> tuple[float | None, int]: return None, 0 self._dict() # trigger lazy load so _dict_unavailable is set if self._dict_unavailable: - # No word list to judge against — abstain rather than scoring + # No word list to judge against: abstain rather than scoring # every page 0.0 (which would make the tier reprocess all prose). return None, len(tokens) legible = sum(1 for t in tokens if self._known(t) or _is_camel(t)) diff --git a/fnd/extract/recovery/models.py b/fnd/extract/recovery/models.py index 78d91b54..cf325489 100644 --- a/fnd/extract/recovery/models.py +++ b/fnd/extract/recovery/models.py @@ -30,7 +30,7 @@ class PageExtraction: @dataclass(frozen=True, eq=False) class ExtractionContext: """Everything the tiers need about the page under extraction. ``flat`` - is ``page.get_text("text")`` — already in scope at the call site, so + is ``page.get_text("text")``: already in scope at the call site, so the coverage gate is free.""" doc: Any # pymupdf.Document diff --git a/fnd/extract/recovery/repairers.py b/fnd/extract/recovery/repairers.py index 86991e6c..6fa9cfda 100644 --- a/fnd/extract/recovery/repairers.py +++ b/fnd/extract/recovery/repairers.py @@ -12,10 +12,10 @@ # The letter runs a ligature glyph can stand for. The repairer recovers # the exact dropped letters from the flat layer (so it isn't tied to this -# corpus's ff/fi/fl), but the recovered run must be an actual ligature — +# corpus's ff/fi/fl), but the recovered run must be an actual ligature, # otherwise a skeleton like "_ows" would match "allows" (gap "all") as # readily as "flows" (gap "fl"). This is the full Unicode ligature set -# (FB00–FB06) plus the extended f-ligatures real text fonts ship; extend it +# (FB00: FB06) plus the extended f-ligatures real text fonts ship; extend it # if a font surfaces another. Compared case-insensitively. _LIGATURES = frozenset( { @@ -23,7 +23,7 @@ "fi", "fl", "ffi", - "ffl", # Unicode FB00–FB04 (the common case) + "ffl", # Unicode FB00: FB04 (the common case) "st", "ct", # FB05/FB06 (long-s t, st) and old-style ct "ft", @@ -46,7 +46,7 @@ # A word run carrying at least one U+FFFD AND at least one real word char, # so a bare standalone U+FFFD (a non-ligature unmapped glyph) is never a -# match — repair only ever fires inside a word. +# match: repair only ever fires inside a word. _BROKEN_WORD_RE = re.compile( rf"\w+{_REPLACEMENT}[\w{_REPLACEMENT}]*|{_REPLACEMENT}[\w{_REPLACEMENT}]*\w+" ) @@ -63,7 +63,7 @@ class LigatureRepairer: Each U+FFFD stands for a ligature glyph the font couldn't map. The flat layer resolves those glyphs, so the repairer treats the broken token as - a skeleton — literal letters around one or more gaps — and looks for the + a skeleton, literal letters around one or more gaps, and looks for the single flat-layer word that fits, where each gap is a real ligature (see ``_LIGATURES``). It recovers the exact dropped letters rather than brute-forcing a fixed list, so it handles any ligature in that set while @@ -73,7 +73,7 @@ class LigatureRepairer: Repair never guesses. A bare U+FFFD, a token with no fitting flat word, or an ambiguous skeleton (more than one distinct flat word fits) is left - untouched — so a hard case is dropped, never corrupted. The true word is + untouched, so a hard case is dropped, never corrupted. The true word is always present in the flat layer (same page), so a unique fit is always the right word. """ @@ -104,7 +104,7 @@ def _fix(self, word: str, vocab: list[str]) -> str: continue # a skeleton twin whose gap isn't a ligature (allows vs flows) seen.add(candidate.lower()) if len(seen) > 1: - return word # two genuine ligature fits — refuse to guess + return word # two genuine ligature fits: refuse to guess if match is None: match = m if match is None: diff --git a/fnd/extract/recovery/tiers.py b/fnd/extract/recovery/tiers.py index 06363f8f..b8dcd5e9 100644 --- a/fnd/extract/recovery/tiers.py +++ b/fnd/extract/recovery/tiers.py @@ -165,8 +165,8 @@ class DoclingTableTier: The invisible-text lever recovers a table's text but flattens it to prose (the grid is gone and it emits no picture marker, so the born-digital splice can't fire). When a recovered page carries a - table caption, route it through docling — whose layout model rebuilds - the grid — and splice the grid in at the caption, preserving the + table caption, route it through docling, whose layout model rebuilds + the grid, and splice the grid in at the caption, preserving the recovered prose. Only acts on texture-recovered pages; born-digital pages keep the existing marker-based inline splice.""" @@ -220,7 +220,7 @@ class FlatFallbackTier: keeps pull-quotes and captions but omits whole body-text blocks it can't place in reading order (and the non-layout path emits nothing), so ``body_md`` is far less complete than the flat ``get_text`` layer the - index searches — a hit on the dropped prose can't be shown in preview. + index searches: a hit on the dropped prose can't be shown in preview. Every page is checked block-by-block: any flat block the structured Markdown missed (token overlap below ``block_present_ratio``) is appended @@ -228,7 +228,7 @@ class FlatFallbackTier: a block already represented is never re-appended. There is deliberately no page-level coverage floor. A floor let a page - that was, say, 92% covered keep the other 8% searchable-but-unrenderable — + that was, say, 92% covered keep the other 8% searchable-but-unrenderable: measured at 5.7% of PDF tokens corpus-wide, with individual pages losing over 30%. Complete coverage is the only exact short-circuit: when every flat token is already in the Markdown, no block can be missing, so the @@ -290,7 +290,7 @@ def _missing_blocks(self, ctx: ExtractionContext, md: str) -> list[str]: class LegibilityReprocessTier: """Reprocess a page whose recovered prose reads as garbled OCR. - DEFERRED — built and unit-tested but not composed into the pipeline: + DEFERRED: built and unit-tested but not composed into the pipeline: the measured corpus has uniformly legible baked OCR, so no real page exercises it. Kept ready for a poorer-scan corpus. When prose legibility falls below the gate (with enough prose to judge), it tries diff --git a/fnd/extras.py b/fnd/extras.py index dd9992c0..7364c2e1 100644 --- a/fnd/extras.py +++ b/fnd/extras.py @@ -106,21 +106,21 @@ def is_package_installed(pkg: Package) -> bool: Multiple layers of stale state can fool naive detection: - The path-importer cache keeps the FileFinder for a directory - after one module has been resolved — even when the files have + after one module has been resolved, even when the files have been removed by an out-of-process tool. ``invalidate_caches`` clears that. - A regular package whose ``__init__.py`` has been removed but whose parent directory still exists (macOS Finder occasionally leaves ``__init__ 2.py`` after a sync conflict; uv's atomic rename has been observed to do similar) is matched as a - *namespace* package — origin is None, submodule_search_locations + *namespace* package: origin is None, submodule_search_locations points at an empty husk. - For ``cli:NAME`` detection, ``shutil.which`` returns ANY binary with that name on ``PATH``. A user with a system-wide ``docling`` (from another Python install) would be detected as "installed" even after ``uv tool uninstall docling-slim`` removed fnd's copy. For uv-tool-installed packages, check the - uv tool install dir directly — that's what fnd manages. + uv tool install dir directly: that's what fnd manages. A "really installed" module needs either a real ``origin`` file or at least one submodule search location containing @@ -147,7 +147,7 @@ def is_package_installed(pkg: Package) -> bool: return True return False if kind == "cli": - # For uv-tool packages, ignore PATH — a system-wide binary + # For uv-tool packages, ignore PATH: a system-wide binary # with the same name isn't what fnd installed. Check the uv # tool root specifically. if pkg.install_via == "uv-tool": @@ -209,7 +209,7 @@ def install_commands(extra: Extra) -> list[list[str]]: Strategy depends on whether fnd's runtime ``sys.executable`` lives inside a uv-managed project venv: - - **uv-managed project venv** (the developer case — ``sys.executable`` + - **uv-managed project venv** (the developer case: ``sys.executable`` is ``/.venv/bin/python3.X`` and the project's ``pyproject.toml`` declares a matching ``[dependency-groups].``): install via ``uv sync --group ``. The caller is also @@ -217,7 +217,7 @@ def install_commands(extra: Extra) -> list[list[str]]: :func:`enable_pdf_structure_default_group` so subsequent ``uv sync`` calls (without a ``--group`` flag) don't wipe the install. This is the persistence fix for "I installed but fnd - says not installed after restart" — without the default-group + says not installed after restart", without the default-group flip, any ``uv run`` re-syncs the venv and removes the extra. - **uv-tool install / standalone** (``sys.executable`` is in a @@ -281,7 +281,7 @@ def _group_exists_in_pyproject(pyproject: Path, group_name: str) -> bool: def enable_pdf_structure_default_group(pyproject: Path) -> None: """Add ``pdf-structure`` to ``[tool.uv] default-groups`` so a subsequent ``uv sync`` (without a ``--group`` flag) keeps the - install. Idempotent — no-op when the group is already in the + install. Idempotent: no-op when the group is already in the list. Preserves comments + ordering via tomlkit.""" _ensure_group_membership(pyproject, "pdf-structure", present=True) @@ -327,7 +327,7 @@ def _pip_install_specs(pkg: Package) -> list[str]: # lockfile / pyproject. pymupdf-layout rides in as a base # requirement of pymupdf4llm at ~=1.27 (the old `[layout]` # extra no longer exists), so installing the base package is - # enough — requesting the extra would only emit a uv warning. + # enough: requesting the extra would only emit a uv warning. primary = pkg.detect.split(":", 1)[1] return [primary] if primary == "pymupdf4llm" else list(pkg.uninstall_targets) return [pkg.detect.split(":", 1)[1]] diff --git a/fnd/filter_dsl.py b/fnd/filter_dsl.py index e3018293..472a8410 100644 --- a/fnd/filter_dsl.py +++ b/fnd/filter_dsl.py @@ -279,7 +279,7 @@ def tokenize(text: str) -> list[Token]: kind = TokenKind.IDENT if ch == '"' else TokenKind.STRING out.append(Token(kind, value, col)) continue - # Date literal (must precede number — same leading digits). + # Date literal (must precede number: same leading digits). date_match = _DATE_RE.match(text, i) if date_match: iso = date_match.group(0) @@ -546,7 +546,7 @@ def _parse_value(self) -> object: if t.kind is TokenKind.NULL: return None if t.kind is TokenKind.IDENT: - # Bare identifier on the value side is an error — values must be + # Bare identifier on the value side is an error: values must be # quoted strings, numbers, dates, or keywords. raise FilterError( f"expected value, got identifier {t.value!r}; quote string values", @@ -609,7 +609,7 @@ def compile_filter(text: str) -> Predicate: def parse_or_error(text: str) -> tuple[Predicate | None, FilterError | None]: - """Same as :func:`compile_filter` but never raises — returns either a + """Same as :func:`compile_filter` but never raises, returns either a usable predicate or a structured error. Used by the TUI form so the user gets inline syntax feedback as they type.""" if not text.strip(): @@ -723,7 +723,7 @@ def _eval_in(fm: Mapping[str, object], value: object, field: str, negated: bool) def _orderable(a: object, b: object) -> bool: if isinstance(a, bool) or isinstance(b, bool): - return False # bool is a subtype of int — explicitly reject + return False # bool is a subtype of int: explicitly reject if isinstance(a, int | float) and isinstance(b, int | float): return True return isinstance(a, dt.date) and isinstance(b, dt.date) diff --git a/fnd/frontmatter.py b/fnd/frontmatter.py index a7cd53ff..64f4e269 100644 --- a/fnd/frontmatter.py +++ b/fnd/frontmatter.py @@ -4,7 +4,7 @@ dep weight. Supports the shapes Obsidian / Jekyll / Hugo / MkDocs use: flat key→scalar / quoted-string / inline list / block list / number / ISO date / bool / null. Nested mappings, multiline strings (``|``/``>``) -and YAML anchors are out of scope — they raise FrontmatterParseError. +and YAML anchors are out of scope: they raise FrontmatterParseError. A document with no leading ``---\\n`` fence returns None (signals "no frontmatter present"). An empty fenced block returns {}. @@ -89,7 +89,7 @@ def _parse_block(lines: list[str]) -> dict[str, object]: i = 0 while i < len(lines): raw = lines[i] - # Reject indented continuation that would imply nested mapping — + # Reject indented continuation that would imply nested mapping: # we don't support nested structures. if raw and raw[0] in (" ", "\t"): raise FrontmatterParseError( @@ -104,7 +104,7 @@ def _parse_block(lines: list[str]) -> dict[str, object]: raise FrontmatterParseError(f"frontmatter line {i + 2}: expected ``key: value``") key = m.group(1).rstrip() value_text = m.group(2) - # YAML anchors / aliases / tags — explicit reject. + # YAML anchors / aliases / tags: explicit reject. if value_text.startswith("&") or value_text.startswith("*") or value_text.startswith("!"): raise FrontmatterParseError( f"frontmatter line {i + 2}: anchors/aliases/tags are unsupported" diff --git a/fnd/fusion.py b/fnd/fusion.py index c96cfeeb..96f49b6c 100644 --- a/fnd/fusion.py +++ b/fnd/fusion.py @@ -8,11 +8,11 @@ Auto-derived sub-queries: -* ``phrase`` — the user's query wrapped in quotes (weight 2.0). Only emitted +* ``phrase``: the user's query wrapped in quotes (weight 2.0). Only emitted for multi-word queries (a phrase pass on a single word is identical to the lex pass and would just inflate the RRF score for everything). -* ``lex`` — the literal user query (weight 1.0). Implicit AND across terms. -* ``syn`` — synonym-expanded version of the query (weight 0.6). Only emitted +* ``lex``: the literal user query (weight 1.0). Implicit AND across terms. +* ``syn``: synonym-expanded version of the query (weight 0.6). Only emitted when the expansion actually changes the query string. A ``stem`` sub-query is omitted: the body field is already analysed with @@ -52,17 +52,17 @@ from fnd.tag_query import TagFilter -# A field qualifier (``kind:pdf``, ``c:papers``) anywhere in the query — phrase +# A field qualifier (``kind:pdf``, ``c:papers``) anywhere in the query, phrase # wrapping such a query would quote the qualifier and produce a junk phrase. _FIELD_SYNTAX_RE: Final = re.compile(r"\b[A-Za-z_][A-Za-z0-9_]*:") -# Explicit operator syntax — boolean keywords, ``+``/``-`` required/prohibited +# Explicit operator syntax: boolean keywords, ``+``/``-`` required/prohibited # prefixes, wildcard/fuzzy chars, or a ``/regex/`` token. Quoting such a query as # a phrase destroys its meaning (``"crypto* -wallet"`` becomes the phrase "crypto # wallet" and re-admits the very doc the ``-`` excluded). The lex pass already # honours it exactly via the boolean AST compiler, so the phrase pass stands down. # ``/`` matches only as a delimited ``/regex/`` token, so plain slashes in paths # or ``TCP/IP`` don't suppress the phrase pass. Parens and ``^`` (grouping / -# boost) also count as structure — ``(cross entropy)`` and ``foo^2`` already +# boost) also count as structure: ``(cross entropy)`` and ``foo^2`` already # carry intent in the lex pass and shouldn't get an auto-phrase pass. _OPERATOR_SYNTAX_RE: Final = re.compile( r"\b(?:AND|OR|NOT)\b|[*?~^()]|(?:^|\s)[+\-]\S|(?:^|\s)/[^/\s]+/(?:\s|$)" @@ -210,7 +210,7 @@ def auto_subqueries(query: str, *, synonyms: SynonymTable | None) -> list[SubQue Multi-word queries get a ``phrase`` + ``lex`` pair; single-word queries only get ``lex`` (a phrase of one word is identical to a term query and would just dilute the RRF math). A ``syn`` pass is appended only - when synonym expansion actually rewrites the query — otherwise it + when synonym expansion actually rewrites the query, otherwise it would issue an identical Tantivy round-trip for nothing. The syn pass, like the phrase pass, stands down for any structured query @@ -218,7 +218,7 @@ def auto_subqueries(query: str, *, synonyms: SynonymTable | None) -> list[SubQue grafts an ``(a OR b)`` disjunction into the string, which strands a proximity brace (``{20}("a b" OR c)``) or mangles the operator the lex pass already honours. Tradeoff: ``kind:pdf threat intelligence`` no longer - auto-expands ``ti`` — synonym widening only applies to plain bag-of-words + auto-expands ``ti``, synonym widening only applies to plain bag-of-words queries. """ q = query.strip() @@ -228,8 +228,8 @@ def auto_subqueries(query: str, *, synonyms: SynonymTable | None) -> list[SubQue # Skip the auto-phrase pass when the query already encodes phrase intent in # the lex pass: a user-supplied quote (Tantivy parses ``"a b c"`` as a # PhraseQuery directly) or proximity (``{N} …`` / ``a NEAR/N b`` expand to - # ``"a b"~N`` downstream). Re-wrapping either would double-quote — ``""a b""`` - # or ``""a b"~N`` — and crash the parser. Also skip when the query carries a + # ``"a b"~N`` downstream). Re-wrapping either would double-quote (``""a b""`` + # or ``""a b"~N``) and crash the parser. Also skip when the query carries a # field qualifier (``kind:pdf``, ``c:papers``): a phrase over the raw qualifier # text is meaningless and quoting it mangles the qualifier. carries_field_syntax = bool(_FIELD_SYNTAX_RE.search(q)) @@ -297,7 +297,7 @@ def parse_multi_input(text: str, *, synonyms: SynonymTable | None) -> MultiInput A ``phrase:`` value that isn't already quoted is wrapped in quotes so the Tantivy parser sees it as a phrase query. A ``syn:`` value is expanded against the supplied table at parse time. - An ``intent:`` line is captured separately — it does NOT produce a + An ``intent:`` line is captured separately; it does NOT produce a sub-query, but is returned on the :class:`MultiInput` so callers can pass it to :func:`fnd.layered.search_layered`. Last-write-wins if multiple intent lines appear (matches QMD's "at most one intent @@ -413,7 +413,7 @@ def _issue(q: str) -> CandidatePool: if sub.source == "lex": # The lex pass carries the user's literal query: reuse the regime # probe's result when supplied, else issue it. A syntax error here is - # the user's to fix, so it must propagate — the Searcher safety-net + # the user's to fix, so it must propagate: the Searcher safety-net # and the TUI's inline notice both rely on malformed queries raising. pool = precomputed_lex if precomputed_lex is not None else _issue(sub.query) rankings.append(pool.hits) diff --git a/fnd/index.py b/fnd/index.py index d611e842..b0c116fe 100644 --- a/fnd/index.py +++ b/fnd/index.py @@ -72,7 +72,7 @@ def _commit_is_retryable(exc: BaseException) -> bool: The prefix is what makes this safe, not the Win32 text: a store-write refusal (``Failed to open file for write:``) prints the same - ``Access is denied.`` and leaves the writer DEAD — measured, every later + ``Access is denied.`` and leaves the writer DEAD, measured, every later commit then returns success and discards its documents in silence.""" text = str(exc) if not text.startswith("An IO error occurred:"): @@ -118,7 +118,7 @@ def commit(writer: IndexWriter) -> None: async def commit_async(writer: IndexWriter) -> None: - """:func:`commit` for the async runner — yields the loop between attempts.""" + """:func:`commit` for the async runner, yields the loop between attempts.""" import asyncio for delay in _commit_attempts(writer): @@ -143,8 +143,8 @@ def _ensure_index(index_dir: Path, *, force: bool = False) -> Index: on mismatch we either raise (default) or wipe the dir (``force=True``). 2. Tantivy itself stores the schema in ``meta.json`` and rejects any constructor call whose schema doesn't match the on-disk segments. - Both gates can disagree — e.g. if a prior rebuild bumped the - sidecar but crashed before Tantivy wrote new segments — so we + Both gates can disagree: e.g. if a prior rebuild bumped the + sidecar but crashed before Tantivy wrote new segments, so we also retry on Tantivy's ``ValueError`` when ``force=True``, wiping and re-opening with a fresh schema. """ @@ -169,7 +169,7 @@ def _ensure_index(index_dir: Path, *, force: bool = False) -> Index: try: return register(Index(schema, path=str(index_dir))) except ValueError as e: - # Tantivy's "Schema error: ... does not match" — the sidecar said + # Tantivy's "Schema error: ... does not match": the sidecar said # OK but Tantivy disagrees (e.g. recovery from a half-completed # rebuild). With force=True we wipe and retry once; otherwise we # surface a clearer recovery instruction. diff --git a/fnd/index_runner.py b/fnd/index_runner.py index aafb9caa..65732be1 100644 --- a/fnd/index_runner.py +++ b/fnd/index_runner.py @@ -9,8 +9,8 @@ display. A ``cancel`` :class:`asyncio.Event` lets the caller request a clean stop at the next file boundary. -State persistence — atomic writes after each file completion to -``$XDG_DATA_HOME/fnd/reindex/.state.toml`` — means a +State persistence: atomic writes after each file completion to +``$XDG_DATA_HOME/fnd/reindex/.state.toml``, means a kill / quit / power-loss can be resumed simply by re-running: - Cache hits skip already-extracted files - State file tells us how far we got, for progress display on resume @@ -220,7 +220,7 @@ def saved_states() -> list[tuple[Path, IndexState]]: """Every readable resume state on disk, newest first. Callers that want "is there anything to resume?" must ask this rather - than probing one well-known name — auto-resume used to look only for a + than probing one well-known name: auto-resume used to look only for a collection literally called ``default``, so an interrupted run of any named collection was silently unresumable and its state file lived on forever. @@ -398,7 +398,7 @@ def _should_reprocess( """Whether a file already in the index needs re-processing. mtime catches content edits. ctime additionally catches metadata-only - edits — a Finder retag moves ctime alone, so an mtime-only check would + edits: a Finder retag moves ctime alone, so an mtime-only check would leave tags stale until the file's content happened to change. ``prior_ctime`` of 0 means the stored doc predates the field (a v7 index @@ -470,14 +470,14 @@ class CloudPolicy: ``skip`` is the live opt-out. The user flips it from the indexer modal when the wait isn't worth it; from then on cloud-only files are listed - as skipped instead of fetched. Run-scoped by design — nothing is + as skipped instead of fetched. Run-scoped by design: nothing is persisted, so the next Update fetches again. """ timeout_s: float = DEFAULT_FETCH_TIMEOUT_S skip: asyncio.Event | None = None # The run's cancel signal, polled while a fetch is blocked. Without it a - # stalled provider holds Cancel for the whole timeout — which would undo + # stalled provider holds Cancel for the whole timeout, which would undo # the responsiveness this change exists to deliver. cancel: asyncio.Event | None = None @@ -497,7 +497,7 @@ def fetch(self, call: Callable[[], Any], path: Path) -> Any: return cloud_files.fetch(call, path=path, timeout_s=self.timeout_s, cancel=self.cancel) def materialise(self, path: Path) -> None: - """Pull ``path`` down. Touching one byte is enough — both macOS + """Pull ``path`` down. Touching one byte is enough: both macOS dataless files and Windows Cloud Files placeholders hydrate on first data access.""" @@ -550,7 +550,7 @@ def _process_one_file( tag_frontmatter_keys: Sequence[str] = (), cloud_policy: CloudPolicy | None = None, ) -> _FileOutcome: - """Synchronous per-file work — extraction + write to Tantivy. + """Synchronous per-file work: extraction + write to Tantivy. ``has_textured`` is True iff any emitted chunk carries a non-empty ``body_md`` (PDFs that hit the structured pipeline). Run inside @@ -558,7 +558,7 @@ def _process_one_file( When ``skip_unchanged`` and ``prior_searcher`` is given, a file already in this collection's committed index with an unchanged mtime AND ctime - is skipped entirely (no delete, no extraction) — returned as a cache + is skipped entirely (no delete, no extraction): returned as a cache hit so it counts as already-indexed. ctime is checked because a Finder retag moves it without touching mtime. The one exception is a flat PDF we could newly texturise this run (``texturise_on`` and no prior @@ -610,7 +610,7 @@ def _process_one_file( if not changed and not improvable: return _FileOutcome(0, True, True, prior_textured, "") - # The file survived the unchanged-skip, so it genuinely needs reading — + # The file survived the unchanged-skip, so it genuinely needs reading, # which for a cloud-only file means a download. Do it here rather than # letting extract() block on the first read, so the wait is bounded and # announced instead of looking like a wedged extractor. @@ -747,7 +747,7 @@ async def run_indexer( already in this collection's committed index with an unchanged mtime and ctime is left untouched. The "Re-texturise outdated" action passes False so it can revisit unchanged files. ``force_fresh`` (default False) is the - "Re-texturise outdated" opt-out from durable cache reuse — when True, + "Re-texturise outdated" opt-out from durable cache reuse, when True, ``extract()`` only reuses a current-signature entry and otherwise re-extracts fresh, upgrading pre-version texturising. """ @@ -761,7 +761,7 @@ async def run_indexer( # Run-scoped "Update cache at index time" toggle. When the user has # turned this off (battery-saver), extract() skips fresh structured - # extraction on cache misses and skips cache writes — see + # extraction on cache misses and skips cache writes: see # fnd/extract/pdf.py::set_skip_structure_extraction. An explicit # texturise_override from the caller wins over the toggle. if texturise_override is True: @@ -779,8 +779,8 @@ async def run_indexer( # Tag settings come from the user's config, sourced here (the same # internal-load pattern used for cache_at_index_time above) so every - # indexing path — including the TUI "Update index" modal that drives - # this runner — honours tag_sources / tag_frontmatter_keys rather than + # indexing path, including the TUI "Update index" modal that drives + # this runner, honours tag_sources / tag_frontmatter_keys rather than # the bare defaults. Missing config falls back to the defaults. from fnd.config import Defaults @@ -860,7 +860,7 @@ def _emit(kind: EventKind, **extra: Any) -> ProgressEvent: def _scan_frontmatter(path: Path) -> dict[str, object] | None: """Frontmatter for a filter candidate, fetching it if it is - cloud-only. Returning None drops the file from the walk — used when + cloud-only. Returning None drops the file from the walk: used when the user has opted out of fetching, or the provider never delivered.""" from fnd.frontmatter import read_frontmatter_from_file @@ -876,7 +876,7 @@ def _scan_frontmatter(path: Path) -> dict[str, object] | None: policy.fetch(lambda: read_frontmatter_from_file(path), path), ) except CloudFetchCancelledError: - # Not a blocked file — the run is ending. The pump checks cancel + # Not a blocked file: the run is ending. The pump checks cancel # immediately after this slice and emits the terminal event. return None except (CloudFetchError, OSError) as e: @@ -987,7 +987,7 @@ def _prepare() -> tuple[dict[str, int], int, int, Any, Any, Any, list[Path]]: # Files the scan could not resolve never reach the per-file loop, so # account for them here. Without this they vanish between "the folder - # holds 400 notes" and "the run processed 180" — the silent data loss + # holds 400 notes" and "the run processed 180": the silent data loss # this whole change exists to avoid. for blocked_path, reason in scan_blocked: state.failed += 1 @@ -1066,7 +1066,7 @@ def _prepare() -> tuple[dict[str, int], int, int, Any, Any, Any, list[Path]]: ) except CloudFetchCancelledError: # Cancel landed while this file's download was still - # outstanding. It is not a failure — commit what the run + # outstanding. It is not a failure: commit what the run # already wrote and end on the truthful terminal event. with contextlib.suppress(Exception): await commit_async(writer) @@ -1145,7 +1145,7 @@ def _prepare() -> tuple[dict[str, int], int, int, Any, Any, Any, list[Path]]: err = f"{err} [last page beat: {_done}/{_total}]" # CLI only. Under the TUI, stderr is the terminal Textual is # painting into, so a skip line lands as garbage across the - # rendered UI — and the same skip already reaches the user + # rendered UI, and the same skip already reaches the user # through the file_error event and the failure log below. if echo_skips: echo_skip(str(err)) @@ -1190,8 +1190,8 @@ def _prepare() -> tuple[dict[str, int], int, int, Any, Any, Any, list[Path]]: has_textured_chunk=has_textured, ) - # Every file the scan found counts as live — including ones the - # incremental skip left untouched and ones that were unreadable — + # Every file the scan found counts as live, including ones the + # incremental skip left untouched and ones that were unreadable, # so only files that genuinely left the collection get pruned. # Skipped on cancel, where the partial walk would read as mass # deletion, and on a missing root (offline volume, same trap). diff --git a/fnd/kind_catalogue.py b/fnd/kind_catalogue.py index 44589595..2e2c6867 100644 --- a/fnd/kind_catalogue.py +++ b/fnd/kind_catalogue.py @@ -1,7 +1,7 @@ """Which file-type kinds exist in the active collections. -Read from the index with a terms aggregation over ``F_KIND`` — the same live -mechanism as :mod:`fnd.tag_catalogue` — so the file-type filter shows only the +Read from the index with a terms aggregation over ``F_KIND`` (the same live +mechanism as :mod:`fnd.tag_catalogue`) so the file-type filter shows only the kinds actually present and never drifts from what is indexed. ``F_KIND`` is ``fast=True, tokenizer_name="raw"`` (see :mod:`fnd.schema`), which diff --git a/fnd/kinds.py b/fnd/kinds.py index e7b1833d..5fa1e2c7 100644 --- a/fnd/kinds.py +++ b/fnd/kinds.py @@ -1,4 +1,4 @@ -"""Central registry of the file types fnd can index — the single source of truth. +"""Central registry of the file types fnd can index: the single source of truth. Every file type is one :class:`KindSpec`: a stable ``id`` (the value stored in the index ``F_KIND`` field), the suffixes that map to it, the extractor module @@ -7,11 +7,11 @@ Consumers (walk, dispatch, the preview router, config/apps validation, the CLI, the Filters panel) derive their lookups from here instead of hard-coding kind -lists, so adding a file type is one row in ``KIND_SPECS`` — plus, for a +lists, so adding a file type is one row in ``KIND_SPECS``, plus, for a brand-new format, a matching ``fnd/extract/.py``. ``kind`` is stored fine-grained (one id per format); ``category`` is a *grouping* -concept only and is **not** stored in the index — a category filter expands to +concept only and is **not** stored in the index: a category filter expands to its member kind ids. This module imports nothing from ``fnd`` so it is safe to import from anywhere without an import cycle. """ diff --git a/fnd/launch_command.py b/fnd/launch_command.py index b0587c74..d19e23be 100644 --- a/fnd/launch_command.py +++ b/fnd/launch_command.py @@ -4,13 +4,13 @@ on, so neither the CLI nor the TUI reaches into the other. Three value objects keep the serialise ↔ relaunch paths provable inverses: -- ``LaunchScope`` — the filter state expressible as ``fnd`` launch flags. +- ``LaunchScope``: the filter state expressible as ``fnd`` launch flags. The ``tui`` command both emits one (this module) and hydrates from one (``ScopeController``); a round-trip test asserts the symmetry. -- ``SearchSnapshot`` — the read-only projection the serializer consumes, +- ``SearchSnapshot``: the read-only projection the serializer consumes, built by ``ScopeController.snapshot`` so serialization never reaches into the controller's internals. -- ``LaunchCommand`` — the rendered command plus any lossy-conversion notes. +- ``LaunchCommand``: the rendered command plus any lossy-conversion notes. """ from __future__ import annotations @@ -40,7 +40,7 @@ class LaunchScope: tag_match_all: bool = True def __bool__(self) -> bool: - # tag_match_all is a mode, meaningless without tags — it never makes + # tag_match_all is a mode, meaningless without tags: it never makes # an otherwise-empty scope "active". return bool(self.created or self.modified or self.kinds or self.tags or self.not_tags) diff --git a/fnd/launcher.py b/fnd/launcher.py index e3d6f396..b3f4be80 100644 --- a/fnd/launcher.py +++ b/fnd/launcher.py @@ -1,4 +1,4 @@ -"""OS launcher — open a file/URL in the desktop's default handler and reveal +"""OS launcher: open a file/URL in the desktop's default handler and reveal a file in the platform file manager, isolated behind one seam. Mirrors :mod:`fnd.tui.clipboard`: the OS-specific choice is made once by @@ -6,7 +6,7 @@ (and, on Windows, ``os.startfile``) as injectable dependencies, so every branch is unit-testable without spawning anything. -Deep-linking to a page/line is deliberately *not* here — that is a per-app +Deep-linking to a page/line is deliberately *not* here: that is a per-app concern owned by the :mod:`fnd.apps` handlers. This seam only covers "hand it to the OS default" (:meth:`Launcher.open_path`), "hand this URL scheme to the OS" (:meth:`Launcher.open_url`), and "show the file in the file manager" @@ -35,7 +35,7 @@ StartFile = Callable[[str], None] # Non-zero code returned when a launch can't even start (missing opener binary, -# no OS handler for the type). The API contract is return-code-only — a UI +# no OS handler for the type). The API contract is return-code-only: a UI # action handler must never have to catch an exception from an open/reveal. LAUNCH_FAILED = 127 @@ -169,7 +169,7 @@ def reveal(self, path: Path) -> None: if self._which(binary): self._spawn([binary, flag, str(path)]) return - # No selecting file manager available — open the containing folder. + # No selecting file manager available: open the containing folder. self._spawn(["xdg-open", str(path.parent)]) @@ -185,7 +185,7 @@ def __init__( ) -> None: # ``os.startfile`` only exists on Windows and is resolved lazily (at # call time) so the class type-checks, imports, and constructs on - # every platform — the factory builds it before any call, and tests + # every platform: the factory builds it before any call, and tests # inject a fake ``startfile`` when running off-Windows. self._startfile = startfile self._spawn = spawn diff --git a/fnd/layered.py b/fnd/layered.py index f3135c6a..71f1c217 100644 --- a/fnd/layered.py +++ b/fnd/layered.py @@ -150,7 +150,7 @@ def search_layered( # Step 1: literal probe. Doubles as the bypass-decision input AND # (when bypass fires) the result set. When bypass does NOT fire, - # fusion reuses this as its precomputed lex ranking — no wasted + # fusion reuses this as its precomputed lex ranking: no wasted # Tantivy round-trip. window = candidate_window(limit) probe = searcher._candidates( @@ -440,7 +440,7 @@ def _evaluate_strong_signal( """Decide whether the literal probe is a clear winner. A single uncontested hit (``len(probe) == 1``) treats the runner-up - score as 0 — the gap then equals the top score, so any top above + score as 0: the gap then equals the top score, so any top above the score threshold fires. Mirrors QMD's behaviour where ``secondScore`` defaults to 0 when no runner-up exists. """ diff --git a/fnd/matching.py b/fnd/matching.py index 0d56e002..b4c5d955 100644 --- a/fnd/matching.py +++ b/fnd/matching.py @@ -1,7 +1,7 @@ """Shared match semantics for the search and highlight pipelines. The cascade fuzzy pass (`fnd.cascade._fuzzy_pass`) widens a query -into "any indexed stem within edit distance N of a query stem" — same +into "any indexed stem within edit distance N of a query stem": same shape as Lucene's ``MultiTermQuery`` rewrite. The preview highlighter needs to mark every word that *would* have caused a hit so the user sees why a chunk surfaced (literal exact, fuzzy variant, or synonym @@ -10,14 +10,14 @@ Public surface: -* :class:`MatchSpec` — frozen carrier for the per-query match data. -* :func:`MatchSpec.from_query` — factory that pre-stems the query +* :class:`MatchSpec`, frozen carrier for the per-query match data. +* :func:`MatchSpec.from_query`, factory that pre-stems the query terms, applies optional synonym expansion, and per-stem assigns the Lucene-AUTO fuzzy distance. -* :func:`word_matches` — predicate evaluated on each rendered word. -* :func:`auto_fuzzy_distance` — exposed for cascade so it picks the +* :func:`word_matches`, predicate evaluated on each rendered word. +* :func:`auto_fuzzy_distance`, exposed for cascade so it picks the same per-term distance bound the highlighter uses. -* :func:`levenshtein_within` — capped edit distance with early exit. +* :func:`levenshtein_within`, capped edit distance with early exit. """ from __future__ import annotations @@ -36,12 +36,12 @@ from fnd.stopwords import STOPWORDS, is_all_stopwords from fnd.synonyms import SynonymTable, expand -# Canonical doc-text word scanner — the single source of truth for how every +# Canonical doc-text word scanner: the single source of truth for how every # match/highlight surface tokenises document text. ``fnd_text`` (the F_BODY # analyser) splits on EVERY non-alphanumeric char, INCLUDING underscore, so an # identifier like ``recursive_directory_iterator`` is indexed as three tokens # and a search for ``iterator`` finds it. A plain ``\w+`` keeps underscore, so -# it would see one token that fails to stem-match ``iterator`` — the match is +# it would see one token that fails to stem-match ``iterator``: the match is # found but goes unhighlighted. ``[^\W_]`` is "word char except underscore" # (Unicode letters/digits intact), the complement of the ``[\W_]+`` split # fnd/query_compile.py already uses to mirror the analyzer. Route all doc-text @@ -58,11 +58,11 @@ _HL_REGEX = re.compile(r"^/(.+)/$") _HL_GLOB = re.compile(r"[*?]") -# Boolean operators are query structure, not content — they must never become +# Boolean operators are query structure, not content: they must never become # highlight terms or consume a colour slot. _BOOL_KEYWORDS = frozenset({"AND", "OR", "NOT"}) # A field qualifier (``kind:pdf``, ``c:notes``, ``title:x``) is a filter, not a -# body term — its value must not highlight or spend a colour slot. +# body term; its value must not highlight or spend a colour slot. _FIELD_QUALIFIER_RE = re.compile(r"^[A-Za-z_]\w*:") # ``~N`` fuzzy / ``^boost`` modifiers. Stripped when deriving plain terms and # colour slots (so ``mitochondira~1`` doesn't light up a bare ``1``), but KEPT in @@ -181,7 +181,7 @@ def _phrase_word_lists(query: str) -> list[list[str]]: def _strip_quoted_spans(query: str) -> str: - """Query with quoted-phrase contents removed — leaves only loose terms.""" + """Query with quoted-phrase contents removed: leaves only loose terms.""" return _QUOTED_PHRASE.sub(" ", query) @@ -327,7 +327,7 @@ class MatchSpec: * The stems of every plain-word query term (``_terms_from_query`` output, as the index analyses them). * Stems of every synonym variant the SynonymTable would expand - those terms into — so a query for "k8s" highlights "kubernetes" + those terms into, so a query for "k8s" highlights "kubernetes" in the doc. ``fuzzy_per_stem`` lists ``(stem, max_distance)`` pairs for every @@ -347,7 +347,7 @@ class MatchSpec: fuzzy_per_stem: tuple[tuple[str, int], ...] = field(default_factory=tuple) raw_terms: tuple[str, ...] = field(default_factory=tuple) # Original glob tokens (``discoun*``, ``cr*to``, ``*graph``, ``gr?y``) and - # literal ``/regex/`` patterns, matched against the stemmed doc word — so + # literal ``/regex/`` patterns, matched against the stemmed doc word, so # wildcard, regex, and glob matches highlight the same words the search # surfaced. The glob tokens are kept verbatim (not pre-compiled) so the # highlighter can colour literal chars yellow and wildcard-filled chars @@ -365,8 +365,8 @@ class MatchSpec: # "and") doesn't light up document-wide. phrases: tuple[tuple[str, ...], ...] = field(default_factory=tuple) # Proximity groups: ``((member, …), slop)`` per ``{N}`` / ``NEAR/N`` / typed - # ``"a b"~N`` operator (slop > 0). A member is a stem, or — when the user - # wrote a wildcard (``{6}respons* mobile``) — the raw glob, matched against + # ``"a b"~N`` operator (slop > 0). A member is a stem, or, when the user + # wrote a wildcard (``{6}respons* mobile``), the raw glob, matched against # the token stem exactly as :func:`word_matches` does. Members ALSO live in # ``exact_stems`` / ``wildcards``; the slop only governs which occurrences # highlight at full vs dim strength (see :func:`proximity_tier_indices`). @@ -397,7 +397,7 @@ def from_query( cascade pass does. ``min_term_chars`` is the post-stem floor below which auto-fuzzy is suppressed. Per-term ``~N`` modifiers in the query always produce fuzzy pairs, regardless of either - knob — explicit user opt-in overrides the safety net. + knob: explicit user opt-in overrides the safety net. Imports :func:`fnd.render._terms_from_query` lazily to avoid a render-side dependency on the parser (render already @@ -415,7 +415,7 @@ def from_query( # Proximity groups. Reuse the DSL's own expansion so ``{N} a b``, # ``a NEAR/N b`` and a typed ``"a b"~N`` all resolve to the same - # ``"…"~N`` form the matcher sees — no drift. A group is a phrase with + # ``"…"~N`` form the matcher sees: no drift. A group is a phrase with # slop > 0; its words also feed the loose term set below so every # occurrence is found (proximity only splits full vs dim at render time). # Globs and regexes match folded index tokens, so fold their accents too. @@ -435,7 +435,7 @@ def from_query( # kept out of the loose (document-wide) term set, so only the # unquoted remainder drives word-by-word highlighting. A typed # ``"a b"~N`` (slop > 0) is a proximity group, not a contiguous phrase, - # so strip those spans before extracting phrases — but keep ``"a b"~0`` + # so strip those spans before extracting phrases, but keep ``"a b"~0`` # (slop 0 == contiguous) and any separately-typed unsloped ``"a b"`` even # when a proximity group happens to share its terms. contiguous_src = _PROX_PHRASE.sub( @@ -448,12 +448,12 @@ def from_query( ) # Loose terms come from the EXPANDED query so ``{N}``/``NEAR/N`` aliases # are already rewritten to ``"…"~N`` (then stripped as quoted spans, with - # their words re-supplied via ``prox_words`` below) — identical handling + # their words re-supplied via ``prox_words`` below): identical handling # to a typed ``"a b"~N``, and the alias's ``{N}`` digits never leak into # term/colour parsing. loose_query = _strip_quoted_spans(expanded_query) # A single-word quote (`"powerhouse"`) is the same as the bare word, so - # fold it back into the loose terms — otherwise it's stripped above and + # fold it back into the loose terms, otherwise it's stripped above and # never highlights, despite the search surfacing it. "Single word" means a # single DOC_WORD_RE token, matching how _phrase_word_lists splits: a # quoted underscore identifier ("recursive_directory_iterator") is a @@ -472,7 +472,7 @@ def from_query( if single_quoted: loose_query = f"{loose_query} {' '.join(single_quoted)}".strip() # Fold proximity-group words into the loose run so they flow through the - # normal term / exact-stem / colour machinery — their quoted ``"…"~N`` + # normal term / exact-stem / colour machinery: their quoted ``"…"~N`` # span was stripped from ``loose_query`` above. Duplicates dedupe # downstream. Tokens below the split point are the free ones. free_token_count = len(loose_query.split()) @@ -487,7 +487,7 @@ def from_query( # (``+``/``-`` prefixes, surrounding parens/quotes, ``^boost``) is stripped # before classifying so a grouped/boosted token like ``(discoun*`` is # recognised as the wildcard it is; field qualifiers and prohibited - # (``-x`` / ``NOT x``) terms are dropped entirely — neither should light up. + # (``-x`` / ``NOT x``) terms are dropped entirely, neither should light up. wildcards: list[str] = [] regexes: list[str] = [] plain_tokens: list[str] = [] @@ -498,7 +498,7 @@ def from_query( skip_depth = 0 # >0 while inside a skipped NOT-/-/field-scoped group for tok_index, tok in enumerate(loose_query.split()): is_free = tok_index < free_token_count - if skip_depth > 0: # inside a skipped group — track until it closes + if skip_depth > 0: # inside a skipped group: track until it closes skip_depth = max(0, skip_depth + tok.count("(") - tok.count(")")) continue if tok == "NOT": # excludes the following token or group @@ -512,7 +512,7 @@ def from_query( # token so the explicit-fuzzy pass below can still read them. cleaned = tok.lstrip("+-").strip("()'\"") is_field = bool(_FIELD_QUALIFIER_RE.match(cleaned)) - # An open ``NOT (…)`` / ``-(…)`` / ``field:(…)`` group — none of its + # An open ``NOT (…)`` / ``-(…)`` / ``field:(…)`` group, none of its # contents are body highlight terms; skip until it closes. if tok.count("(") > tok.count(")") and (negated or is_field): skip_depth = tok.count("(") - tok.count(")") @@ -525,7 +525,7 @@ def from_query( key = re.sub(r"(?:~\d*|\^[\d.]+)+$", "", cleaned) rm = _HL_REGEX.match(key) if rm: - # Keep the pattern verbatim — lowercasing would corrupt syntax + # Keep the pattern verbatim: lowercasing would corrupt syntax # (``\D``→``\d``, ``\B``→``\b``, named groups). Case-insensitivity # is applied at match time (the doc word is already lowercased). regexes.append(rm.group(1)) @@ -583,7 +583,7 @@ def from_query( if t: raw.add(t.lower()) exact.add(index_token(t)) - # Explicit per-term ~N — always honoured (user opt-in). + # Explicit per-term ~N: always honoured (user opt-in). explicit_pairs: dict[str, int] = {} for term, dist in _terms_with_fuzzy(loose_query): if dist is None or dist <= 0: @@ -609,8 +609,8 @@ def from_query( order: list[tuple[str, str, int]] = [] seen: set[tuple[str, str]] = set() # A quoted phrase renders in the single phrase colour (slot 0). Reserve - # that slot so the loose terms around it get distinct *following* colours - # — e.g. `"defence in depth" OR diverse` → phrase yellow, diverse cyan. + # that slot so the loose terms around it get distinct *following* colours: + # e.g. `"defence in depth" OR diverse` → phrase yellow, diverse cyan. # The sentinel never matches a word in ``match_color`` (it ignores the # "phrase" kind); it only occupies the index. if multicolour and quoted_word_lists: @@ -736,7 +736,7 @@ def word_matches(word: str, spec: MatchSpec) -> bool: def match_color(word: str, spec: MatchSpec) -> int: - """Colour slot of the first ordered query term ``word`` matches — so each + """Colour slot of the first ordered query term ``word`` matches, so each distinct term in a multi-word query highlights in its own colour. Returns 0 (the default yellow slot) when no ordered term matches (e.g. a synonym variant, or an empty ``order``).""" @@ -886,7 +886,7 @@ def proximity_qualifying_indices( ``group`` is ``((member, …), slop)``. A window of tokens qualifies when all ``n`` distinct group members appear within a token span ``<= slop + (n - 1)`` (Tantivy's in-order slop bound; order is ignored, so this is a slight - superset of the matcher — fine for a dim/full split). Returns the indices of + superset of the matcher: fine for a dim/full split). Returns the indices of every group-member occurrence that falls inside at least one such window.""" positions, n = _group_member_positions(stems_by_token, group[0]) return _qualifying_from_positions(positions, n, group[1]) @@ -932,7 +932,7 @@ def glob_match_mask(word: str, glob: str) -> list[bool] | None: """For ``word`` matched by a wildcard ``glob``, one bool per char: True where the char aligns to a LITERAL glob char (a "match"), False where ``*``/``?`` filled it (a wildcard "variance"). None when the surface word doesn't fully - match the glob (e.g. stemming dropped a suffix) — caller falls back to a + match the glob (e.g. stemming dropped a suffix): caller falls back to a whole-word highlight.""" folded = fold(word).lower() if len(folded) != len(word): @@ -953,12 +953,12 @@ def align_doc_word(doc_word: str, query_word: str) -> list[bool]: Returns one ``bool`` per char of ``doc_word``: ``True`` when that char aligns to an identical char in ``query_word`` (a "match" - character), ``False`` otherwise (substitution or insertion — the + character), ``False`` otherwise (substitution or insertion; the doc has a char the query didn't have, or a different char in its place). Deletions on the query side don't produce a doc char and are silently absorbed. - Comparison is case-insensitive — case differences alone don't + Comparison is case-insensitive: case differences alone don't count as mismatches. """ a = fold(query_word).lower() @@ -1000,16 +1000,16 @@ def align_doc_word(doc_word: str, query_word: str) -> list[bool]: i -= 1 j -= 1 elif i > 0 and j > 0 and dp[i][j] == dp[i - 1][j - 1] + 1: - # Substitution — doc has a different char at this position. + # Substitution: doc has a different char at this position. matches[j - 1] = False i -= 1 j -= 1 elif j > 0 and dp[i][j] == dp[i][j - 1] + 1: - # Insertion — doc has an extra char the query didn't. + # Insertion: doc has an extra char the query didn't. matches[j - 1] = False j -= 1 elif i > 0: - # Deletion — query had a char missing from doc; nothing to + # Deletion: query had a char missing from doc; nothing to # paint on the doc side. i -= 1 else: diff --git a/fnd/meta_blob.py b/fnd/meta_blob.py index da9c3821..d739dd3d 100644 --- a/fnd/meta_blob.py +++ b/fnd/meta_blob.py @@ -11,7 +11,7 @@ The decoder restores them via a JSON ``object_hook``. The DSL evaluator needs ``dt.date`` instances on both sides for ordered comparisons (the :func:`fnd.filter_dsl._orderable` helper rejects str-vs-date), so the -round-trip is load-bearing — not just cosmetic. +round-trip is load-bearing, not just cosmetic. """ from __future__ import annotations diff --git a/fnd/migrate.py b/fnd/migrate.py index ddd1fbe2..172784c3 100644 --- a/fnd/migrate.py +++ b/fnd/migrate.py @@ -37,10 +37,10 @@ def check_schema_status(index_dir: Path) -> tuple[SchemaStatus, str | None]: ``existing_version_string`` is None for READY / EMPTY; for STALE it is the raw text content of the sidecar (so callers can show it in - error messages — including when the sidecar is garbled). + error messages, including when the sidecar is garbled). The sidecar is the cheap first signal, but Tantivy stores the schema - in ``meta.json`` too — if a prior rebuild bumped the sidecar but + in ``meta.json`` too, if a prior rebuild bumped the sidecar but crashed before Tantivy committed new segments, the sidecar lies. So when the sidecar matches we additionally try opening the index; on Tantivy ``ValueError`` we report STALE (with ``"inconsistent"`` as the diff --git a/fnd/number_synonyms.py b/fnd/number_synonyms.py index 54774f69..40cdb417 100644 --- a/fnd/number_synonyms.py +++ b/fnd/number_synonyms.py @@ -91,7 +91,7 @@ def number_synonym_groups() -> list[list[str]]: """Bidirectional ``[digit, word]`` groups for every cardinal and ordinal. - Digit-first so ``expand`` renders ``(4 OR four)`` — the original surface + Digit-first so ``expand`` renders ``(4 OR four)``: the original surface form leads regardless, this just fixes the alternative's order.""" return [[digit, word] for word, digit in (*CARDINALS.items(), *ORDINALS.items())] diff --git a/fnd/opener.py b/fnd/opener.py index 650319eb..530493ec 100644 --- a/fnd/opener.py +++ b/fnd/opener.py @@ -1,10 +1,10 @@ """Open-in-app dispatch. -* PDF: Skim URL scheme — ``skim:///#page=N`` - — handed to ``open ``. ``urllib.parse.quote`` covers every byte +* PDF: Skim URL scheme, ``skim:///#page=N``: +handed to ``open ``. ``urllib.parse.quote`` covers every byte including filename newlines/quotes that could otherwise inject into a shell-out or AppleScript string literal. -* PPTX / DOCX / MD / TXT: ``open `` (LaunchServices default app) — these +* PPTX / DOCX / MD / TXT: ``open `` (LaunchServices default app); these formats have no reliable page-jump protocol on macOS; the TUI surfaces the slide/heading in its footer so the user can scroll to it manually. """ @@ -84,7 +84,7 @@ def open_smart( 3. ``Config.app_defaults[kind]`` 4. The ``system`` built-in (``open ``). - ``pdf_strategy = "default"`` is preserved for back-compat — it forces + ``pdf_strategy = "default"`` is preserved for back-compat: it forces the ``system`` handler for the current call regardless of the resolved app. Always-default callers should use :func:`open_default`. """ diff --git a/fnd/os_labels.py b/fnd/os_labels.py index 7cd66d71..8ea585b1 100644 --- a/fnd/os_labels.py +++ b/fnd/os_labels.py @@ -1,4 +1,4 @@ -"""OS display vocabulary — what the running platform *calls* things. +"""OS display vocabulary: what the running platform *calls* things. Third seam alongside :mod:`fnd.paths` (where files live) and :mod:`fnd.launcher` (how to open/reveal them). Those two answer "where" and @@ -6,7 +6,7 @@ rows, and app labels read natively instead of hardcoding ``Finder`` / ``Alt`` on every OS. -Deliberately uncached pure functions of :func:`platform.system` — the value +Deliberately uncached pure functions of :func:`platform.system`; the value never changes within a process, and staying uncached means a test can monkeypatch the platform without the ``cache_clear()`` bookkeeping ``launcher.get_launcher`` needs. @@ -61,13 +61,13 @@ def file_manager_name() -> str: def file_manager_phrase() -> str: """The file manager as it reads mid-sentence. "Finder" and "File Explorer" are proper nouns and take no article; the generic Linux fallback is a common - noun, so it needs one — "reveals in file manager" is not English.""" + noun, so it needs one: "reveals in file manager" is not English.""" name = file_manager_name() return name if name != _FILE_MANAGER_FALLBACK else f"your {name}" def reveal_label() -> str: - """Title for the reveal action — "Reveal in Finder" and friends.""" + """Title for the reveal action: "Reveal in Finder" and friends.""" return f"Reveal in {file_manager_name()}" @@ -102,7 +102,7 @@ def alt_word() -> str: def localise(text: str) -> str: """Resolve the OS-vocabulary placeholders in ``text``. - Literal token replacement, not :meth:`str.format` — help text is full of + Literal token replacement, not :meth:`str.format`, help text is full of unrelated braces (``{60}`` proximity syntax, ``{path}`` app-argv templates) that formatting would mangle or raise on. """ diff --git a/fnd/paths.py b/fnd/paths.py index 95c32106..37cf4f5d 100644 --- a/fnd/paths.py +++ b/fnd/paths.py @@ -1,18 +1,18 @@ -"""Filesystem locations — the single source of truth for every per-user +"""Filesystem locations: the single source of truth for every per-user directory fnd reads or writes. All app state lives under two roots resolved by ``platformdirs``: -* :func:`app_data_dir` — durable state (index, config, reindex-resume, +* :func:`app_data_dir`, durable state (index, config, reindex-resume, keybindings, dismissed markers, calibration logs). -* :func:`app_cache_dir` — recomputable caches (PDF structure, seen-log, +* :func:`app_cache_dir`, recomputable caches (PDF structure, seen-log, worker stderr). Both pass ``appauthor=False`` so the two roots stay siblings on Windows (``%LOCALAPPDATA%\\fnd\\…``). Passing it inconsistently splits app data across ``…\\fnd\\`` and ``…\\fnd\\fnd\\``; on macOS/Linux ``appauthor`` is ignored, so the split only appears on Windows. Every helper below is a pure -function — no directory is created here; callers create what they need +function: no directory is created here; callers create what they need (often via :func:`fnd._perms.secure_mkdir`). """ diff --git a/fnd/query.py b/fnd/query.py index a98080bc..dfe4f8e0 100644 --- a/fnd/query.py +++ b/fnd/query.py @@ -142,7 +142,7 @@ def scope_arms( # Below this many chunks/file the thread-pool overhead outweighs the -# decode parallelism — fall back to serial decode regardless of the +# decode parallelism: fall back to serial decode regardless of the # requested ``max_workers``. _PARALLEL_DECODE_THRESHOLD: Final = 50 @@ -183,7 +183,7 @@ class Hit: # phrase-proximity reranker can measure term spread across the whole # chunk, not just the ~240-char snippet. Empty until populated. body_text: str = "" - # Decoded markdown source (from F_BODY_MD) — what the structural preview + # Decoded markdown source (from F_BODY_MD): what the structural preview # renders for this chunk. Carried alongside ``body_text`` so a caller # holding only a Hit can still ask which text the user will actually read # (see ``fnd.tui.match_evidence``); the two substrates diverge, and that @@ -210,7 +210,7 @@ def rank_key(self) -> float: @dataclass(slots=True, frozen=True) class FileChunk: - """One indexed chunk in document order — used by the TUI's full-document + """One indexed chunk in document order: used by the TUI's full-document preview. ``score`` is None for chunks that didn't match the query. ``blocks`` is the legacy plain-text Block list (used by the snippet @@ -235,7 +235,7 @@ class FileChunk: @property def body_text(self) -> str: - """Block text joined on newlines — the flat renderer's substrate. + """Block text joined on newlines: the flat renderer's substrate. Named to match :attr:`Hit.body_text` so both satisfy :class:`fnd.tui.preview_dispatcher.PreviewBody` and one evidence check @@ -404,7 +404,7 @@ def _make_snippet( words, so a stem / wildcard / fuzzy / phrase hit centres the window on the occurrence the user will see highlighted. Anchoring literally meant a hit on "testing" for the query "test" found no anchor at all and fell back to the - chunk's opening characters — a row whose snippet showed no match. + chunk's opening characters: a row whose snippet showed no match. When ``intent`` is supplied, prefers a window whose context overlaps with intent tokens. Otherwise prefers the window covering the most @@ -440,7 +440,7 @@ def _make_snippet( snippet = _window(body_text, best_pos, half) # The chosen window is centred on an anchor, so it normally shows that - # match — but a phrase span can straddle the edge once display sanitising + # match, but a phrase span can straddle the edge once display sanitising # has run. Verify rather than assume; the whole point of the change is that # a listed row shows its match. from fnd.render import text_has_any_match @@ -735,7 +735,7 @@ def _build_query( if lifted.content.strip(): raise ValueError(f"not a filter clause: {clause!r}") filters.extend(lifted.filters) - # Tags are typed state, never query text — see fnd/tag_query.py. + # Tags are typed state, never query text: see fnd/tag_query.py. if tag_filter is not None and not tag_filter.is_empty(): compiled_tags = compile_tag_filter(tag_filter, schema) if compiled_tags is not None: @@ -760,7 +760,7 @@ def _build_query( # Must-clause: chunk's visible content (F_BODY) must match. A pure-filter # query (e.g. ``kind:pdf`` alone) has no content → match every chunk and # let the filters narrow. With neither content nor filters there is - # nothing to match — return no results rather than the whole corpus. + # nothing to match: return no results rather than the whole corpus. # Building and running the query, not reading what it found back. with _engine_refusals(): has_content = bool(content.strip()) @@ -867,7 +867,7 @@ def search( def _decode_chunk(self, searcher: object, address: object) -> FileChunk: """Decode a single chunk's stored fields at ``address`` into a - :class:`FileChunk`. Independent per-address — safe to call from + :class:`FileChunk`. Independent per-address: safe to call from a worker thread because ``Searcher.doc()`` releases the GIL inside tantivy. ``searcher`` is the generation-pinned view the caller searched against; passing it explicitly (rather than @@ -900,13 +900,13 @@ def get_file_chunks(self, parent_id: str, *, max_workers: int | None = None) -> ``max_workers`` controls decode parallelism: - * ``None`` (default) or ``<= 1``: serial decode — the historic + * ``None`` (default) or ``<= 1``: serial decode; the historic behaviour, used by tests and by callers that don't want the thread-pool startup cost. * ``> 1`` *and* chunk count ≥ ``_PARALLEL_DECODE_THRESHOLD``: decode addresses concurrently via a :class:`ThreadPoolExecutor`. Tantivy releases the GIL inside ``Searcher.doc()`` so threads - are the right primitive — multiprocessing would force schema / + are the right primitive: multiprocessing would force schema / searcher serialisation per worker. * ``> 1`` and chunk count below the threshold: serial decode (the thread-pool overhead is not worth it). diff --git a/fnd/query_ast.py b/fnd/query_ast.py index ef964e9b..55bca7a5 100644 --- a/fnd/query_ast.py +++ b/fnd/query_ast.py @@ -3,14 +3,14 @@ inside ``AND``/``OR``/``NOT`` and parentheses. Tantivy's own ``parse_query`` is the only string→Query parser the binding -exposes, and it silently drops ``*`` and no-ops ``~N`` — so anything wrapped in +exposes, and it silently drops ``*`` and no-ops ``~N``, so anything wrapped in a boolean expression lost its wildcard/fuzzy. There is no native helper for the boolean *structure* (``wildcard_query_to_regex_str`` only translates one term), so we own the tree: parse here, lower each leaf with the existing resolvers in :mod:`fnd.query_compile`, and assemble with ``boolean_query``. Grammar (precedence low→high), implicit adjacency = ``OR`` (the weighted -default — bare multi-term OR-retrieves, BM25 ranks all-term docs higher):: +default: bare multi-term OR-retrieves, BM25 ranks all-term docs higher):: or := and ( (OR | ) and )* and := unary ( AND unary )* @@ -27,13 +27,13 @@ from fnd.query_spans import literal_spans -# Leaf classification — mirrors fnd.query so the boolean path and the flat path +# Leaf classification: mirrors fnd.query so the boolean path and the flat path # recognise the same operator tokens. _WILDCARD_RE: Final = re.compile(r"^(\w+)\*$") # trailing prefix wildcard ``crypto*`` _FUZZY_RE: Final = re.compile(r"^(\w+)~(\d*)$") # ``term~`` / ``term~N`` _REGEX_RE: Final = re.compile(r"^/(.+)/$") # ``/pattern/`` _GLOB_RE: Final = re.compile(r"[*?]") # any ``*``/``?`` → glob (infix/leading) -# A boost literal is a plain number (`^2`, `^1.5`) — kept strict so a malformed +# A boost literal is a plain number (`^2`, `^1.5`): kept strict so a malformed # form like `foo^1.2.3` never reaches float() and crashes the query. _NUMBER: Final = r"\d+(?:\.\d+)?" _PHRASE_RE: Final = re.compile(rf"""^(['"])(.*)\1(?:~(\d+))?(?:\^({_NUMBER}))?$""", re.DOTALL) @@ -129,7 +129,7 @@ def prefix_only() -> bool: continue if ch in "[{" and buf and buf[-1] == ":": # ``field:[lo TO hi]`` (inclusive) or ``field:{lo TO hi}`` - # (exclusive) — a range. The ``TO`` and its surrounding spaces are + # (exclusive): a range. The ``TO`` and its surrounding spaces are # part of the range syntax, so keep the whole bracket/brace run as # one atom (the leaf is handed to parse_query, which understands # ranges). Ranges may mix delimiters (``[lo TO hi}``), so close on @@ -147,7 +147,7 @@ def prefix_only() -> bool: continue if ch == "(": if buf and buf[-1] == ":": - # ``field:(…)`` — keep the scoped group attached as one atom. The + # ``field:(…)``, keep the scoped group attached as one atom. The # AST has no field node, so the leaf is handed to parse_query # (which understands field syntax); other leaves around it # (wildcards/fuzzy) still compile normally. @@ -231,7 +231,7 @@ def _next(self) -> tuple[str | None, str]: def parse(self) -> Node | None: node = self._parse_or() - # Ignore any dangling tokens (malformed tails) — best-effort. + # Ignore any dangling tokens (malformed tails): best-effort. return node def _parse_or(self) -> Node | None: @@ -318,7 +318,7 @@ def _classify(value: str) -> Node | None: def _classify_core(value: str) -> Node | None: rm = _REGEX_RE.match(value) if rm: - return Regex(rm.group(1)) # verbatim — lowercasing corrupts \D/\B/named groups + return Regex(rm.group(1)) # verbatim: lowercasing corrupts \D/\B/named groups fuzzy = fuzzy_word(value) if fuzzy is not None: return fuzzy diff --git a/fnd/query_compile.py b/fnd/query_compile.py index b8807315..4c287dc1 100644 --- a/fnd/query_compile.py +++ b/fnd/query_compile.py @@ -101,7 +101,7 @@ def compile(self, n: Node) -> Query: return Query.boolean_query( [(Occur.Must, Query.all_query()), (Occur.MustNot, self.compile(n.child))] ) - # Required (the only remaining node) — its child carries the query; the + # Required (the only remaining node): its child carries the query; the # forced-Must occur is applied by the enclosing group in _assemble. return self.compile(n.child) diff --git a/fnd/query_dsl.py b/fnd/query_dsl.py index d56c280c..e9434fbf 100644 --- a/fnd/query_dsl.py +++ b/fnd/query_dsl.py @@ -144,11 +144,11 @@ def repl(match: re.Match[str]) -> str: # boolean operator nor a field qualifier (``word:``) and carries no paren or # brace. Excluding braces stops a following ``{N}`` from being swallowed as a # run word (``{0}{0}`` → ``"{0}"~0``), which left a brace inside quotes that the -# next pass re-expanded — breaking idempotency. +# next pass re-expanded: breaking idempotency. _RUN_TOKEN: Final = r'(?:"[^"]*"|(?:(?!(?:AND|OR|NOT)\b)(?![^\s()]*:)[^\s(){}]+))' # noqa: S105 (a regex, not a password) _BRACE_PROX: Final = re.compile(rf"\{{(\d+)\}}\s*((?:{_RUN_TOKEN})(?:\s+{_RUN_TOKEN})*)?") -# A residual brace group that is a proximity attempt (no ``TO`` — that would be +# A residual brace group that is a proximity attempt (no ``TO``: that would be # a Tantivy exclusive range, which we leave alone). _NEAR_WORD: Final = r"[\w*?]+(?:~\d*)?" @@ -160,7 +160,7 @@ def _expand_proximity_aliases(q: str) -> str: ``{N}`` binds to the immediately-following run of plain words / quoted phrases and stops at the first boolean operator, field qualifier, or - parenthesis — the remainder is preserved verbatim. A ``{N}`` with no usable + parenthesis: the remainder is preserved verbatim. A ``{N}`` with no usable run is left in place for :func:`check_proximity` to flag. """ @@ -168,7 +168,7 @@ def brace_repl(match: re.Match[str]) -> str: slop = int(match.group(1)) run = (match.group(2) or "").strip() if not run: - return match.group(0) # nothing to bind to — leave for validation + return match.group(0) # nothing to bind to: leave for validation inner = " ".join(run.replace('"', " ").split()) return f'"{inner}"~{slop}' @@ -187,8 +187,8 @@ def check_proximity(expanded: str) -> None: """Raise :class:`QuerySyntaxError` if a proximity brace survived expansion. Runs on the *expanded* query: a well-formed ``{N} a b`` is already - ``"a b"~N`` (no brace left), so anything matching here — ``{60}`` alone, - ``{abc}``, ``{}``, ``{-5}`` — is a malformed proximity the user can fix. + ``"a b"~N`` (no brace left), so anything matching here (``{60}`` alone, + ``{abc}``, ``{}``, ``{-5}``) is a malformed proximity the user can fix. Braces inside quotes (literal text) and ``{lo TO hi}`` ranges are ignored. """ from fnd.query_errors import QuerySyntaxError @@ -218,10 +218,10 @@ def split_metadata_filter(query: str) -> tuple[str, str | None]: Returns ``(lexical_query, metadata_filter_or_None)``. ``[…]`` blocks appearing inside a quoted phrase or a ``/regex/`` are left intact, and a ``field:[lo TO hi]`` - range (the ``[`` follows a ``:``) is a numeric range — left in the lexical + range (the ``[`` follows a ``:``) is a numeric range, left in the lexical query, not treated as a filter. A filter may contain nested ``in [ … ]`` lists. An empty ``[]`` is treated as no filter. Two or more top-level filter - blocks raise ``ValueError`` — compose alternatives with ``AND``/``OR`` + blocks raise ``ValueError``: compose alternatives with ``AND``/``OR`` inside the single block. Whitespace around the extracted clause is collapsed so the resulting @@ -240,7 +240,7 @@ def split_metadata_filter(query: str) -> tuple[str, str | None]: continue if ch == "[": if bracket_start is None and i > 0 and query[i - 1] == ":": - # field:[lo TO hi] — a numeric range, not a metadata filter. + # field:[lo TO hi]; a numeric range, not a metadata filter. i += 1 continue if bracket_start is None: diff --git a/fnd/query_errors.py b/fnd/query_errors.py index 51a5cd9f..19940423 100644 --- a/fnd/query_errors.py +++ b/fnd/query_errors.py @@ -1,5 +1,5 @@ """Typed query errors shared across the DSL pre-pass, the query planner, and -the Tantivy boundary. Leaf module — no runtime imports — so any layer can raise +the Tantivy boundary. Leaf module (no runtime imports) so any layer can raise these without risking an import cycle.""" from __future__ import annotations @@ -22,7 +22,7 @@ class QueryError(ValueError): class QuerySyntaxError(QueryError): """A query the user can fix: malformed proximity, unbalanced brackets, or a string Tantivy's parser rejects. ``hint`` is a practical one-liner shown - next to the query input — keep it calm and actionable.""" + next to the query input: keep it calm and actionable.""" def __init__(self, message: str, *, hint: str | None = None) -> None: super().__init__(message) @@ -42,7 +42,7 @@ def __init__(self) -> None: class UnknownFilterValueError(QueryError): - """A filter value outside its closed set of legal values — a typo'd + """A filter value outside its closed set of legal values: a typo'd collection name, file kind, or date token. Carries the near-misses so the CLI can offer a correction instead of a @@ -102,7 +102,7 @@ class MissingFilterValueError(UnknownFilterValueError): """A required scope the user didn't name at all. Same shape as an unrecognised value so the CLI's report-and-offer path - handles it unchanged — the difference is only what the one-liner says and + handles it unchanged: the difference is only what the one-liner says and what it proposes. Used where naming nothing must not silently widen: a reindex covering every collection is a minutes-long rebuild, so it has to be asked for rather than defaulted into. @@ -140,5 +140,5 @@ def hint(self) -> str | None: class QueryTooLargeError(QueryError): """Query exceeds the size / complexity bounds in :mod:`fnd.extract._limits`. - Defensive today — the local user is the only author — but pinned so any + Defensive today (the local user is the only author) but pinned so any future URL-handler / Spotlight / ``--query-from-file`` path inherits it.""" diff --git a/fnd/query_fields.py b/fnd/query_fields.py index 5528f902..1d615228 100644 --- a/fnd/query_fields.py +++ b/fnd/query_fields.py @@ -95,13 +95,13 @@ def date_token_range(token: str) -> tuple[int, int] | None: ``(low, high)`` unix range, or None if not a known token. ``today`` means "since UTC midnight today" (modified today), not "since this - exact instant" — the latter would match nothing. week/month/year are the + exact instant": the latter would match nothing. week/month/year are the documented "within the last N days" cumulative windows. """ if token not in _DATE_TOKEN_DAYS: return None now = _now_ts() - if token == "today": # noqa: S105 — keyword, not a secret + if token == "today": # noqa: S105, keyword, not a secret return (now - now % 86_400, FAR_FUTURE) # UTC midnight today return (now - _DATE_TOKEN_DAYS[token] * 86_400, FAR_FUTURE) diff --git a/fnd/query_filters.py b/fnd/query_filters.py index 856de079..2c045c3f 100644 --- a/fnd/query_filters.py +++ b/fnd/query_filters.py @@ -9,7 +9,7 @@ Extraction is deliberately conservative: a ``field:`` clause is lifted only when it sits at the top level (not inside ``(...)``) and is not adjacent to a boolean operator. Boolean-composed field clauses (``kind:pdf OR kind:docx``) are left in -the content query for tantivy's parser to handle — never silently mis-filtered. +the content query for tantivy's parser to handle, never silently mis-filtered. """ from __future__ import annotations @@ -26,7 +26,7 @@ from fnd.query_spans import literal_spans _BOOL_OPS = frozenset({"AND", "OR", "NOT"}) -# field:value head — value captured greedily (the tokenizer already kept any +# field:value head, value captured greedily (the tokenizer already kept any # bracketed/quoted run together as one token). _CLAUSE_RE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*):(.+)$", re.DOTALL) # ``[`` / ``]`` include a bound, ``{`` / ``}`` exclude it, mixed freely. @@ -90,7 +90,7 @@ def scan_exact_values(query: str) -> list[tuple[str, str]]: Reuses the tokenizer and clause grammar that lower these clauses into filters, so a caller validating the values can't disagree with the engine about what counts as one. Groups (``kind:(a b)``) and collection comma - lists (``c:a,b``) are split exactly the way :func:`_compile` splits them — + lists (``c:a,b``) are split exactly the way :func:`_compile` splits them, notably ``kind:a,b`` is NOT split, because the compiler treats it as a single term. @@ -252,7 +252,7 @@ def extract_filters( field, value = m.group(1), m.group(2) if field in ("has", "exists"): # Presence query. A text field: any doc with a non-empty term. - # A numeric (u64) field: a real (non-zero) value — ``regex_query`` + # A numeric (u64) field: a real (non-zero) value, ``regex_query`` # is text-only and ``.+`` would also match the 0 default, so use a # ``>= 1`` range (``has:page`` ⇒ paginated, ``exists:mtime`` ⇒ dated). target = resolve(value) diff --git a/fnd/render.py b/fnd/render.py index 9dcb1f7d..fbaa82c1 100644 --- a/fnd/render.py +++ b/fnd/render.py @@ -29,7 +29,7 @@ # Colours are RGB, never ANSI names: Textual's opacity blend skips ANSI colours, # so an ANSI foreground shows through a preview hidden at opacity 0. HIGHLIGHT_STYLE = "bold #1a1a1a on #ffd866" -# Mismatch overlay for fuzzy-pass hits — orange so the eye can read +# Mismatch overlay for fuzzy-pass hits: orange so the eye can read # at a glance which char(s) in a near-match diverge from what the user # typed. Same black foreground as HIGHLIGHT_STYLE so adjacent # yellow/orange runs feel like one painted word. @@ -47,7 +47,7 @@ ] # Dimmed ("receded") variants, used for proximity-group matches that fall OUTSIDE # a qualifying co-occurrence window: each swatch is a ~50/50 blend of the full -# colour and the dark app background, with a normal-weight body foreground — so +# colour and the dark app background, with a normal-weight body foreground, so # the match stays clearly visible but reads as obviously secondary to the bright, # black-on-colour full highlights. Pre-computed (not SGR ``faint``) for # deterministic, terminal-independent rendering. Parallel to MATCH_STYLES. @@ -109,7 +109,7 @@ def text_has_any_match(text: str, spec: MatchSpec) -> bool: def text_has_full_match(text: str, spec: MatchSpec) -> bool: """Like :func:`text_has_any_match`, but a proximity-group word that falls - OUTSIDE a qualifying co-occurrence window does NOT count — only a *full* + OUTSIDE a qualifying co-occurrence window does NOT count: only a *full* (in-window) match or a quoted-phrase span does. Lets a preview scroll target prefer a real co-occurrence cell over a lone dimmed term above it. @@ -149,7 +149,7 @@ def phrase_gap_spans( by a per-term span. Phrase highlighting (a stopword between content words, or a quoted phrase) must never *overlap* a term span: in multi-colour mode the phrase colour and the term colour differ, and Textual's Content drops - overlapping differently-styled spans — so the whole word goes unhighlighted. + overlapping differently-styled spans, so the whole word goes unhighlighted. Filling only the gaps (e.g. the ``in`` of ``Defence-in-Depth``) keeps every span non-overlapping.""" out: list[tuple[int, int]] = [] @@ -176,7 +176,7 @@ def _proximity_tiers( by a glob member such as ``respons*``), minus any the query also asserts outside the group; ``full`` are those inside a qualifying window. A member outside ``full`` renders dimmed. Returns empties when the query carries no - proximity operator — so plain queries skip stemming entirely.""" + proximity operator, so plain queries skip stemming entirely.""" if not spec.proximity_groups: return frozenset(), frozenset() from fnd.matching import proximity_tier_indices @@ -198,7 +198,7 @@ def match_word_spans_multi( ``segments`` are consecutive slices of one document (the markdown blocks of a chunk, a chunk's rendered lines), so a ``{N}`` / ``NEAR/N`` / ``"a b"~N`` - window has to be able to straddle a boundary — scoping it to one block is + window has to be able to straddle a boundary: scoping it to one block is what made an adjacent paragraph and list item render dimmed. Offsets in the result are segment-local. @@ -303,7 +303,7 @@ def keep_shown(hits: list[Hit], query: str) -> list[Hit]: def match_word_spans(plain: str, spec: MatchSpec) -> list[tuple[int, int, str]]: """Absolute ``(start, end, style)`` highlight runs for every matching word in - ``plain`` — the single-segment form of :func:`match_word_spans_multi`.""" + ``plain``: the single-segment form of :func:`match_word_spans_multi`.""" return match_word_spans_multi((plain,), spec)[0] @@ -313,7 +313,7 @@ def apply_match_highlights_multi(rendered: Sequence[Text], spec: MatchSpec) -> l Per line: loose words via :func:`match_word_spans_multi` (proximity-aware across the WHOLE run, so a ``{N}`` window can straddle a line break), then - quoted/connector phrases in the GAPS between term spans — never overlapping + quoted/connector phrases in the GAPS between term spans, never overlapping them, so per-term colours survive. Phrases stay line-local because a quoted phrase is contiguous by definition.""" from fnd.matching import phrase_char_spans @@ -365,11 +365,11 @@ def word_highlight_runs( ) -> list[tuple[int, int, str]]: """Per-char highlight runs for ``word``, coloured by *how* it matched: - * wildcard / glob — literal chars in the term's colour, ``*``/``?``-filled + * wildcard / glob: literal chars in the term's colour, ``*``/``?``-filled chars orange; - * exact-stem / fuzzy — chars aligning to the typed term in its colour, the + * exact-stem / fuzzy: chars aligning to the typed term in its colour, the typo / stem-suffix divergence orange; - * regex (or any match with no clean char attribution) — whole word in colour. + * regex (or any match with no clean char attribution): whole word in colour. In a multi-word query each term has its own colour (see :data:`MATCH_STYLES`); single-term queries use slot 0 (yellow). ``dim`` selects the receded palette @@ -393,7 +393,7 @@ def word_highlight_runs( # suffix shows as orange. Checked BEFORE the wildcard mask so a word that # exactly matches a typed term (e.g. ``discount`` under ``discount discoun*``) # renders as a clean exact hit rather than having its tail painted as - # wildcard variance. Only for words that matched THIS way (not regex) — + # wildcard variance. Only for words that matched THIS way (not regex), # otherwise an unrelated raw term would paint the whole word orange. s = index_token(word) matched_exact_or_fuzzy = s in spec.exact_stems or spec.fuzzy_admits(s) @@ -458,7 +458,7 @@ def _terms_from_query(query: str, *, keep_stopwords: bool = False) -> list[str]: # Tokenize the same way the highlighter splits doc text (``DOC_WORD_RE``, # which mirrors the index analyser and splits on underscore too) so a term # carrying adjacent punctuation ("3." / "Monitoring,") or an underscore - # ("recursive_directory_iterator") yields the bare sub-words — their stems + # ("recursive_directory_iterator") yields the bare sub-words: their stems # then match the clean doc-word stems instead of silently failing. # Stopwords are dropped: they carry ~zero IDF and highlighting every # "and"/"in" doc-wide is noise (quoted phrases keep their stopwords via the @@ -529,13 +529,13 @@ def render_chunk_pieces( can mount per-line widgets and ``scroll_to_widget`` targets the specific matched line. * **Chunk has no match**: emit a single piece containing the whole - chunk body — far fewer widgets to mount on long PDFs (a 105-page + chunk body: far fewer widgets to mount on long PDFs (a 105-page winelist is ~3000 widgets all-per-line vs ~280 with this split). The returned line Texts already have query-term highlights applied. When ``match_spec`` is provided (an :class:`fnd.matching.MatchSpec`), fuzzy-AUTO and synonym variants are highlighted in addition to - literal stems — same semantics the cascade fuzzy / synonym passes + literal stems, same semantics the cascade fuzzy / synonym passes use. Fall-back to literal-only stem matching when ``match_spec`` is ``None`` (preserves the old test surface). """ @@ -562,7 +562,7 @@ def render_chunk_pieces( return header, pieces # Match-bearing chunk: per-line pieces so we have precise scroll targets. - # Build every line first, then highlight them as one run — a proximity + # Build every line first, then highlight them as one run: a proximity # window has to be able to straddle a line break, which per-line # highlighting made impossible at any slop. lines: list[Text] = [] @@ -620,7 +620,7 @@ def render_chunk_body_only(chunk: Any, *, query: str = "") -> Text: def render_chunk_rich(chunk: Any, *, query: str = "") -> Text: - """Render one chunk as a Rich :class:`Text` — section header + body + """Render one chunk as a Rich :class:`Text`, section header + body blocks + query-term highlights. Used by the TUI's preview pane (one Static widget per chunk so scroll_to_widget targets work precisely regardless of how lines wrap visually).""" @@ -674,7 +674,7 @@ def _chunk_header(c: object) -> str: breaking"); falls back to whichever piece is present otherwise. PDF page locator prefers the printed ``page_label`` (e.g. "292" or - "iv") when the PDF carries explicit labels — that's what the + "iv") when the PDF carries explicit labels: that's what the reader actually sees on the page. ``page`` (the PDF page index) is used only for the opener and as a fallback when no label exists. """ diff --git a/fnd/rerank.py b/fnd/rerank.py index 87816f9f..232c008f 100644 --- a/fnd/rerank.py +++ b/fnd/rerank.py @@ -40,7 +40,7 @@ class RankingProfile: """Per-profile knobs. - All zeros / empty maps are the identity — passing :class:`RankingProfile` + All zeros / empty maps are the identity: passing :class:`RankingProfile` with no args leaves the BM25 order untouched. """ @@ -85,7 +85,7 @@ def apply_recency_boost(*, score: float, mtime: int, profile: RankingProfile, no * Future-dated ``mtime`` (clock skew) is clamped at ``now`` so the bonus is bounded by the configured maximum. * ``mtime = 0`` (unknown) effectively returns the base score because - the decay over ``now`` seconds is ~0 — neutral, not penalising. + the decay over ``now`` seconds is ~0: neutral, not penalising. """ if profile.recency_boost == 0.0: return score @@ -112,7 +112,7 @@ def apply_phrase_proximity( contiguous window covering at least one occurrence of every term. Map that window size onto ``[0, profile.phrase_proximity]``. - Multi-term queries only — single-term queries get neutral 0 bonus + Multi-term queries only: single-term queries get neutral 0 bonus because there's nothing to cluster. """ if profile.phrase_proximity == 0.0: @@ -128,7 +128,7 @@ def apply_phrase_proximity( positions[st].append(i) if any(not v for v in positions.values()): - # A required term doesn't appear in the body — no window possible. + # A required term doesn't appear in the body: no window possible. return score window = _smallest_covering_window(positions) diff --git a/fnd/schema.py b/fnd/schema.py index 1a7c0d76..d635a1be 100644 --- a/fnd/schema.py +++ b/fnd/schema.py @@ -1,4 +1,4 @@ -"""Tantivy schema definition — single source of truth for the index format. +"""Tantivy schema definition: single source of truth for the index format. Changing fields = full reindex; the writer compares ``SCHEMA_VERSION`` against the index sidecar and refuses to load on mismatch. @@ -47,7 +47,7 @@ # freshness) and the two tag fields. Tags are split by provenance so # toggling a source in config is a query-time change, not a reindex. # v9 (2026-07-23): fine-grained kinds + new suffixes (epub, html, ipynb, odf, -# per-language code, data/config). No field or schema-shape change — the bump +# per-language code, data/config). No field or schema-shape change: the bump # forces a reindex so existing collections pick up the newly-supported files. # v10 (2026-09-18): normalise cross-collection storage. A file is stored once, # not once per collection: F_COLLECTION is multi-valued (the set a file belongs @@ -89,7 +89,7 @@ F_SLIDE: Final = "slide" # 1-based source line of the chunk's first character (MD heading-open # line, TXT chunk window start). 0 for kinds without line tracking -# (PDF, DOCX, PPTX) — they have page/slide/heading_path instead. +# (PDF, DOCX, PPTX): they have page/slide/heading_path instead. F_LINE: Final = "line" F_HEADING_PATH: Final = "heading_path" F_TITLE: Final = "title" diff --git a/fnd/state.py b/fnd/state.py index b2479ed6..4e6a1926 100644 --- a/fnd/state.py +++ b/fnd/state.py @@ -1,4 +1,4 @@ -"""Persistent UI state — active scope, panel collapse, etc. +"""Persistent UI state: active scope, panel collapse, etc. Stored in ``/state/scope.toml`` so it survives app restarts. Atomic writes (temp file + rename) so a crash mid-write @@ -40,7 +40,7 @@ class UiState: # ``"any"`` = "any date". Anything else is treated as a literal token # for the DSL pre-pass (kind:pdf, mtime:week, …). # - # Fuzzy/synonym/etc. are NOT filters — they're cascade passes that + # Fuzzy/synonym/etc. are NOT filters: they're cascade passes that # broaden a sparse-result query automatically. The filters # panel only carries scope-narrowing knobs. filter_kinds: list[str] = field(default_factory=list) @@ -51,7 +51,7 @@ class UiState: tag_include: dict[str, list[str]] = field(default_factory=dict) tag_exclude: dict[str, list[str]] = field(default_factory=dict) tag_match_all: bool = True - # True only when an on-disk state file was actually read. Not persisted — + # True only when an on-disk state file was actually read. Not persisted: # it distinguishes "this profile has never saved a scope" from "the user # deliberately saved an empty one", which look identical in the fields # above but mean opposite things when seeding a first-launch scope. @@ -62,7 +62,7 @@ class UiState: def load(path: Path | None = None) -> UiState: """Read the on-disk state. Missing / unreadable files return an - empty :class:`UiState` rather than raising — first launch is a + empty :class:`UiState` rather than raising, first launch is a valid state.""" p = path if path is not None else _state_path() if not p.exists(): diff --git a/fnd/stopwords.py b/fnd/stopwords.py index ac5e4602..338ca375 100644 --- a/fnd/stopwords.py +++ b/fnd/stopwords.py @@ -2,7 +2,7 @@ Function words carry ~zero IDF. They matter inside quoted phrases (so the phrase still matches) but a bare-word query that overlaps a chunk *only* -via a stopword is pure noise — the chunk ranks at the tail yet is still +via a stopword is pure noise: the chunk ranks at the tail yet is still retrieved. :func:`strip_query_stopwords` drops standalone stopwords from a plain bag-of-words query so retrieval gates on content terms. It bails on any query carrying special syntax (quotes, field qualifiers, booleans, @@ -60,7 +60,7 @@ ) # Any of these means the query carries explicit syntax (phrase, field -# qualifier, boolean/required term, range, proximity/fuzzy, wildcard) — we +# qualifier, boolean/required term, range, proximity/fuzzy, wildcard): we # leave it verbatim so stripping can never break it. ``-`` / ``+`` only count # as operators at a token boundary so hyphenated words ("defence-in-depth") # stay plain. diff --git a/fnd/synonyms.py b/fnd/synonyms.py index d54b704c..672eeee0 100644 --- a/fnd/synonyms.py +++ b/fnd/synonyms.py @@ -10,7 +10,7 @@ * Multi-word synonyms come back wrapped in double quotes so the Tantivy parser treats them as phrases, not three independent OR'd terms. * Terms inside an existing quoted phrase in the user's query are NOT - expanded — the user already asked for an exact match, and overriding + expanded: the user already asked for an exact match, and overriding that would surprise them. Group entries are bidirectional: any one form expands to the rest. @@ -33,7 +33,7 @@ class SynonymTable: """Canonical lookup table built from user TOML. - Stored as an immutable tuple of normalized groups — fully immutable (not + Stored as an immutable tuple of normalized groups: fully immutable (not just ``frozen``) so the memoised default table cannot be poisoned by an in-place mutation. Lookups are case-insensitive on the surface form but the original casing of the synonyms is preserved in the expansion output @@ -75,7 +75,7 @@ def expansions_for(self, term: str) -> tuple[str, ...] | None: def load_synonyms(path: Path) -> SynonymTable: - """Load a synonyms TOML file. Missing files yield an empty table — the + """Load a synonyms TOML file. Missing files yield an empty table: the user opts in by creating the file.""" if not path.exists(): return SynonymTable() @@ -182,7 +182,7 @@ def expand(query: str, table: SynonymTable) -> str: Matches single words, multi-word phrases (hyphen/space-agnostic, so both ``multi-factor authentication`` and ``multi factor authentication`` expand), and whole quoted phrases. Words inside a user's quoted phrase are not - expanded individually — that span is already an exact-match request. The + expanded individually: that span is already an exact-match request. The original term stays in the disjunction so an exact hit still scores. """ if not table.groups: @@ -207,7 +207,7 @@ def in_literal(pos: int) -> bool: continue # Token-tuple lookup (not exact string) so a quoted phrase expands # regardless of hyphen/space, matching the bare-word path below. A - # single quoted token (e.g. "4", "mfa") is left literal — quoting one + # single quoted token (e.g. "4", "mfa") is left literal: quoting one # word is the clearest exact-match request, so it never expands; only # genuine multi-word phrases ("multi factor authentication") do. key = tuple(re.findall(r"\w+", fold(m.group(1)))) diff --git a/fnd/tag_catalogue.py b/fnd/tag_catalogue.py index f6a0f2cd..0128a2de 100644 --- a/fnd/tag_catalogue.py +++ b/fnd/tag_catalogue.py @@ -5,7 +5,7 @@ Counts are FILE counts, not document counts. A tantivy document is a chunk, so a 40-chunk PDF tagged ``report`` would otherwise report as 40. The aggregation -buckets by file and tallies tags within each, which is exact — see the note in +buckets by file and tallies tags within each, which is exact: see the note in :func:`tag_catalogue` for why the cardinality approach was abandoned. """ @@ -107,8 +107,8 @@ def tag_catalogue( # Bucket by FILE, then by tag within each file, and count the file buckets # each tag appears in. The obvious shape (bucket by tag, cardinality over - # parent_id) silently returns 0 for some buckets — measured on a real - # corpus, one tag had 34 chunks and a cardinality of 0.0 — which would hide + # parent_id) silently returns 0 for some buckets: measured on a real + # corpus, one tag had 34 chunks and a cardinality of 0.0, which would hide # real tags behind a "(0)" count. This inversion is exact and, measured on # the same corpus, faster. agg: dict[str, object] = { diff --git a/fnd/tag_query.py b/fnd/tag_query.py index ee24b163..abfb0f33 100644 --- a/fnd/tag_query.py +++ b/fnd/tag_query.py @@ -43,7 +43,7 @@ def _terms(selection: dict[str, frozenset[str]], schema: tantivy.Schema) -> list Grouping by value matters: ``--tag recipe`` with both sources enabled fans out to {frontmatter: recipe, os: recipe}, and ANDing those would - demand the same tag in frontmatter *and* Finder — which a real file + demand the same tag in frontmatter *and* Finder, which a real file essentially never satisfies. Distinct values stay separate, so ``match_all`` still ANDs genuinely different selections. diff --git a/fnd/tags.py b/fnd/tags.py index 24906c7c..685eeacb 100644 --- a/fnd/tags.py +++ b/fnd/tags.py @@ -144,7 +144,7 @@ class FrontmatterTagProvider: """YAML frontmatter tags. Portable to every platform. Reads ``tags:`` / ``tag:`` always. ``extra_keys`` opts additional - frontmatter keys in as tag sources — many vaults keep their real taxonomy + frontmatter keys in as tag sources: many vaults keep their real taxonomy in fields like ``Project:`` or ``Type:`` rather than ``tags:``. Extra-key values are namespaced under the key (``project/kitchen``) so they @@ -197,7 +197,7 @@ def read(self, ctx: TagContext) -> frozenset[str]: # Finder tags live in an xattr holding a binary plist. os.getxattr is # Linux-only and absent on macOS, and /usr/bin/xattr costs a process spawn # per file (measured ~192x slower), so this calls libc directly. macOS's -# getxattr takes two extra arguments (position, options) versus Linux — +# getxattr takes two extra arguments (position, options) versus Linux: # one reason the call stays inside the platform-specific provider. _XATTR_USER_TAGS = b"com.apple.metadata:_kMDItemUserTags" @@ -206,7 +206,7 @@ def _load_getxattr() -> object | None: try: lib = ctypes.util.find_library("c") if not lib: - # CDLL(None) loads the running program, not libc — bail rather + # CDLL(None) loads the running program, not libc: bail rather # than resolve getxattr against the wrong image. return None libc = ctypes.CDLL(lib, use_errno=True) @@ -310,7 +310,7 @@ def providers_for( def read_tags(ctx: TagContext, providers: Sequence[TagProvider]) -> dict[str, frozenset[str]]: """Tags per provider id. - A failing provider contributes an empty set — tag reading must never fail + A failing provider contributes an empty set: tag reading must never fail an index run. """ out: dict[str, frozenset[str]] = {} diff --git a/fnd/texture_maintenance.py b/fnd/texture_maintenance.py index 50115fb1..0e6f3729 100644 --- a/fnd/texture_maintenance.py +++ b/fnd/texture_maintenance.py @@ -19,7 +19,7 @@ def live_content_shas(config: Config) -> set[str]: """Content hashes of every PDF currently reachable under any - collection's sources. Hashes file bytes, so it's O(corpus) — only + collection's sources. Hashes file bytes, so it's O(corpus): only call from an explicit maintenance action, never a hot path.""" import contextlib diff --git a/fnd/tui/_md_flat.py b/fnd/tui/_md_flat.py index 41899798..cc2fc3db 100644 --- a/fnd/tui/_md_flat.py +++ b/fnd/tui/_md_flat.py @@ -1,4 +1,4 @@ -"""W8 prototype — render md chunks to styled flat lines. +"""W8 prototype: render md chunks to styled flat lines. Renders each chunk's ``body_md`` (or falls back to ``body_text``) via ``rich.markdown.Markdown`` to width-wrapped styled lines, then bakes diff --git a/fnd/tui/_md_hybrid.py b/fnd/tui/_md_hybrid.py index 01b7242d..d87e349c 100644 --- a/fnd/tui/_md_hybrid.py +++ b/fnd/tui/_md_hybrid.py @@ -1,4 +1,4 @@ -"""W-Hybrid prototype — per-chunk hybrid widget. +"""W-Hybrid prototype, per-chunk hybrid widget. Replaces the per-block widget tree (`FNDMarkdown` -> 50+ widgets) with a small ordered list of widgets per chunk: @@ -112,7 +112,7 @@ def _bake_match_spans_into_text(text: Text, spec: MatchSpec) -> bool: text.stylize(str(style), a, b) covered.update(range(a, b)) hit = True - # Phrases fill only the GAPS between word spans — an overlapping span of a + # Phrases fill only the GAPS between word spans: an overlapping span of a # different style makes Textual's Content drop both, blanking the word. for start, end in phrase_gap_spans(phrase_char_spans(plain, spec), covered): text.stylize(HIGHLIGHT_STYLE, start, end) @@ -215,7 +215,7 @@ def _build_table_widget(md_text: str, spec: MatchSpec) -> tuple[DataTable[Text], def _build_fence_widget(md_text: str, spec: MatchSpec) -> tuple[Static, bool]: """Render a fence as syntax-highlighted ``Text`` with match spans - overlaid on top. One Static widget per fence — fits in the parent + overlaid on top. One Static widget per fence: fits in the parent ScrollView, but no horizontal scroll / focus (those are MarkdownFence's value-add and would require the upstream widget, which needs a Markdown parent). diff --git a/fnd/tui/_sigint_kill_switch.py b/fnd/tui/_sigint_kill_switch.py index 379b9fae..1f3cb1c5 100644 --- a/fnd/tui/_sigint_kill_switch.py +++ b/fnd/tui/_sigint_kill_switch.py @@ -2,7 +2,7 @@ Textual normally clears ``ISIG`` in termios so ``^C`` arrives as a byte on stdin, handled via a regular key binding. That binding can only fire -when the event loop is responsive — if a coroutine ever blocks the loop +when the event loop is responsive, if a coroutine ever blocks the loop (slow filesystem walk, hung syscall, downstream silent crash) the user's ``^C`` does nothing and the only escape is killing the terminal emulator. @@ -18,7 +18,7 @@ - Signal handlers run on the **main thread** (the same thread as the asyncio loop). Textual's ``call_from_thread`` rejects calls from the loop's own thread with ``RuntimeError``. A direct ``app.exit()`` call - sets the exit flag but doesn't actually unwind the loop — the loop + sets the exit flag but doesn't actually unwind the loop: the loop has to process its own message queue first, and ``post_message`` inside a signal-handler frame doesn't reliably do so. The working primitive is ``loop.call_soon_threadsafe(app.exit)``: thread-safe by @@ -61,13 +61,13 @@ def install(app: App[Any]) -> Iterator[None]: installs a SIGINT handler that: - First press: schedule ``app.exit`` via - ``loop.call_soon_threadsafe`` — the only call that reliably fires + ``loop.call_soon_threadsafe``; the only call that reliably fires the asyncio wakeup pipe from inside a signal-handler frame on the loop's own thread (a direct ``app.exit()`` sets the flag but the loop doesn't necessarily notice; ``call_from_thread`` raises). If no loop is running yet (or it has already torn down), fall back to a direct ``app.exit()`` so the next press still hard-exits. - - Any subsequent press: ``os._exit(130)``. No timing window — every + - Any subsequent press: ``os._exit(130)``. No timing window: every ^C after the first hard-exits, so a wedged loop can never trap the user even if they pace presses slowly. diff --git a/fnd/tui/actions.py b/fnd/tui/actions.py index cb43a668..6ed6b282 100644 --- a/fnd/tui/actions.py +++ b/fnd/tui/actions.py @@ -1,4 +1,4 @@ -"""Action registry — single source of truth for the TUI's behaviour surface. +"""Action registry: single source of truth for the TUI's behaviour surface. Every TUI action is declared here once, with its default keybinding and the ``:command`` name. Footer hints, the help overlay, and the keymap loader all @@ -27,7 +27,7 @@ @dataclass(slots=True, frozen=True) class Action: - """One TUI action — keymap and help-overlay row.""" + """One TUI action: keymap and help-overlay row.""" id: str # snake_case name; matches FNDApp.action_ description: str @@ -42,7 +42,7 @@ class Action: # tree focused), ``"preview"`` (preview pane focused), and the sidebar # panels ``"outline"``, ``"collections"`` and ``"filters"``. contexts: tuple[str, ...] = () - # Priority bindings fire before any focused widget's own handlers — + # Priority bindings fire before any focused widget's own handlers: # required when the chosen key collides with a Textual widget # default (e.g. Input owns ctrl+f as "delete right word"). Override # with care: it removes the widget's behaviour for that key. @@ -407,7 +407,7 @@ def load_keymap(path: Path | None = None) -> Keymap: Unknown action ids in the user file are dropped silently (the validator surfaces them via :func:`validate_keymap`).""" # ``default_key`` may list several keys, comma-separated, so one action can - # bind more than one — needed because a single physical combo reaches the + # bind more than one: needed because a single physical combo reaches the # app under different key names depending on the terminal (e.g. Option+Right # arrives as ``ctrl+right`` under a natural-text-editing remap but as # ``alt+right`` on a Kitty-keyboard-protocol terminal). diff --git a/fnd/tui/app.py b/fnd/tui/app.py index 3de66e09..a88044b2 100644 --- a/fnd/tui/app.py +++ b/fnd/tui/app.py @@ -103,7 +103,7 @@ def _short_label(action_id: str) -> str: - """Footer-hint label for an action — uses Action.footer_label when set, + """Footer-hint label for an action: uses Action.footer_label when set, otherwise falls back to the first word of the description. Localised because this also becomes the Textual ``Binding`` description, which no longer passes through the hint-bar renderer.""" @@ -151,7 +151,7 @@ def _cluster(pairs: tuple[tuple[str, str], ...]) -> Text: out.append_text(Text("…", style="dim")) continue # Both clusters run through localise so a hint table can hold - # ``{alt_key}`` and render ⌥ on macOS, Alt elsewhere — the footer + # ``{alt_key}`` and render ⌥ on macOS, Alt elsewhere: the footer # and the Keybindings page then can't disagree. key = os_labels.localise(key) label = os_labels.localise(label) @@ -329,7 +329,7 @@ class FNDApp(PlainToastApp): stock MarkdownFence sets ``overflow: scroll hidden`` and lets its inner Label size to the longest line, so long lines spill into a horizontal scrollbar. Hiding overflow-x and pinning the Label to - the fence width makes the content reflow to the pane — no + the fence width makes the content reflow to the pane: no horizontal bar to chase. The bottom padding is dropped (the stock ``padding: 1 2`` left a dark backdrop row that read as a thick scrollbar) and the vertical bar stays the app-wide hairline. */ @@ -358,12 +358,12 @@ class FNDApp(PlainToastApp): width: auto; } /* Pane borders dim by default, brighten when the pane (or any - descendant) is focused — lazygit's active-section convention. + descendant) is focused, lazygit's active-section convention. Results expands to fill remaining space; Collections + Filters size to their content so each panel stays exactly as tall as its rows demand. */ #results_column { width: 1fr; height: 1fr; } - /* Cancel Textual's focused-Tree tint — border colour is the focus signal. */ + /* Cancel Textual's focused-Tree tint: border colour is the focus signal. */ #results_column Tree { background-tint: transparent; } @@ -372,20 +372,20 @@ class FNDApp(PlainToastApp): border: round $primary 50%; overflow-x: hidden; /* `scroll`, not `auto`: file rows are elided to the pane's content - width, and that budget is measured before the rows exist — under + width, and that budget is measured before the rows exist, under `auto` the rows then bring the scrollbar back and every label runs a cell long, losing the last character of the extension. */ overflow-y: scroll; } /* Class-toggled focus border (not :focus-within) so it survives a - terminal blur — Textual clears :focus-within when the app loses focus, + terminal blur, Textual clears :focus-within when the app loses focus, which dropped the border on tab-away and reinstated it a beat late on return. ``on_descendant_focus`` drives the class; nothing clears it on blur. Mirrors the preview pane below. */ #results_pane.-focused { border: round $accent; } /* height is driven at runtime by the dynamic allocator (_reflow_sidebar / sidebar_layout.py); the `auto` here is only the pre-reflow default. No - max-height cap — the allocator, not CSS, decides each panel's share. */ + max-height cap: the allocator, not CSS, decides each panel's share. */ #collections_panel_tree { width: 100%; height: auto; border: round $primary 50%; @@ -414,7 +414,7 @@ class FNDApp(PlainToastApp): /* Docked at the top so it floats above the scrolling tree, always in view while a filter is active; hidden otherwise. */ #filters_pane #clear_filters_bar { - /* visibility (not display) so the row is always reserved — the bar + /* visibility (not display) so the row is always reserved: the bar appearing on the first active filter must not shove the tree down. */ dock: top; height: 1; padding: 0 1; visibility: hidden; color: $primary 50%; @@ -453,7 +453,7 @@ class FNDApp(PlainToastApp): the collapsed height when the inner tree has more rows than fit. The results pane intentionally keeps a 1-row content area (height: 3 = top border + 1 row + bottom border) so the - currently-selected hit — which drives the preview pane — stays + currently-selected hit, which drives the preview pane, stays visible. The secondary panels (collections, filters) drop to just the two border rows so their cursor highlight doesn't bleed through and read as "still selected" when the panel is closed. */ @@ -481,8 +481,8 @@ class FNDApp(PlainToastApp): padding: 0 0 0 1; /* `scroll`, not `auto`: the gutter is reserved whether or not there is anything to scroll, so the pane's content width does not depend on - its content HEIGHT. Under `auto` it does — a file short enough to - drop the bar widens the pane by a column — and that width is part of + its content HEIGHT. Under `auto` it does: a file short enough to + drop the bar widens the pane by a column, and that width is part of the capture key, so every warmed capture taken at the old width goes unreachable in a single navigation. Measured on the real index, the width flipped 76 -> 77 mid-session and READY files collapsed 6 -> 1; @@ -491,7 +491,7 @@ class FNDApp(PlainToastApp): rather than by size: changing the gutter re-wraps the document. */ overflow-y: scroll; } - /* Class-toggled focus border — :focus-within would re-style every descendant. */ + /* Class-toggled focus border, :focus-within would re-style every descendant. */ #preview_pane.-focused { border: round $accent; } /* Reading view: drop the border + padding so a full-width terminal selection copies only the text, not the frame. Listed after @@ -516,7 +516,7 @@ class FNDApp(PlainToastApp): bar's chrome, not scrolling. Hidden by COLOUR, not by size. ``scrollbar-size-vertical: 0`` removes - the gutter, which changes the pane's content width — so every chunk in + the gutter, which changes the pane's content width, so every chunk in the document re-wraps, twice per navigation, and the class toggle has to restyle the whole subtree to make that happen. Measured, that was the single widest restyle in a navigation. Painting the bar in the pane's own @@ -541,8 +541,8 @@ class FNDApp(PlainToastApp): .chunk-first { padding: 1 0 0 0; } #placeholder { color: $text-muted; } Tree > .tree--label { padding: 0 1; } - /* Selected-row highlight: only when the panel actually owns focus - — sidebar panels stop showing a stale ``current selection`` band + /* Selected-row highlight: only when the panel actually owns focus, +sidebar panels stop showing a stale ``current selection`` band when the user is typing in the query bar or has focus elsewhere. */ Tree > .tree--cursor { background: transparent; color: $text; } Tree:focus-within > .tree--cursor { background: $accent 40%; text-style: bold; } @@ -555,7 +555,7 @@ class FNDApp(PlainToastApp): # BINDINGS is built from the action registry at import time so footer # hints, help overlay, and runtime keymap all share one source. - BINDINGS = [ # noqa: RUF012 — Textual's App.BINDINGS expects a class-level list literal + BINDINGS = [ # noqa: RUF012, Textual's App.BINDINGS expects a class-level list literal Binding("ctrl+c", "quit", "Quit", show=False), Binding("escape", "escape_back", "Back", show=False), Binding("ctrl+shift+d", "diag_dump_preview", "Dump preview widget tree", show=False), @@ -629,7 +629,7 @@ def __init__( # update-all chain bookkeeping); see fnd/tui/indexer_service.py. # The modal reads this through the app's _indexer_* accessors. self._indexer = IndexerService(self) - # Structured-PDF extras install/uninstall — sibling to the + # Structured-PDF extras install/uninstall: sibling to the # indexer task so the modal can be dismissed (Background) and # reopened against the live task. See # fnd/tui/extras_install_progress.py. @@ -693,17 +693,17 @@ def on_unmount(self) -> None: def compose(self) -> ComposeResult: yield Input(placeholder="Search…", id="query_bar", value=self._initial_query) - # Calm, practical one-liner for malformed queries — collapsed until set. + # Calm, practical one-liner for malformed queries: collapsed until set. yield PlainStatic("", id="query_notice") with Horizontal(): # Left column: results on top, the outline, then Collections and Filters. with Vertical(id="results_column"): yield ResultsTree("Results", id="results_pane") yield OutlineTree("Outline", id="outline_pane") - # Single-widget panels — each carries its summary in + # Single-widget panels: each carries its summary in # ``border_title``, matching the results-pane styling. # Collections tree uses the plain Tree (expanded - # collection rows stay selectable — Enter toggles the + # collection rows stay selectable: Enter toggles the # whole collection in/out of scope). Filters tree uses # the skip-expanded-parent subclass so File-type / # Modified headers behave the same as file rows. @@ -726,7 +726,7 @@ def compose(self) -> ComposeResult: with Vertical(id="preview_column"), MatchAwareScroll(id="preview_pane") as pane: pane.fit_edges = self._fit_preview_edges yield PlainStatic("Type a query and press Enter.", id="placeholder") - # App-level progress strip — one row, full width, hidden via + # App-level progress strip: one row, full width, hidden via # ``visibility: hidden`` so the row occupancy is stable across # show / hide (no preview reflow). Driven by ``ProgressFacility``. yield FNDProgressBar() @@ -756,7 +756,7 @@ def action_toggle_reading_mode(self) -> None: border/padding so the frame isn't copied. Toggle to restore.""" # Hiding the sidebar widens the preview, which re-wraps the content and # shifts the scroll position. Read the current reading position and - # scroll back to it once the reflow lands — the same regardless of how + # scroll back to it once the reflow lands: the same regardless of how # the position was reached (match nav or user scroll), so a reader who # scrolled away keeps their place rather than snapping back to the match. location = self._preview_scroll.locate() @@ -770,14 +770,14 @@ def action_toggle_reading_mode(self) -> None: self._apply_mouse_capture(not self._reading_mode) if self._reading_mode: preview.focus() - # Reading View is pure scroll-nav — there are no result-driven + # Reading View is pure scroll-nav: there are no result-driven # match-jumps here, so the full-document mount (kept for instant # in-file jumps in normal preview) buys nothing and just makes the # widen-reflow and every subsequent scroll repaint a heavier tree. # Drop to the visible window; scroll-driven lazy-mount refills as # the reader scrolls. prune_active_to_window is scroll-compensated # and flash-free, so the on-screen content stays put. On exit we - # leave it windowed — the next match-nav's render_full_doc restores + # leave it windowed: the next match-nav's render_full_doc restores # the full mount when the results-driven workflow actually needs it. self._preview.prune_active_to_window() else: @@ -791,8 +791,8 @@ def action_toggle_reading_mode(self) -> None: # measured at 37 columns, because it drops the border, the left padding # and the bar on top of widening the column. Every capture taken before # the toggle is filed under the old width and can never be served, and - # coverage starts building a second full set against the same row budget - # — so this must invalidate exactly as a terminal resize does. Deferred, + # coverage starts building a second full set against the same row budget, + # so this must invalidate exactly as a terminal resize does. Deferred, # so the width it reads is the one after the reflow rather than before. self.call_after_refresh(self._preview.invalidate_captures_on_resize) self._refresh_preview_match_indicator() @@ -802,7 +802,7 @@ def on_frozen_chunk_view_width_stale(self, event: FrozenChunkView.WidthStale) -> """A frozen chunk is painting strips cut for a width it no longer has. Bubbles up from the view because only the view knows: the pane-level - resize hook runs before the re-layout. The presenter debounces — a drag + resize hook runs before the re-layout. The presenter debounces: a drag produces one of these per column per mounted chunk. """ event.stop() @@ -812,7 +812,7 @@ async def _on_exit_app(self) -> None: """Stop background work before Textual tears the app down. Textual's exit drains the message queue, and the preview's background - capturing holds the event loop for the length of each build — so a quit + capturing holds the event loop for the length of each build, so a quit issued mid-capture waits for work whose result is about to be discarded. """ import contextlib @@ -821,7 +821,7 @@ async def _on_exit_app(self) -> None: self._preview.stop_background_work() # The drainer is a raw `create_task`, so Textual's worker teardown does # not know about it. Left running it can pull one more mount job off the - # queue while the app is already tearing down — the exact class of work + # queue while the app is already tearing down: the exact class of work # the line above exists to stop. drainer = getattr(self._prefetch, "sink_drainer", None) if drainer is not None: @@ -832,12 +832,12 @@ async def _on_exit_app(self) -> None: # job returns normally and the drainer loops back to `q.get()` and # blocks for ever. An unbounded await then never returns and the app # cannot be quit without a kill. Reachable only if - # `PREVIEW_CACHE_MAX_FILES` rises above 1 — a tuning constant, which + # `PREVIEW_CACHE_MAX_FILES` rises above 1: a tuning constant, which # is not a thing to stake "the app can be closed" on. # # CancelledError is named explicitly: it is a BaseException, so it - # lands ON this await and walks straight through `suppress(Exception)` - # — taking the stall-watch stop and `super()._on_exit_app()` with it. + # lands ON this await and walks straight through `suppress(Exception)`, + # taking the stall-watch stop and `super()._on_exit_app()` with it. # `suppress` also swallows an OUTER cancel of the teardown itself, # which is deliberate: finishing the teardown is the point. with contextlib.suppress(asyncio.CancelledError, Exception): @@ -882,7 +882,7 @@ def on_mount(self) -> None: _cache = PdfStructureCache() _sentinel = _cache.root / ".keys-migrated" - # Only when a cache actually exists — never create the dir for a + # Only when a cache actually exists: never create the dir for a # fresh user (a freshly-written entry is already tex-vN anyway). if _cache.root.exists() and not _sentinel.exists(): _migrated, _failed = _cache.promote_current_engine_entries( @@ -905,7 +905,7 @@ def on_mount(self) -> None: try: self._search.searcher = Searcher(index_dir=self._index_dir) except (FileNotFoundError, RuntimeError): - # No index yet — the app still opens so the user can manage + # No index yet: the app still opens so the user can manage # collections, then reindex outside or from the CLI. self._search.searcher = None # Before the panel's first paint, or adopted rows flash "not indexed". Only @@ -915,28 +915,28 @@ def on_mount(self) -> None: tree = self.query_one("#results_pane", Tree) tree.show_root = False tree.guide_depth = 2 - # Results parents (file rows) are not cursor-selectable when expanded — + # Results parents (file rows) are not cursor-selectable when expanded: # the cursor bounces past them onto the first hit. See # ``_skip_expanded_parent``. tree._skip_expanded_parents = True # type: ignore[attr-defined] - # Collections panel — populated from the loaded Config. + # Collections panel: populated from the loaded Config. ctree = self.query_one("#collections_panel_tree", Tree) ctree.show_root = False ctree.guide_depth = 2 # Collections parents stay cursor-selectable so Enter still toggles # the whole collection (`_on_collections_panel_selected`). ctree._skip_expanded_parents = False # type: ignore[attr-defined] - # Enter must not auto-expand — that fires NodeExpanded and + # Enter must not auto-expand: that fires NodeExpanded and # persists state the user didn't ask for. Right still expands # via action_tree_smart_expand. ctree.auto_expand = False self._scope.refresh_collections_panel() - # Filters panel — kind/date selectors that compose into the query. + # Filters panel: kind/date selectors that compose into the query. ftree = self.query_one("#filters_panel_tree", Tree) ftree.show_root = False ftree.guide_depth = 2 # Filters parents (Modified / Created / Tags headers) are no-ops on - # Enter — skip past them when expanded. File-type *category* rows are + # Enter: skip past them when expanded. File-type *category* rows are # exempt: they toggle, so they stay selectable (see ResultsTree). ftree._skip_expanded_parents = True # type: ignore[attr-defined] # Enter must TOGGLE only, never auto-expand the category (left/right do @@ -952,7 +952,7 @@ def on_mount(self) -> None: # Focus the query bar first and let the search take it back: results # no longer exist synchronously after ``run()``, so there is nothing to # branch on here. ``_refresh_results_tree`` focuses the tree itself once - # groups land, and skips that when there are none — the same end state + # groups land, and skips that when there are none: the same end state # the old ``not self._search.groups`` check produced. self.query_one("#query_bar", Input).focus() if self._initial_query: @@ -966,7 +966,7 @@ def on_mount(self) -> None: self._indexer.maybe_resume() # Pre-upgrade cache entries (PDFs textured on an older extractor # version) are surfaced passively in Settings → Indexing & PDF - # Texture, not via a startup popup — re-texturising is a + # Texture, not via a startup popup: re-texturising is a # preview-quality refresh the user opts into, never urgent. # ── Ranking profile ─────────────────────────────────────────── @@ -1071,7 +1071,7 @@ def _previewed_collections(self) -> Content: def _current_match_unlocatable(self) -> bool: """True when the chunk the preview is showing for the current result carries no paintable match. Reads the decoded chunk, not the mounted - widgets, so it is settle-independent — the answer is the same before + widgets, so it is settle-independent: the answer is the same before and after the scroll lands. Gated on the anchor still being *armed*: the notice describes where the @@ -1154,7 +1154,7 @@ def _focus_context(self) -> str: ``"global"`` (when nothing app-relevant is focused, e.g. an overlay).""" # A background mount task can finalise and refresh the footer - # after the app has begun tearing down — at which point the screen + # after the app has begun tearing down: at which point the screen # stack is empty and ``self.focused`` (→ ``self.screen``) raises # ScreenStackError. No screen means no focus context to resolve. if not self.screen_stack: @@ -1181,7 +1181,7 @@ def _focus_context(self) -> str: node = getattr(node, "parent", None) return "global" - # Anchor cluster — keys that never move regardless of focus. Muscle + # Anchor cluster: keys that never move regardless of focus. Muscle # memory: search, settings menu, keybindings, quit. Sourced as a # tuple-of-tuples so the order is explicit. _FOOTER_ANCHORS: tuple[tuple[str, str], tuple[str, str], tuple[str, str], tuple[str, str]] = ( @@ -1191,7 +1191,7 @@ def _focus_context(self) -> str: ("q", "Quit"), ) - # Contextual cluster, keyed by focus context — what's *relevant* in + # Contextual cluster, keyed by focus context: what's *relevant* in # the pane that has focus right now. Empty list = no contextual hints # (anchor cluster alone). _FOOTER_CONTEXTUAL: ClassVar[dict[str, tuple[tuple[str, str], ...]]] = { @@ -1249,11 +1249,11 @@ def _refresh_footer_hints(self) -> None: overlay_hint if overlay_hint is not None else self._FOOTER_CONTEXTUAL.get(ctx, ()) ) # Reading view focuses the preview, so the per-pane table would hide - # the toggle key — surface the exit hint while it's active. + # the toggle key: surface the exit hint while it's active. if self._reading_mode and overlay_hint is None: contextual = (("z", "Reading View"), ("j/k", "Scroll")) - # Match navigation key hint — shown in the keybinding area (like every + # Match navigation key hint: shown in the keybinding area (like every # other key) whenever the current preview has matches. The k/N COUNT # itself lives on the preview's border, not here. Placed first so it # isn't clipped off the crowded footer line. Suppressed in Reading View, @@ -1283,7 +1283,7 @@ def _refresh_footer_hints(self) -> None: self.query_one("#footer_hints", Static).update(render_hint_bar(anchors, contextual)) # Maps a ``_focus_context`` result to the pane id that wears the accent - # border. ``query``/``global`` map to nothing — no pane is accented. + # border. ``query``/``global`` map to nothing: no pane is accented. _FOCUS_BORDER_PANES: ClassVar[dict[str, str]] = { "results": "#results_pane", "outline": "#outline_pane", @@ -1311,8 +1311,8 @@ def _sync_focus_border(self) -> None: terminal blur: Textual clears focus on ``AppBlur`` and only restores it on the next keypress, which made every pane's border vanish on tab-away and reappear a beat late. We only touch the class on a genuine focus - move; ``AppBlur`` fires a descendant *blur*, not focus, so the class — - and the border — stays put. ``set_class(update=False)`` + a per-pane + move; ``AppBlur`` fires a descendant *blur*, not focus, so the class, + and the border, stays put. ``set_class(update=False)`` + a per-pane ``stylesheet.apply`` keeps this off the subtree style-walk path.""" # No screen (teardown mid-quit) → self.query_one would raise # ScreenStackError; bail like _focus_context does rather than mask it. @@ -1405,7 +1405,7 @@ def _on_results_selected(self, ev: Tree.NodeSelected[Any]) -> None: # Enter loads the highlighted row even after an Option-scan (which # suppressed the per-row load): end scan mode and load it now, so a user # can browse with Option then press Enter to mount exactly what they - # landed on. (NodeSelected stays unbound from opening the file — see the + # landed on. (NodeSelected stays unbound from opening the file: see the # note above.) Cancel any armed cooldown timer first so the load fires # immediately (leading edge) rather than waiting out the debounce window. self._preview._scan_move = False @@ -1524,7 +1524,7 @@ def start_indexer( force_fresh: bool = False, _bump_seq: bool = True, ) -> bool: - """Spawn the async indexer task for ``collection`` — see + """Spawn the async indexer task for ``collection``: see :meth:`IndexerService.start`. Kept as a real app method so chain continuations and test patches address the app.""" return self._indexer.start( @@ -1668,7 +1668,7 @@ async def _offer_full_warm(self, group: FileGroup, hit: Hit) -> None: from fnd.tui.preview import tuning # The estimate decodes an uncached file and the app stays live for - # all of it, so the query can change before the answer arrives — and + # all of it, so the query can change before the answer arrives, and # a request made after that reset outlives it. asked_under = self._search.query_signature() count, chars = await self._preview.full_warm_estimate(group.parent_id) @@ -1823,7 +1823,7 @@ def _reflow_sidebar(self, *, immediate: bool = False) -> None: """Recompute the sidebar panels' heights from live content demand. Coalesced to one pass per frame, so it's safe (and cheap) to call from - any change that alters what the panels want to show — a new result set, + any change that alters what the panels want to show: a new result set, a rebuilt filter/collection list, a section expanding or collapsing, a panel collapsing to its header, or a terminal resize. That breadth is the point: the split stays responsive to every one of those the way a @@ -1832,8 +1832,8 @@ def _reflow_sidebar(self, *, immediate: bool = False) -> None: ``immediate`` runs the reallocation NOW, in the same message handler as the change, rather than after the next refresh. Use it when a panel toggles its ``collapsed`` class: deferring the reflow leaves the panel - drawn at its old (taller) inline height for one frame — visible as a - container with side walls but no bottom edge — before it snaps to the + drawn at its old (taller) inline height for one frame, visible as a + container with side walls but no bottom edge, before it snaps to the header box. Doing it inline clears the stale height in the same frame, so the collapse/expand is a single clean step. Content-demand reflows (new results, section folds) still defer, since their new @@ -1955,7 +1955,7 @@ def action_tree_smart_collapse(self) -> None: parent = node.parent # tree.root is a hidden virtual root; treat it as "no parent". if parent is None or parent is tree.root: - # Top of the tree, already collapsed — collapse the + # Top of the tree, already collapsed: collapse the # entire panel to its header strip. self._collapse_panel_frame(frame) return @@ -2020,7 +2020,7 @@ def action_tree_smart_expand(self) -> None: self._move_cursor_to_first_child(tree, node) def action_tree_expand_all_children(self) -> None: - """Expand the focused node and reveal its whole subtree — the node plus + """Expand the focused node and reveal its whole subtree: the node plus every descendant. Works in every focusable sidebar tree (results, collections, filters).""" tree = self._focused_tree() @@ -2079,7 +2079,7 @@ def action_scope_toggle_batch(self) -> None: def action_tree_collapse_all_children(self) -> None: """Collapse the focused node's *children* (each child and its subtree), - leaving the node itself open. Only the descendants fold away — the row + leaving the node itself open. Only the descendants fold away: the row you're on stays put, so this tidies a deep subtree without backing out of it (use the plain Left arrow to collapse the node itself).""" tree = self._focused_tree() @@ -2100,7 +2100,7 @@ def _move_cursor_to_first_child(tree: Tree[Any], node: TreeNode[Any]) -> None: the freshly-revealed children still carry a stale ``_line`` of -1. Going through ``move_cursor(child)`` then sets ``cursor_line`` to -1, which the skip-expanded-parents validator clamps to 0 and walks into - a jump to the first file — visible when the rebuild is delayed (e.g. + a jump to the first file: visible when the rebuild is delayed (e.g. the preview pane is mid full-mount). ``move_cursor_to_line`` forces the rebuild (it reads ``_tree_lines``) and the first child always sits one row below its parent, so the move is correct synchronously. @@ -2117,13 +2117,13 @@ def _on_results_geometry_changed(self, _ev: ResultsTree.GeometryChanged) -> None @on(ResultsTree.ReopenRequested) def _on_results_reopen_requested(self, ev: ResultsTree.ReopenRequested) -> None: """A click on a collapsed-to-header panel reopens it and expands the - clicked result — the ``Right``-arrow reopen (``action_tree_smart_expand``) + clicked result: the ``Right``-arrow reopen (``action_tree_smart_expand``) for the mouse, surfacing the specific row the user pointed at rather than toggling a node hidden behind the collapsed height.""" tree = ev.tree - # The collapse state lives on the panel frame — the tree itself for the + # The collapse state lives on the panel frame: the tree itself for the # results/collections panels, but the #filters_pane wrapper for the - # filters tree — so resolve it the same way the keyboard reopen does. + # filters tree, so resolve it the same way the keyboard reopen does. if not self._reopen_panel_frame(tree): return node = ev.node @@ -2163,7 +2163,7 @@ def _on_filters_pane_click(self, event: events.Click) -> None: @property def _effective_match_spec(self) -> MatchSpec: """The MatchSpec the renderers should consult. Falls back to an - empty spec when the user has toggled highlights off — the + empty spec when the user has toggled highlights off: the preview pane then renders the plain document with no yellow / orange overlays and no scrollbar match markers.""" return self._search.match_spec if self._search.highlights_enabled else MatchSpec() @@ -2174,7 +2174,7 @@ def _effective_evidence_spec(self) -> MatchSpec: match (:mod:`fnd.tui.match_evidence`). Same as :meth:`_effective_match_spec` but without AUTO-fuzzy. Painting - is deliberately generous — a query for "test" highlights "best" and + is deliberately generous: a query for "test" highlights "best" and "rest" at edit distance 1 so a typo still lands somewhere. Those are not evidence: judging visibility by them reported "your match is here" over a paragraph containing nothing the user typed, which is precisely the @@ -2192,7 +2192,7 @@ def action_search_as_typed(self) -> None: self._search.search_as_typed() def action_nav_next_match(self) -> None: - # Reading View is pure scroll-nav — no result-driven match jumps there. + # Reading View is pure scroll-nav: no result-driven match jumps there. if self._reading_mode: return self._match_nav.next() @@ -2288,7 +2288,7 @@ def _on_filter_branch_collapsed(self, ev: Tree.NodeCollapsed[dict[str, object]]) self._scope.on_filter_branch_collapsed(ev) # A section (branch) folding open or closed in any sidebar tree changes how - # many rows that panel wants — reflow so the shares track it live. + # many rows that panel wants: reflow so the shares track it live. _SIDEBAR_TREE_IDS: ClassVar[frozenset[str]] = frozenset( {"results_pane", "collections_panel_tree", "filters_panel_tree"} ) @@ -2339,7 +2339,7 @@ def action_escape_back(self) -> None: handler (back one level / dismiss form). This action only fires when the main app screen is on top. """ - # Step 1 — dismiss in-app overlay if present. + # Step 1: dismiss in-app overlay if present. dismissed = False for selector in ("#explain_overlay", "#multi_panel"): for w in self.query(selector): @@ -2348,13 +2348,13 @@ def action_escape_back(self) -> None: if dismissed: return - # Step 1.5 — in reading view, Esc returns to the normal app + # Step 1.5: in reading view, Esc returns to the normal app # (restore the sidebar) rather than cascading focus. if self._reading_mode: self.action_toggle_reading_mode() return - # Step 2 — cascade focus toward the results pane. + # Step 2: cascade focus toward the results pane. ctx = self._focus_context() if ctx in ("query", "preview", "outline", "filters", "collections"): import contextlib @@ -2368,7 +2368,7 @@ def action_show_help(self) -> None: ``?`` always pushes Keybindings ON TOP of whatever screen the user is currently on so ``Esc`` from Keybindings returns them to exactly that screen (a sub-menu, the source-edit form, the - Open-with modal — wherever they invoked ``?``). The previous + Open-with modal: wherever they invoked ``?``). The previous behaviour popped the entire settings stack first, which dropped users back to the main app and made ``?`` useless as a "what can I do here?" affordance. @@ -2406,7 +2406,7 @@ def _keybindings_context_hint(self) -> str | None: # If we're inside the Settings stack, the relevant section # depends on which screen the user is on. SourceFormScreen # has its own static section; SettingsScreen catches the rest - # (Preferences / Collections / Keybindings sub-screens — the + # (Preferences / Collections / Keybindings sub-screens: the # SettingsList widget bindings apply across all of them). current = self.screen if isinstance(current, FilterBrowserScreen): @@ -2416,7 +2416,7 @@ def _keybindings_context_hint(self) -> str | None: if isinstance(current, SettingsScreen): return "Settings menu" - # Main app — pick by the focused pane. + # Main app: pick by the focused pane. focused = self.focused widget = focused while widget is not None: @@ -2442,7 +2442,7 @@ def action_show_explain_overlay(self) -> None: Mirrors the help-overlay style: a Vertical with an embedded Markdown widget rendering the SearchTrace as a fenced ``json`` block. The trace covers the entire search call, not just the - focused hit — focused-hit details are visible in the trace's + focused hit: focused-hit details are visible in the trace's per-hit ``contributions`` list. """ existing = self.query("#explain_overlay") diff --git a/fnd/tui/ax_permission_screen.py b/fnd/tui/ax_permission_screen.py index d18c32ee..8b61b259 100644 --- a/fnd/tui/ax_permission_screen.py +++ b/fnd/tui/ax_permission_screen.py @@ -28,7 +28,7 @@ from fnd.tui.ui_text import PlainStatic -# macOS Settings URL — opens straight to the Accessibility privacy pane +# macOS Settings URL: opens straight to the Accessibility privacy pane # (Sequoia and earlier). Stable since macOS 13. _SETTINGS_URL = "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility" @@ -84,7 +84,7 @@ class AccessibilityPermissionScreen(ModalScreen[None]): def __init__(self, *, action_desc: str = "open the file at its match position") -> None: super().__init__() - # What the user was trying to do — flows into the modal copy so the + # What the user was trying to do: flows into the modal copy so the # message reads naturally regardless of which AX-gated path tripped. self._action_desc = action_desc diff --git a/fnd/tui/clipboard.py b/fnd/tui/clipboard.py index 6e1c9e88..0dc660b7 100644 --- a/fnd/tui/clipboard.py +++ b/fnd/tui/clipboard.py @@ -1,6 +1,6 @@ """System-clipboard writes, isolated behind one seam. -Each OS clipboard tool is described as an argv (``clipboard_argv``) — a +Each OS clipboard tool is described as an argv (``clipboard_argv``): a pure resolver that unit-tests per platform. ``copy_text`` takes both the resolver and the process runner as injectable dependencies, so callers can exercise the copy path without spawning anything. diff --git a/fnd/tui/config_recovery_screen.py b/fnd/tui/config_recovery_screen.py index 6cdca69b..ebcc2648 100644 --- a/fnd/tui/config_recovery_screen.py +++ b/fnd/tui/config_recovery_screen.py @@ -1,6 +1,6 @@ """Config recovery flow. -Shown when ``fnd.config.load()`` raises at TUI startup — i.e. when the +Shown when ``fnd.config.load()`` raises at TUI startup: i.e. when the on-disk ``config.toml`` is unparseable or fails Pydantic validation. The user is offered three deliberate actions (open in editor, reset to defaults with a timestamped backup, exit) instead of seeing a crash. diff --git a/fnd/tui/extras_install_progress.py b/fnd/tui/extras_install_progress.py index f9130efc..f2e32002 100644 --- a/fnd/tui/extras_install_progress.py +++ b/fnd/tui/extras_install_progress.py @@ -7,7 +7,7 @@ screen, so dismissing (Background) keeps work going. - Cancel sends SIGTERM to the active subprocess; subsequent commands in the chain don't run. -- Progress is derived from stderr / stdout lines emitted by uv — +- Progress is derived from stderr / stdout lines emitted by uv: parsed loosely (uv prints lines like ``Resolved 23 packages``, ``Prepared 5 packages``, ``Installed 5 packages`` we surface for the user) and falls back to the most recent line as a status string. @@ -57,7 +57,7 @@ class ProgressEvent: class ExtrasInstallProgressScreen(ModalScreen[None]): """Modal that drains the events queue on ``FNDApp._extras_events``. - Reopening the screen reattaches to the live task — the task + Reopening the screen reattaches to the live task: the task survives modal dismissal.""" BINDINGS = [ # noqa: RUF012 @@ -69,7 +69,7 @@ class ExtrasInstallProgressScreen(ModalScreen[None]): # Visual consistency with the rest of fnd's UI: brackets reserved # for actions in the settings rows render exactly like the # ``[ Run ]`` affordance. The Textual ``Button`` widget adds - # ``▔``/``▁`` decorative chrome that's wider than its label — + # ``▔``/``▁`` decorative chrome that's wider than its label: # never matched fnd's settings-row buttons. Use plain Statics. # # All three button states (Background / Cancel / Close) are @@ -230,7 +230,7 @@ async def on_option_list_option_selected(self, ev: Any) -> None: self.dismiss(None) async def action_background_or_close(self) -> None: - """Esc / b behaves as Close once the operation has finished — + """Esc / b behaves as Close once the operation has finished: the modal has no work to background at that point.""" if self._is_terminal: self.dismiss(None) @@ -369,7 +369,7 @@ def start_extras_install( ) -> bool: """Spawn the install/uninstall task and push the progress modal. - Idempotent — refuses to start a second run while one is in flight, + Idempotent: refuses to start a second run while one is in flight, re-attaches the modal to the running task instead. """ existing = getattr(app, "_extras_task", None) diff --git a/fnd/tui/first_reindex_warning.py b/fnd/tui/first_reindex_warning.py index 4f7be124..dc4bb1ea 100644 --- a/fnd/tui/first_reindex_warning.py +++ b/fnd/tui/first_reindex_warning.py @@ -6,7 +6,7 @@ file is written to ``$XDG_DATA_HOME/fnd/`` so we don't nag again. Estimation: count PDFs in the configured collection's sources × an -average ~30s/PDF heuristic. Rough on purpose — the user just needs an +average ~30s/PDF heuristic. Rough on purpose: the user just needs an order-of-magnitude warning before walking away from a multi-hour run. """ diff --git a/fnd/tui/flat_pdf_scan.py b/fnd/tui/flat_pdf_scan.py index d702f5f1..83f08630 100644 --- a/fnd/tui/flat_pdf_scan.py +++ b/fnd/tui/flat_pdf_scan.py @@ -4,7 +4,7 @@ source on disk and diffs it against the tantivy index. On a real corpus that costs seconds (measured 2-3 s; 20-30 s mid-rebuild, when the scan contends with the index the runner is rewriting). Run on the UI thread -it freezes the whole TUI until it returns — the recurring "portal takes +it freezes the whole TUI until it returns: the recurring "portal takes forever to open" stall. Every UI consumer reads through this module instead of calling the scan @@ -30,7 +30,7 @@ if TYPE_CHECKING: from fnd.tui.app import FNDApp -# (collection, path, reason, recorded_at) — the shape returned by +# (collection, path, reason, recorded_at): the shape returned by # settings_screen._flat_pdfs_with_reasons. Row = tuple[str, str, str, str | None] @@ -50,7 +50,7 @@ def cached_rows(collection: str | None = None) -> list[Row] | None: """Last computed rows for ``collection``, or None if never computed. - Instant — safe to call from the render path / event loop.""" + Instant: safe to call from the render path / event loop.""" with _LOCK: entry = _CACHE.get(collection) return entry[0] if entry is not None else None @@ -147,7 +147,7 @@ def _deliver(app: FNDApp | Any, cb: Callable[[list[Row]], None], rows: list[Row] """Marshal one callback onto the UI thread. Under test runs the worker can finish before the loop is up; close any returned coroutine so it isn't reported as never-awaited. App teardown / screen - reshuffle is benign — the cached value is read on the next render.""" + reshuffle is benign: the cached value is read on the next render.""" import asyncio with contextlib.suppress(Exception): diff --git a/fnd/tui/indexer_modal.py b/fnd/tui/indexer_modal.py index d77c9cc8..93e44031 100644 --- a/fnd/tui/indexer_modal.py +++ b/fnd/tui/indexer_modal.py @@ -82,7 +82,7 @@ def invalidate_todo_count_cache() -> None: """Drop every cached flat-PDF scan so the next refresh recomputes. Called when a chain finishes: a run can resolve files across multiple collections, so the unscoped count AND each per-collection scope (read - by StillFlatDrillIn) are stale — clear all of them.""" + by StillFlatDrillIn) are stale, clear all of them.""" from fnd.tui import flat_pdf_scan flat_pdf_scan.invalidate_all() @@ -118,7 +118,7 @@ def _format_current_line( A cloud fetch blocks the worker that asked for it, so no progress event can arrive while it runs. When one is in flight it takes the line and - names the provider, the file, and how long — that is the difference + names the provider, the file, and how long: that is the difference between a legitimate download and an app the user reads as frozen. """ if wait is not None: @@ -301,7 +301,7 @@ def _title_text(self) -> str: return f"Update index › {self._collection}" def _refresh_title(self) -> None: - """Re-render the box title — used when the chain advances to + """Re-render the box title: used when the chain advances to the next collection. Reads the new collection name + chain index from the FNDApp.""" app = self._fnd_app() @@ -555,13 +555,13 @@ def _todo_scope(self) -> str | None: """Collection scope for the flat-PDF badge + drill-in: the active collection for a single Update, all collections (None) mid-chain (the chain cycles through several). Keeps the badge count and the - drill-in it opens consistent — same scope for both.""" + drill-in it opens consistent: same scope for both.""" return self._collection if self._chain_total <= 1 else None def _schedule_todo_refresh(self) -> None: """Recompute the flat-PDF count off the event loop and re-sync the action options when it lands. Bounded to mount + per- - collection completion — never the 1Hz tick — so a long rebuild + collection completion, never the 1Hz tick, so a long rebuild doesn't spawn a scan every few seconds that contends with the indexer it's measuring.""" from fnd.tui import flat_pdf_scan @@ -841,7 +841,7 @@ async def action_skip_cloud(self) -> None: The compromise for a corpus that is mostly evicted: keep the run going at local-disk speed and list what was left behind, without - having to cancel. Run-scoped — the next Update fetches again. + having to cancel. Run-scoped: the next Update fetches again. """ app = self._fnd_app() skip = app._indexer.skip_cloud @@ -1151,8 +1151,8 @@ async def drive_indexer( ) app._indexer.chain_history = history # type: ignore[attr-defined] - # A superseded run — a newer explicit run bumped the generation while - # this one was winding down (e.g. cancel-then-Rebuild-all) — must not + # A superseded run: a newer explicit run bumped the generation while + # this one was winding down (e.g. cancel-then-Rebuild-all): must not # touch the shared chain state, or its late teardown would clobber the # queue the newer run just set up. The newer run owns the chain now. if getattr(app._indexer, "run_seq", run_seq) != run_seq: @@ -1236,7 +1236,7 @@ class _SuppressFullQueueLoss: Unbounded queue is fine for thousands of events; if for some reason a backlog appears (UI completely frozen), losing intermediate - file_complete events is acceptable — the next one carries fresh + file_complete events is acceptable: the next one carries fresh totals. """ diff --git a/fnd/tui/indexer_service.py b/fnd/tui/indexer_service.py index a8d85422..1c40fc60 100644 --- a/fnd/tui/indexer_service.py +++ b/fnd/tui/indexer_service.py @@ -41,7 +41,7 @@ class IndexerService: def __init__(self, app: FNDApp) -> None: self._app = app - # Async indexer state — see fnd/tui/indexer_modal.py. None when + # Async indexer state: see fnd/tui/indexer_modal.py. None when # no indexer is running; populated for the lifetime of one # `fnd collection reindex` invoked from the TUI / palette. self.task: asyncio.Task[None] | None = None @@ -88,7 +88,7 @@ def __init__(self, app: FNDApp) -> None: # with durable cache reuse. self.skip_unchanged: bool = True self.force_fresh: bool = False - # rebuild=True drops each collection's chunks before re-indexing — + # rebuild=True drops each collection's chunks before re-indexing: # the "Rebuild" actions pair it with force_fresh for a true redo. self.rebuild: bool = False # Per-collection final snapshots captured as each chain step @@ -122,7 +122,7 @@ def start( A new explicit request (``_bump_seq=True``) bumps the run generation. If a run is already in flight, it is cancelled and - this one is started only once the old has fully torn down — so the + this one is started only once the old has fully torn down, so the old teardown can never race the new setup (and can't clobber this request's chain queue). Chain continuations pass ``_bump_seq=False`` to inherit the current generation. @@ -139,7 +139,7 @@ def start( # Either a chain continuation racing a busy task (defensive) # or an actively-running run the user re-opened to watch: # don't start a second, don't disturb its generation. Show - # the running modal so "view progress" still works — with the + # the running modal so "view progress" still works, with the # in-flight chain context so a mid-chain re-open keeps its # "(N of M)" title instead of dropping to a single-run one. if open_modal and _bump_seq: @@ -178,7 +178,7 @@ def start( ) return False # In flight but already cancelling (cancel-then-start-again): - # serialise — bump the generation so the dying run's teardown + # serialise: bump the generation so the dying run's teardown # knows it's superseded and won't clobber this request's chain, # then start fresh once it has fully torn down. self.run_seq += 1 @@ -277,7 +277,7 @@ async def _await_then_start() -> None: ) ) # Put the run on the app-level line. The modal, when it is open, sits - # on its own screen and hides this — which is right: it shows strictly + # on its own screen and hides this, which is right: it shows strictly # more. What this covers is the background run (auto-resume on launch, # or a modal the user dismissed with "Background"), which until now # reported nothing at all after its opening toast. @@ -314,7 +314,7 @@ def reindex_with_warning( indexer. Otherwise start the indexer directly. ``rebuild=True`` is for callers that need a fresh build after a - config change (collection rename, source delete, etc.) — it + config change (collection rename, source delete, etc.): it drops the collection's existing chunks before re-indexing. This is the one user-initiated reindex entry point, so it owns the @@ -346,7 +346,7 @@ def reindex_with_warning( col_cfg = cfg.collections[collection] # Establish the chain queue for this request before start() reads - # it — but only when start() will actually act on it. If a run is + # it, but only when start() will actually act on it. If a run is # already in flight and NOT cancelling, start() rejects this request # (busy modal); overwriting here would wipe the live chain's queue # and strand its remaining collections. When there's no run, or the @@ -405,7 +405,7 @@ def _after_warning(confirmed: bool | None) -> None: ) def maybe_resume(self) -> None: - """Auto-resume an interrupted background index on launch — only when + """Auto-resume an interrupted background index on launch: only when the user has opted in (defaults.indexer_auto_resume, off by default) AND a saved state is genuinely resumable. Indexing is heavy, so it must never start unbidden; see index_runner.is_state_resumable for the @@ -413,7 +413,7 @@ def maybe_resume(self) -> None: Every collection's state file is considered, most recently touched first, and the rest of the resumable ones are queued behind it as a - chain — an interrupted Update-all should pick up where it stopped, + chain: an interrupted Update-all should pick up where it stopped, not just its first collection. States that will never be resumable (their collection is gone, or they're older than the resume window) are deleted rather than left to accumulate forever.""" @@ -436,7 +436,7 @@ def maybe_resume(self) -> None: resumable.append(state) elif state.collection not in known or state.files_completed >= state.total_files: # Dead weight: a collection that no longer exists, or a run - # that actually finished. Age alone isn't enough to delete — + # that actually finished. Age alone isn't enough to delete: # a stale-but-valid state is still the record of where a run # got to, and a manual Update reads it. with contextlib.suppress(Exception): @@ -558,7 +558,7 @@ def on_reindex_complete(self) -> None: self._app._search.searcher = Searcher(index_dir=self._app._index_dir) except (FileNotFoundError, RuntimeError, ValueError): self._app._search.searcher = None - # A rebuild may have added a new file-type kind — drop the present-kinds + # A rebuild may have added a new file-type kind: drop the present-kinds # cache so the file-type filter recomputes on the next panel refresh. self._app._scope.invalidate_present_kinds_cache() # And take that refresh now. The facets are index-derived, so without diff --git a/fnd/tui/lazy_trailing.py b/fnd/tui/lazy_trailing.py index 02faaf4d..a669a2a6 100644 --- a/fnd/tui/lazy_trailing.py +++ b/fnd/tui/lazy_trailing.py @@ -2,7 +2,7 @@ A row's ``value_getter`` runs synchronously inside the render path. When that getter walks the filesystem (cache size, extra disk usage, etc.) -the first render blocks until the walk finishes — the user sees an +the first render blocks until the walk finishes: the user sees an empty trailing column for ~200 ms. Solution: rows that touch the filesystem wrap their compute callable @@ -79,7 +79,7 @@ def _worker() -> None: # work back onto the UI thread. Under test runs the worker can # complete before the event loop is fully up; close the returned # coroutine in that case to silence the "never awaited" warning. - # App may be shutting down or the screen stack reshuffled — safe + # App may be shutting down or the screen stack reshuffled: safe # to ignore; next render will pick up the cached value. import asyncio diff --git a/fnd/tui/line_buffer.py b/fnd/tui/line_buffer.py index 69605100..2abfeed1 100644 --- a/fnd/tui/line_buffer.py +++ b/fnd/tui/line_buffer.py @@ -10,7 +10,7 @@ is a list of pre-rendered :class:`rich.text.Text` objects (one per line), converted to :class:`textual.strip.Strip` once at build time and stored. ``render_line`` returns the cached strip for the current -viewport row — Textual's line API only paints the visible viewport, +viewport row: Textual's line API only paints the visible viewport, so the rendering cost is bounded by the terminal height (~50 rows), not the document size. @@ -22,7 +22,7 @@ line-precise positions so they're accurate for files with mixed chunk sizes (the old chunk-uniform mapping was off for any chunk bigger or smaller than average). -* The widget is a thin ScrollView subclass — selection across lines +* The widget is a thin ScrollView subclass: selection across lines becomes a single bounding-box operation (``ALLOW_SELECT = True``) so multi-line copy starts working too. * Building a FileView is independent of mounting the widget. The @@ -88,7 +88,7 @@ class FileView: # top" when the sidebar resolves a section click. first_hit_line_in_chunk: dict[int, int] = field(default_factory=dict) # All line indices that contain at least one match. Drives the - # scrollbar marker map directly — every entry is a line position, + # scrollbar marker map directly: every entry is a line position, # so marker accuracy is bounded by terminal-cell resolution, not # chunk granularity. match_lines: set[int] = field(default_factory=set) @@ -105,7 +105,7 @@ def widest_line(self) -> int: # Match-spans are now styled tuples: ``(byte_start, byte_end, style)``. -# The previous 2-tuple form (start, end) is still accepted — a missing +# The previous 2-tuple form (start, end) is still accepted: a missing # style is taken to mean ``"bold"`` so legacy tests continue to pass. MatchSpan = tuple[int, int] | tuple[int, int, str] @@ -140,7 +140,7 @@ def build_file_view( (e.g. ``"black on #ffd866"``). 2-tuples default to bold so existing callers / tests don't need to change. - Match highlights are **word-level only** — the lines themselves + Match highlights are **word-level only**: the lines themselves are not tinted. The widget's row-level overlays remain available via component classes for the focused-chunk band, but a row containing a match does NOT get a full-row background by default. @@ -149,7 +149,7 @@ def build_file_view( chunks so the user can see where chunks split. The gap row belongs to the *following* chunk for offset-mapping purposes. - Pure function — no widget mounting, no Textual side-effects. + Pure function: no widget mounting, no Textual side-effects. """ fv = FileView() for nth, (chunk_id, chunk_text, match_spans) in enumerate(chunks): @@ -163,7 +163,7 @@ def build_file_view( cursor += len(raw_line) + 1 # +1 for the dropped \n chunk_first_match: int | None = None # first FULL (qualifying) match line chunk_first_dim: int | None = None # fallback: a dim-only proximity line - # Per-line list of (local_start, local_end, style) — preserves + # Per-line list of (local_start, local_end, style): preserves # styles per span so each matched word can render with its own # colour (e.g. fuzzy matches differ from exact matches). local_styled_offsets: list[list[tuple[int, int, str]]] = [[] for _ in line_offsets] @@ -255,17 +255,17 @@ class LineBufferPreview(StripDocumentView): Public surface (the only methods the host app should call): - * :meth:`set_file_view` — install a fresh FileView. Recomputes the + * :meth:`set_file_view`, install a fresh FileView. Recomputes the virtual size and the cached Strips. Cheap to call when the query changes, since the cost is O(lines) and there's no DOM churn. - * :meth:`scroll_to_line` — jump to a specific line index, with an + * :meth:`scroll_to_line`, jump to a specific line index, with an optional ``center=True`` to put the line mid-viewport rather than at the top. - * :meth:`scroll_to_chunk` — convenience: scroll to a chunk by id, + * :meth:`scroll_to_chunk`, convenience: scroll to a chunk by id, optionally to the chunk's first matched line. - * :meth:`set_focused_chunk` — paint a stronger accent band over + * :meth:`set_focused_chunk`, paint a stronger accent band over the focused chunk's lines. Pass ``None`` to clear. - * :attr:`match_lines` — the sorted set of line indices the + * :attr:`match_lines`; the sorted set of line indices the scrollbar renderer maps for feature 6 accuracy. """ @@ -327,7 +327,7 @@ def _rebuild_for_width(self, width: int) -> bool: return False # Pass the width through. `_rebuild_strips` otherwise re-reads # `self.size`, which on the `on_resize` path is not guaranteed to hold - # the width the event just reported — so the guard would compare against + # the width the event just reported, so the guard would compare against # one value and the wrap be cut at another. self._rebuild_strips(width=width) return True @@ -351,7 +351,7 @@ def first_match_address_of_chunk(self, chunk_id: int) -> int | None: return self._fv.first_hit_line_in_chunk.get(chunk_id) def top_address(self) -> int | None: - """The logical (pre-wrap) line at the viewport top — exact across a + """The logical (pre-wrap) line at the viewport top: exact across a width reflow, which a visual row would not be.""" if not self._visual_to_logical: return None @@ -464,8 +464,8 @@ def clear(self) -> None: def set_wrap(self, enabled: bool) -> None: """Toggle viewport-width line wrapping. Rebuilds cached Strips when the mode changes so the next paint reflects the new layout - immediately (the alternative — wrapping lazily inside - ``render_line`` — would re-wrap on every keystroke).""" + immediately (the alternative: wrapping lazily inside + ``render_line``, would re-wrap on every keystroke).""" if self._wrap == enabled: return self._wrap = enabled @@ -581,7 +581,7 @@ def _render_lines( ``on_progress`` is called with the number of lines walked so far, every :data:`_PROGRESS_EVERY` lines. This is the only real unit of work the - flat path exposes — everything else about it is one opaque call — so + flat path exposes, everything else about it is one opaque call, so the progress line reads it rather than estimating a duration it cannot predict. Off by default and free when unset. """ diff --git a/fnd/tui/match_evidence.py b/fnd/tui/match_evidence.py index 7722063c..87a867ac 100644 --- a/fnd/tui/match_evidence.py +++ b/fnd/tui/match_evidence.py @@ -6,24 +6,24 @@ a genuine engine match with nothing to highlight where the user lands. This module is the one place that answers "what text will the user read, and is -the match visible in it". Every surface that claims a match — the row marker, -the preview's landing signal, the scrollbar markers — reads from here, so they +the match visible in it". Every surface that claims a match (the row marker, +the preview's landing signal, the scrollbar markers) reads from here, so they cannot drift apart. **It reports; it must never filter.** Nothing here may drop, hide or reorder a result. The *engine's* match is what makes a result a result; if the highlighter cannot paint that match, it is a defect to surface, not a reason to withhold the row. Filtering on paintability would make the highlighter the gate, and a bug in -it would silently subtract matches the user never learns they missed — the +it would silently subtract matches the user never learns they missed: the failure mode this module exists to make loud instead. Some search/render divergences are deliberate and stay: * markdown ``F_BODY`` is the raw inline source, so link URLs and inline HTML are - searchable but never rendered — wanted, people search for domains; + searchable but never rendered: wanted, people search for domains; * ODF slide and sheet names are synthesised labels, not document text. -An *accidental* divergence is an extractor bug, and belongs there — see +An *accidental* divergence is an extractor bug, and belongs there: see :class:`fnd.extract.heading_fold.HeadingFolder` (which folds a heading into every representation or none) and ``FlatFallbackTier`` (which backfills prose the structured parser dropped). Repairing one here, at render time, would pay @@ -63,8 +63,8 @@ def evidence_spec_for_pass(pass_index: int, *, strict: MatchSpec, painting: Matc def rendered_text(body: PreviewBody) -> str: """The text the preview will actually paint for this chunk. - Routes on :func:`fnd.tui.preview_dispatcher.uses_markdown_renderer` — the - same predicate the mount loop uses — so this can't disagree with what gets + Routes on :func:`fnd.tui.preview_dispatcher.uses_markdown_renderer` (the + same predicate the mount loop uses) so this can't disagree with what gets mounted. """ return body.body_md if uses_markdown_renderer(body) else body.body_text diff --git a/fnd/tui/match_navigator.py b/fnd/tui/match_navigator.py index 7f7db41a..d50ec808 100644 --- a/fnd/tui/match_navigator.py +++ b/fnd/tui/match_navigator.py @@ -7,7 +7,7 @@ A hop stays inside the current result's chunk. At its edge the press HANDS OVER to the file's adjacent listed section in document order, by moving the results -selection — so leaving a section is always a visible result switch, never a +selection, so leaving a section is always a visible result switch, never a silent scroll into a neighbour's match. The counts stay per-section, which is what they are for: ``▼0`` means the next press leaves this section, not that it does nothing. @@ -34,7 +34,7 @@ def view_anchors(ys: list[int], vh: int, home: int | None = None) -> list[int]: - """Content-space tops of the views ``n``/``b`` step through — a greedy tiling + """Content-space tops of the views ``n``/``b`` step through: a greedy tiling of the stops, each view spanning ``vh`` from the first match it reaches. ONE list drives both directions and both border counts, so a hop and a count @@ -47,7 +47,7 @@ def view_anchors(ys: list[int], vh: int, home: int | None = None) -> list[int]: match, so tiling from the match leaves it a position no key can return to. """ if vh <= 0: - return [] # unlaid-out / hidden pane — no meaningful screenful width + return [] # unlaid-out / hidden pane: no meaningful screenful width anchors: list[int] = [] covered_to: int | None = None if home is not None and ys and home <= ys[0] < home + vh: @@ -61,10 +61,10 @@ def view_anchors(ys: list[int], vh: int, home: int | None = None) -> list[int]: def adjacent_section(seqs: list[int], current: int, *, forward: bool) -> int | None: - """The listed section ``n``/``b`` hands over to once ``current`` is exhausted - — the next (or previous) in DOCUMENT order, wrapping at the file's ends, and - ``None`` when there is nowhere to go. Not results order: the tree ranks by - score, so its neighbouring row is somewhere else in the file entirely.""" + """The listed section ``n``/``b`` hands over to once ``current`` is exhausted: + the next (or previous) in DOCUMENT order, wrapping at the file's ends, and + ``None`` when there is nowhere to go. Not results order: the tree ranks by + score, so its neighbouring row is somewhere else in the file entirely.""" ordered = sorted(set(seqs)) if not ordered or ordered == [current]: return None @@ -74,7 +74,7 @@ def adjacent_section(seqs: list[int], current: int, *, forward: bool) -> int | N def _view_of(anchors: list[int], y: int) -> int: - """Index of the view that shows stop ``y`` — the last anchor at or above it. + """Index of the view that shows stop ``y``: the last anchor at or above it. Every stop falls in a view by construction of :func:`view_anchors`.""" return max(0, bisect_right(anchors, y) - 1) @@ -112,7 +112,7 @@ def _reveals(stops: list[int], target: int, top: int, vh: int) -> bool: def offscreen_views(anchors: list[int], stops: list[int], top: int, vh: int) -> tuple[int, int]: - """``(views above, views below)`` — the ``▲a ▼b`` border: views holding a + """``(views above, views below)``: the ``▲a ▼b`` border: views holding a match this viewport does not show. Counted over the same stops :func:`step_view` walks, so ``▼2`` is two more @@ -145,41 +145,41 @@ def offscreen_views(anchors: list[int], stops: list[int], top: int, vh: int) -> class MatchNavigator: """Owns intra-file match navigation for the live preview pane. - The three surfaces deliberately use DIFFERENT data so each stays correct, - cheap, AND free of side effects on unrelated scroll timing: - - * **The footer-hint gate** (`count`) is derived from match DATA — - ``_fnd_match_coords`` on the mounted tables + each chunk's ``match_blocks`` - — plain list attributes set at compose time. Counting them never reads a - region, so it never forces layout, so it can't perturb the delicate cold- - nav scroll-settle window (region reads did — measured to stall the - landing). It is cached; the footer reads the cache, so a focus change - never re-walks the preview subtree (a perf contract the pane keeps). - * **Navigation** (``next``/``prev``) enumerates the match stops fresh on - the keypress — a snapshot goes stale as the lazy-mounted preview reflows - (the reported bug: n did nothing because the snapshot was captured empty - before a deep table laid out). Layout reads here are fine: they happen on - a deliberate keypress, never during a cold-nav settle. - * **The ▲a/▼b view arrows** answer the original awareness question — "does - the result I'm on have matches I can't see?" Users navigate *between* - results with the results-pane arrows, which skip straight past matches - lower down in the *same* chunk; the arrows count how many screenfuls - ("views") of the CURRENT result hold an off-screen match, above and below. - Everything here is scoped to the current chunk (``anchor.focus_chunk_seq`` - and its widget extent) — never the whole file. The counts ARE layout- - derived, but the read is deferred to settle-safe moments only (after the - mount settles, on a nav keypress, on a user scroll) and cached, so the - border refresh stays layout-free. - - ``n``/``b`` hop between the current result's matching views, and at its edge - hand over to the file's adjacent listed section (see the module docstring). + The three surfaces deliberately use DIFFERENT data so each stays correct, + cheap, AND free of side effects on unrelated scroll timing: + + * **The footer-hint gate** (`count`) is derived from match DATA: + ``_fnd_match_coords`` on the mounted tables + each chunk's ``match_blocks``: + plain list attributes set at compose time. Counting them never reads a + region, so it never forces layout, so it can't perturb the delicate cold- + nav scroll-settle window (region reads did: measured to stall the + landing). It is cached; the footer reads the cache, so a focus change + never re-walks the preview subtree (a perf contract the pane keeps). + * **Navigation** (``next``/``prev``) enumerates the match stops fresh on + the keypress: a snapshot goes stale as the lazy-mounted preview reflows + (the reported bug: n did nothing because the snapshot was captured empty + before a deep table laid out). Layout reads here are fine: they happen on + a deliberate keypress, never during a cold-nav settle. + * **The ▲a/▼b view arrows** answer the original awareness question: "does + the result I'm on have matches I can't see?" Users navigate *between* + results with the results-pane arrows, which skip straight past matches + lower down in the *same* chunk; the arrows count how many screenfuls + ("views") of the CURRENT result hold an off-screen match, above and below. + Everything here is scoped to the current chunk (``anchor.focus_chunk_seq`` + and its widget extent): never the whole file. The counts ARE layout- + derived, but the read is deferred to settle-safe moments only (after the + mount settles, on a nav keypress, on a user scroll) and cached, so the + border refresh stays layout-free. + + ``n``/``b`` hop between the current result's matching views, and at its edge + hand over to the file's adjacent listed section (see the module docstring). """ def __init__(self, app: FNDApp) -> None: self._app = app self._count = 0 # cached data-derived match count (footer-hint gate) # Cached matching-view counts for the current result, relative to the - # viewport — the ▲a/▼b markers on the preview border. Region-derived + # viewport: the ▲a/▼b markers on the preview border. Region-derived # (unlike _count) but only re-measured at settle-safe moments (see # _measure_after_settle); the border reads the cache, never the regions. self._above = 0 @@ -196,7 +196,7 @@ def __init__(self, app: FNDApp) -> None: self._confirm_until = 0.0 # The generation a confirmation chain is armed for, or None. Keyed by # generation, not a bare flag: a chain armed under an older one is dead - # on arrival, and a flag cannot say so — it just blocks the arm the new + # on arrival, and a flag cannot say so; it just blocks the arm the new # navigation needs, leaving an open window with nothing running. self._confirm_armed_gen: int | None = None # The last hop's destination and the landing the chunk was entered at, @@ -242,7 +242,7 @@ def _pane(self) -> VerticalScroll | None: return None def _stops_within(self, root: Widget, spec: MatchSpec) -> int: - """Match stops inside ``root``, from DATA only — no region reads, so no + """Match stops inside ``root``, from DATA only: no region reads, so no layout is forced: one per matching table cell, one per non-table match block, one per matching plain line. @@ -253,7 +253,7 @@ def _stops_within(self, root: Widget, spec: MatchSpec) -> int: while a fill is hiding chunks. Shared by the preview-wide count and the current-chunk check so the two - can't drift on what counts as a stop — they answer the same question at + can't drift on what counts as a stop: they answer the same question at different scopes, and a footer hint disagreeing with what n/b can reach is exactly the confusion this scan exists to prevent. """ @@ -281,7 +281,7 @@ def _stops_within(self, root: Widget, spec: MatchSpec) -> int: # Frozen chunks carry their stop count from capture time. Counting them # keeps the footer hint and ``current_chunk_has_stops`` honest: without # it a frozen chunk reads as having no matches, so ``n``/``b`` would be - # advertised as unavailable on a chunk that does have them — or, worse, + # advertised as unavailable on a chunk that does have them, or, worse, # silently do nothing. Data only, no region reads, like everything else # in this method. from fnd.tui.preview.frozen import FrozenChunkView @@ -296,7 +296,7 @@ def _stops_within(self, root: Widget, spec: MatchSpec) -> int: return n def _count_stops(self, pane: VerticalScroll) -> int: - """Stops across the whole mounted preview — the footer-hint cache.""" + """Stops across the whole mounted preview: the footer-hint cache.""" return self._stops_within(pane, self._app._effective_match_spec) def _content_stops(self, pane: VerticalScroll) -> list[int]: @@ -307,7 +307,7 @@ def _content_stops(self, pane: VerticalScroll) -> list[int]: return enumerate_stop_rows(pane, self._app._effective_match_spec) def _current_chunk_extent(self, pane: VerticalScroll) -> tuple[int, int] | None: - """Content-space ``[top, bottom)`` of the CURRENT result's chunk — the + """Content-space ``[top, bottom)`` of the CURRENT result's chunk: the one the results-pane selection revealed (``anchor.focus_chunk_seq``). ``bottom`` is the next chunk's top (so it works whether the chunk widget is the full ``FNDMarkdown`` or just a plain chunk's first line); the last @@ -354,7 +354,7 @@ def _chunk_top(pane: VerticalScroll, widget: object) -> int | None: def _chunk_stops(self, pane: VerticalScroll) -> list[int]: """The current result's match stops (content-space tops). An unknown - extent (flat preview) falls back to every mounted stop — which on that + extent (flat preview) falls back to every mounted stop, which on that substrate is none, so the keys are inert rather than unscoped.""" stops = self._content_stops(pane) extent = self._current_chunk_extent(pane) @@ -381,7 +381,7 @@ def _offscreen_views(self, pane: VerticalScroll) -> tuple[int, int]: @property def count(self) -> int: - return self._count # cached — cheap, no subtree walk + return self._count # cached: cheap, no subtree walk def current_chunk_has_stops(self) -> bool: """Whether ``n``/``b`` can do anything from where the user is: walk this @@ -389,7 +389,7 @@ def current_chunk_has_stops(self) -> bool: return self._chunk_has_match_data() or self.can_hop_section() def _is_dead_end(self) -> bool: - """Whether this chunk offers nothing to walk to — the only state a + """Whether this chunk offers nothing to walk to: the only state a hand-over may fire from when no stop resolved. A chunk still mounting or still building is not one: the data gate reads False in both windows, and handing over there walks the reader out of the section mid-navigation.""" @@ -399,8 +399,8 @@ def _is_dead_end(self) -> bool: if seq is None or seq not in widgets: return False # Its blocks register DURING the build, so an empty match set before - # ``build_done`` means "not yet". This is the wide window — seconds on a - # big chunk — where the un-mounted one above is a tick. + # ``build_done`` means "not yet". This is the wide window, seconds on a + # big chunk, where the un-mounted one above is a tick. build_done = getattr(widgets[seq], "build_done", None) if build_done is not None and not build_done.is_set(): return False @@ -410,10 +410,10 @@ def _chunk_has_match_data(self) -> bool: """Whether the current chunk carries a match at all. ``count`` spans the whole mounted preview, but ``_go`` operates on - ``_chunk_stops`` — scoped to the current result's chunk. Gating the + ``_chunk_stops``: scoped to the current result's chunk. Gating the footer hint on ``count`` therefore advertised ``n/b Matches`` on a chunk where both keys silently no-op. Mirrors ``_chunk_stops``' own scoping, - and reads data only — never regions — so it answers while the layout is + and reads data only, never regions, so it answers while the layout is still resolving, which is what tells a mid-mount press from a dead end. """ @@ -428,7 +428,7 @@ def _chunk_has_match_data(self) -> bool: widgets: dict[int, object] = getattr(self._app._preview, "chunk_widgets", None) or {} current = widgets.get(anchor.focus_chunk_seq) if anchor is not None else None if current is None: - # Flat preview, or nothing mounted yet — _chunk_stops falls back to + # Flat preview, or nothing mounted yet: _chunk_stops falls back to # every mounted stop here, so the file-wide count is the honest gate. return self._count > 0 if isinstance(current, FNDMarkdown): @@ -436,26 +436,26 @@ def _chunk_has_match_data(self) -> bool: from fnd.tui.preview.frozen import FrozenChunkView if isinstance(current, FrozenChunkView): - # A capture has no blocks and no match target — serving one pops the - # target — so without this branch the plain-chunk fallback below + # A capture has no blocks and no match target: serving one pops the + # target, so without this branch the plain-chunk fallback below # reads `None` and hides the `n/b Matches` hint on a chunk where both # keys work. Routed through `_stops_within` rather than reading # `stop_rows` here, so the count and this gate cannot drift on what - # counts as a stop — which is the reason that method has a frozen arm. + # counts as a stop, which is the reason that method has a frozen arm. return self._stops_within(current, self._app._effective_match_spec) > 0 # Plain per-line chunk: the mount records the first matching line as the - # match target, falling back to the first line when nothing matched — so + # match target, falling back to the first line when nothing matched, so # the match class on that target is exactly "this chunk has a stop". target = self._app._preview.match_targets.get(anchor.focus_chunk_seq) if anchor else None return target is not None and target.has_class("chunk-line-match") @property def above(self) -> int: - return self._above # cached — matching views above the viewport (this result) + return self._above # cached: matching views above the viewport (this result) @property def below(self) -> int: - return self._below # cached — matching views below the viewport (this result) + return self._below # cached: matching views below the viewport (this result) def on_manual_scroll(self) -> None: """Drop the burst memory so the next ``n``/``b`` is computed purely from @@ -480,12 +480,12 @@ def rebuild(self) -> None: # Unconditional: this used to skip the notify when the arrow counts were # already (0, 0), which is exactly the case for a result the preview # can't paint. The border then kept whatever the PREVIOUS result had put - # there — so the "match not shown here" notice never appeared on the + # there, so the "match not shown here" notice never appeared on the # rows that need it, and never cleared on the rows that don't. One # border + footer update per preview mount is not worth the ambiguity. # ``_count`` too: it is the fallback ``current_chunk_has_stops`` uses # while no current chunk is resolvable, which is exactly the window a - # mounting preview sits in — so a retained count advertised "n/b + # mounting preview sits in, so a retained count advertised "n/b # Matches" for the result the user just navigated AWAY from. Recomputed # by the count tick below once the new preview is up. self._above = self._below = self._count = 0 @@ -507,7 +507,7 @@ def _await_mount(self, gen: int, retries: int) -> None: landing). Just wait for is_complete, then count. ``is_complete`` means every chunk of the file is mounted, which above - ``FULLMOUNT_CHUNK_BUDGET`` is deliberately never true — the file stays + ``FULLMOUNT_CHUNK_BUDGET`` is deliberately never true: the file stays windowed by design. So on a large file this runs its whole budget out and looks like pure waste. **It is not.** Burning the budget is what holds ``_count_tick``'s subtree walk back until after the mount, which is the @@ -518,7 +518,7 @@ def _await_mount(self, gen: int, retries: int) -> None: The protection is therefore accidental, and anything that makes ``is_complete`` reachable removes it silently. Gating on the landing instead (``is_settling`` plus ``pipeline_busy``) states it deliberately - and measures neutral — correct, but not worth the churn on its own.""" + and measures neutral: correct, but not worth the churn on its own.""" if gen != self._refresh_gen: return # superseded by a newer rebuild active = getattr(getattr(self._app, "_preview", None), "active", None) @@ -543,7 +543,7 @@ def _count_tick(self, gen: int, retries: int) -> None: if retries > 0 and (retries > 1 or self._count != prev): self._app.call_after_refresh(lambda: self._count_tick(gen, retries - 1)) else: - # Count is stable; now measure the off-screen arrows — but only once + # Count is stable; now measure the off-screen arrows, but only once # the nav scroll has provably landed (region reads mid-settle perturb # the cold-nav landing). self._measure_after_settle(gen, retries=30) @@ -558,7 +558,7 @@ def _poll_until_landed( ) -> None: """Reschedule until the reveal scroll has actually LANDED, then run ``on_landed``. Two conditions must hold: the controller is not settling - (cold-nav gate) AND ``scroll_y`` has stopped moving — a warm nav commits + (cold-nav gate) AND ``scroll_y`` has stopped moving; a warm nav commits its reveal scroll without ever flipping ``is_settling``, so the settle flag alone would measure against the pre-reveal viewport. Only the scroll reactive is read while polling (no layout), so the delicate settle path @@ -568,7 +568,7 @@ def _poll_until_landed( return pane = self._pane() if pane is None: - on_landed() # pane gone (teardown) — let on_landed settle/clear state + on_landed() # pane gone (teardown): let on_landed settle/clear state return cur = pane.scroll_offset.y ctrl = getattr(self._app, "_preview_scroll", None) @@ -584,7 +584,7 @@ def _poll_until_landed( on_landed() def _measure_after_settle(self, gen: int, retries: int) -> None: - """Post-mount initial measure: land, then measure — self-cancelling if a + """Post-mount initial measure: land, then measure, self-cancelling if a newer rebuild superseded this generation.""" self._poll_until_landed( retries, @@ -620,7 +620,7 @@ def _confirm_tick(self, gen: int) -> None: """One confirmation: read now unless a navigation is mid-settle. Not routed through :meth:`_schedule_measure`, whose landing poll - reschedules per refresh — a window of that is per-frame work on the + reschedules per refresh; a window of that is per-frame work on the settle path. A skipped tick costs nothing; the next is 250ms away.""" if gen != self._refresh_gen: return # superseded; its own generation's chain owns the window @@ -667,7 +667,7 @@ def on_preview_scrolled(self) -> None: self._schedule_measure() def on_result_revealed(self) -> None: - """A new result was positioned in the preview — the AUTHORITATIVE switch + """A new result was positioned in the preview: the AUTHORITATIVE switch event. The scroll watcher misses switches whose reveal doesn't move the scroll (so the old result's markers would linger); this fires regardless. Clear the old markers now, reset the burst memory (a new result is a @@ -690,7 +690,7 @@ def _schedule_measure(self) -> None: clear the counts and advance the generation, and this poll would then land mid-mount, read regions during the cold-nav settle window, and repopulate ▲/▼ counts belonging to the previous result. Coalescing made - it worse — while the stale poll was pending, requests for the NEW + it worse, while the stale poll was pending, requests for the NEW preview were dropped as duplicates. """ if self._measure_pending: @@ -726,7 +726,7 @@ def prev(self) -> None: def _active_parent(self) -> str | None: """The file both the preview and the anchor name, or ``None`` when they - disagree — a whole navigation long, since the anchor is armed before the + disagree: a whole navigation long, since the anchor is armed before the container is activated. ``None`` on a flat preview, which nulls ``active`` and contributes no stops for n/b to walk.""" preview = getattr(self._app, "_preview", None) @@ -805,7 +805,7 @@ def _select_section_row(self, seq: int) -> bool: return True def can_hop_section(self) -> bool: - """Whether a hand-over is available from here — data only (results rows + """Whether a hand-over is available from here: data only (results rows and the active file), so it is safe wherever the footer gate is. Never with highlights off: the spec is empty, there is nothing to walk @@ -845,7 +845,7 @@ def _go(self, *, forward: bool) -> None: return # Scope to the CURRENT result's chunk so a hop reveals its hidden matches # and stops at its boundaries; the edge is where the hand-over below takes - # over. Fresh each press — never a stale snap. + # over. Fresh each press: never a stale snap. stops = self._chunk_stops(pane) vh = pane.scrollable_content_region.height if not stops or vh <= 0: @@ -875,7 +875,7 @@ def _go(self, *, forward: bool) -> None: return self._last_rel, self._last_seq = target - base, self._focus_seq() self._scroll_to_stop(pane, target, vh) - # Re-measure the view arrows AFTER the scroll commits — reading regions + # Re-measure the view arrows AFTER the scroll commits: reading regions # synchronously here (before layout settles) yields an unresolved # viewport. Coalesced; runs on the next refresh with the scroll applied. self._schedule_measure() @@ -894,7 +894,7 @@ def _scroll_to_stop(self, pane: VerticalScroll, top_y: int, vh: int) -> None: # treat it as a user scroll (which would clear the burst memory we just # set). Mirrors StructuralScrollStrategy's reconcile-scroll guard. # ``immediate`` (not animated) so the scroll commits synchronously - # inside the reconcile window — an animated scroll's watcher trips fire + # inside the reconcile window: an animated scroll's watcher trips fire # after ``end_reconcile_scroll``, outside the guard, and would then be # mistaken for a user scroll. preview = getattr(self._app, "_preview", None) diff --git a/fnd/tui/menu.py b/fnd/tui/menu.py index 5e344583..3af619f3 100644 --- a/fnd/tui/menu.py +++ b/fnd/tui/menu.py @@ -1,4 +1,4 @@ -"""Unified Settings & Commands menu — data model. +"""Unified Settings & Commands menu: data model. The menu is a tree of :class:`MenuItem` records, one row per item the user sees in the Settings screen. ``kind`` decides what ``Enter`` does: @@ -15,7 +15,7 @@ ``toggle`` Flip a boolean inline. Menu stays open so the user sees the new value. ``picker`` Push a picker sub-screen with a list of choices. - ``external`` Run a custom callable that takes the app — used to push + ``external`` Run a custom callable that takes the app: used to push custom screens (per-source form, delete confirm, rename). The root menu is one flat scrollable list grouped with ``KIND_HEADER`` @@ -241,7 +241,7 @@ def header( ``hint=True`` stamps the header (and every body row until the next header) with a ``-hint-section`` CSS class via the SettingsList's - rendering loop — used by the Keybindings cheat sheet to highlight + rendering loop: used by the Keybindings cheat sheet to highlight the section relevant to the screen that called ``?``. """ keywords: tuple[str, ...] = ("_hint_section_",) if hint else () @@ -263,7 +263,7 @@ def walk_leaves( breadcrumb: tuple[str, ...] = (), ) -> Iterator[tuple[tuple[str, ...], MenuItem]]: """Yield ``(breadcrumb, leaf)`` pairs for every selectable item in - the tree. Headers are skipped — they're visual structure, not + the tree. Headers are skipped: they're visual structure, not discoverable content. """ path = (*breadcrumb, item.label) @@ -281,7 +281,7 @@ def walk_leaves( # Each row is a (key, action_id_or_empty, description) tuple. Activating # the row dispatches the action if action_id is set; built-in widget # keys (tree navigation, scroll) are listed for discoverability with an -# empty action_id — Enter on those is a no-op (the user invokes them +# empty action_id: Enter on those is a no-op (the user invokes them # directly when the relevant pane has focus). @@ -310,7 +310,7 @@ def walk_leaves( def _pretty_key(key: str) -> str: # A comma-separated ``default_key`` lists alternatives (same action, more - # than one key) — render each and join them. + # than one key): render each and join them. if "," in key: return " / ".join(_pretty_key(k.strip()) for k in key.split(",") if k.strip()) if key in _KEY_PRETTY: @@ -329,7 +329,7 @@ def _pretty_key(key: str) -> str: return "+".join(parts) if len(parts) > 1 else key -# Sections that wrap bindings NOT in the Action registry — widget-level +# Sections that wrap bindings NOT in the Action registry: widget-level # bindings from SettingsList, SourceFormScreen, OpenWithScreen, # AccessibilityPermissionScreen, etc. Listed here so the Keybindings # screen surfaces them too. @@ -631,7 +631,7 @@ def _key_row( ) -> MenuItem: """Build a Keybindings cheat-sheet row. ``label`` is the short title shown in the row list; ``description`` is the long-form explanation - surfaced in the DetailStrip when the row is focused — DON'T pass + surfaced in the DetailStrip when the row is focused: DON'T pass the same string for both, or the DetailStrip just echoes the row. ``section`` disambiguates widget-only rows (action_id="") that @@ -640,7 +640,7 @@ def _key_row( and the second mount would shadow the first. """ # Slug the row id from the *un*localised key/label so ids stay identical on - # every OS — "⌥ ↑" and "Alt ↑" must not mint two different ids for one row. + # every OS: "⌥ ↑" and "Alt ↑" must not mint two different ids for one row. item_id = f"key.{action_id}" if action_id else "key." + _slug(section, key, label) # An action that works in several panes is listed in each of them; the # first keeps the plain id so nothing referring to it moves. @@ -678,20 +678,20 @@ def _action_label(action: Any) -> str: def _provider_keybindings(_app: FNDApp, *, context_hint: str | None = None) -> tuple[MenuItem, ...]: """Build the Keybindings list from the live ``Action`` registry plus - static widget-binding tables. + static widget-binding tables. - Single source of truth = ``fnd.tui.actions.REGISTRY``. Sub-sections: + Single source of truth = ``fnd.tui.actions.REGISTRY``. Sub-sections: - * Global — actions with no ``contexts`` constraint. - * Per-pane sections (Results / Outline / Preview / Query / Filters / Collections) - — actions whose primary (first) context matches that pane. - * Static sections — Settings menu, Source form, Open-with modal, - Accessibility prompt. These live in widget BINDINGS, not the - registry, so they're hand-curated; they're at least short. + * Global: actions with no ``contexts`` constraint. + * Per-pane sections (Results / Outline / Preview / Query / Filters / Collections): + actions whose primary (first) context matches that pane. + * Static sections: Settings menu, Source form, Open-with modal, + Accessibility prompt. These live in widget BINDINGS, not the + registry, so they're hand-curated; they're at least short. - ``context_hint`` is the section label that should be moved to the - top (right after Global) — e.g. "Source form" when the user pressed - ``?`` from inside the source-edit form. Empty sections are dropped. + ``context_hint`` is the section label that should be moved to the + top (right after Global): e.g. "Source form" when the user pressed + ``?`` from inside the source-edit form. Empty sections are dropped. """ from fnd.tui.actions import REGISTRY @@ -706,7 +706,7 @@ def _provider_keybindings(_app: FNDApp, *, context_hint: str | None = None) -> t } for action in REGISTRY: if action.default_key is None: - continue # palette-only — no key to show + continue # palette-only: no key to show # Every pane it works in, not just the first: ←/→ expand and collapse # in all three trees but were documented under Results alone, so the # Filters section named neither of the keys that move around it. @@ -731,7 +731,7 @@ def _provider_keybindings(_app: FNDApp, *, context_hint: str | None = None) -> t _key_row(*row, section="outline_widget") for row in _KEYS_OUTLINE_WIDGET ] - # Static widget bindings — append AFTER the registry-derived + # Static widget bindings: append AFTER the registry-derived # sections in declaration order; reordering happens below. # Pass section name so widget-only rows that share labels across # sections (multiple "Cancel" rows) get distinct MenuItem ids. @@ -743,7 +743,7 @@ def _provider_keybindings(_app: FNDApp, *, context_hint: str | None = None) -> t ] sections["Open with… modal"] = [_key_row(*row, section="open_with") for row in _KEYS_OPEN_WITH] # AX permission gates the macOS Preview AppleScript page-jump, so the modal - # can never surface on Linux/Windows — listing its keys there would point + # can never surface on Linux/Windows: listing its keys there would point # users at a System Settings pane their OS doesn't have. if os_labels.is_macos(): sections["Accessibility prompt"] = [ @@ -896,7 +896,7 @@ def _set_highlights(app: FNDApp, value: bool) -> None: def _provider_preferences(_app: FNDApp) -> tuple[MenuItem, ...]: - """Content of the Preferences sub-screen — scalars / toggles / pickers + """Content of the Preferences sub-screen: scalars / toggles / pickers grouped by area.""" return ( header("Search behaviour", level=2), @@ -1236,7 +1236,7 @@ def _choices_apps_for_kind(app: FNDApp, kind: str) -> list[ChoiceOption]: continue if not app_def.available(): continue - # `reveal` acts on the file without opening it — offering it here would + # `reveal` acts on the file without opening it: offering it here would # let a default silently stop Open from opening this kind. It stays in # the Open-with picker, which is a one-shot choice. if not app_def.selectable_default: @@ -1260,7 +1260,7 @@ def _get_app_default_for_kind(app: FNDApp, kind: str) -> str: def _set_app_default_for_kind(app: FNDApp, kind: str, value: Any) -> None: """Persist the picker choice. Empty value (auto-resolve sentinel) - clears the explicit default — done by reloading config, removing + clears the explicit default: done by reloading config, removing the key, and writing the whole [app_defaults] back via tomlkit.""" from fnd.config import default_config_path, load, write_setting @@ -1268,7 +1268,7 @@ def _set_app_default_for_kind(app: FNDApp, kind: str, value: Any) -> None: if value: write_setting(config_path=cfg_path, dotted_path=("app_defaults", kind), value=value) else: - # No write_unset helper — round-trip via tomlkit to drop the key. + # No write_unset helper: round-trip via tomlkit to drop the key. import tomlkit if cfg_path.exists(): @@ -1306,7 +1306,7 @@ def interrupted_index(name: str) -> tuple[int, int] | None: def _collection_summary(app: FNDApp, name: str) -> str: - """Trailing slot for a collection row in the Collections sub-screen — + """Trailing slot for a collection row in the Collections sub-screen: shows scope dot, source count, ranking profile, and whether the last index of it stopped part-way.""" cfg = app._config # type: ignore[attr-defined] @@ -1470,7 +1470,7 @@ def _open_keybindings_file_action(app: FNDApp) -> None: def _provider_collections(app: FNDApp) -> tuple[MenuItem, ...]: """Content of the Collections sub-screen. - Two actions at the top — Add collection + Update all collections — + Two actions at the top, Add collection + Update all collections, then one drill-in row per configured collection.""" cfg = app._config # type: ignore[attr-defined] names = sorted(cfg.collections.keys()) if cfg else [] @@ -1601,7 +1601,7 @@ def _compute_flat_pdfs_for(name: str) -> str: def _summary_update_all(app: FNDApp) -> str: - """Trailing context for the Update all collections action — count + """Trailing context for the Update all collections action: count of collections + rough total file count if known.""" cfg = app._config # type: ignore[attr-defined] if cfg is None: @@ -2083,7 +2083,7 @@ def _summary_keybindings_path(_app: FNDApp) -> str: def _provider_indexing(_app: FNDApp) -> tuple[MenuItem, ...]: - """Indexing sub-screen — app-wide search-index actions and behaviour. + """Indexing sub-screen: app-wide search-index actions and behaviour. The combined screen prepends an "Update everything (index + texturise)" action at the top via ``_provider_indexing_pdf_texture``; @@ -2154,7 +2154,7 @@ def _provider_indexing(_app: FNDApp) -> tuple[MenuItem, ...]: def _provider_pdf_texture(_app: FNDApp) -> tuple[MenuItem, ...]: - """PDF Texture sub-screen — engine, status / actions, cache, and + """PDF Texture sub-screen: engine, status / actions, cache, and texturising-while-indexing behaviour. Texturising is the act of turning a PDF's pages into structured @@ -2643,7 +2643,7 @@ def _pdf_install_verb() -> str: def _open_pdf_install_confirm(app: FNDApp) -> None: """Push the disclosure + Yes/Cancel confirm. On Yes the install (or - uninstall) progress modal lands — wired in step 6b.""" + uninstall) progress modal lands: wired in step 6b.""" from fnd.tui.settings_screen import StructuredPdfConfirmScreen app.push_screen(StructuredPdfConfirmScreen()) @@ -2660,7 +2660,7 @@ def _get_cache_at_index_time(app: FNDApp) -> bool: def _summary_cache_update(app: FNDApp) -> str: - """Trailing context for the Update cache action — number of PDFs + """Trailing context for the Update cache action: number of PDFs that don't yet have a cache entry, with a rough time estimate. Lazy-loaded since it scans every source dir.""" from fnd.tui.lazy_trailing import get_or_schedule @@ -2760,7 +2760,7 @@ def _forget_cache_for_flat_pdfs() -> None: file doesn't take down the whole texturise run. Reuses the most recent background scan when one is cached; otherwise - computes inline (this runs on a worker thread, never the event loop — + computes inline (this runs on a worker thread, never the event loop: see ``_run_update_cache``).""" import contextlib @@ -2797,7 +2797,7 @@ def _forget_cache_for_flat_pdfs() -> None: def _summary_indexing(app: FNDApp) -> str: - """Auto-resume chip — still used by tests after the section combine.""" + """Auto-resume chip: still used by tests after the section combine.""" return "✓ auto-resume" if _get_indexer_auto_resume(app) else "✗ auto-resume" @@ -2812,7 +2812,7 @@ def _engine_chip() -> str: def _summary_pdf_texture(app: FNDApp) -> str: - """Engine + cache chip — still used by tests after the section combine.""" + """Engine + cache chip: still used by tests after the section combine.""" from fnd.tui.lazy_trailing import PLACEHOLDER, get_or_schedule parts: list[str] = [] @@ -2831,7 +2831,7 @@ def _summary_pdf_texture(app: FNDApp) -> str: def _structured_engine_active() -> bool: """Whether the current extractor actually produces structured output (pymupdf4llm importable). Distinct from _is_pdf_structure_installed(), - which tracks the in-app install marker — re-texturisability depends on + which tracks the in-app install marker: re-texturisability depends on the live capability that extractor_signature() encodes, so re-texturise counts gate on this. A 'flat' signature means re-texturising would only re-produce flat output, so there's nothing to offer.""" @@ -2842,7 +2842,7 @@ def _structured_engine_active() -> bool: def _stale_count_short() -> str: """Compact '⟳ N old-engine' chip counting SAVED TEXTURINGS (cache - entries) on an older engine version — a reason to rebuild. Counts + entries) on an older engine version: a reason to rebuild. Counts cache entries, not PDFs, so it stays honest about its source.""" if not _structured_engine_active(): return "" @@ -2854,9 +2854,9 @@ def _stale_count_short() -> str: def _summary_rebuild_all(app: FNDApp) -> str: """Trailing for the 'Rebuild all collections' row. Nudges with the - number of SAVED TEXTURINGS (cache entries) on an older engine version - — counts cache entries, not PDFs (an orphaned entry counts too), so it - states its source honestly rather than implying a PDF tally.""" + number of SAVED TEXTURINGS (cache entries) on an older engine version: + counts cache entries, not PDFs (an orphaned entry counts too), so it + states its source honestly rather than implying a PDF tally.""" from fnd.tui.lazy_trailing import get_or_schedule def _compute() -> str: @@ -2900,7 +2900,7 @@ def _compute() -> str: def _summary_cache_location_row(_app: FNDApp) -> str: - """Trailing for the Cache location row — abbreviated home-dir path. + """Trailing for the Cache location row: abbreviated home-dir path. Cheap (string ops only) so no lazy wrapper needed.""" from fnd.cache import default_cache_dir @@ -3006,7 +3006,7 @@ def _compute_pdfs_textured() -> str: if total == 0: return "no PDFs" - # X: PDFs in the index with at least one chunk carrying body_md — + # X: PDFs in the index with at least one chunk carrying body_md; # the Markdown texturing payload that drives the structural # preview. body_struct (flat Blocks) is present on EVERY indexed # PDF, so it can't distinguish textured from flat; body_md is the @@ -3053,7 +3053,7 @@ def _run_prune_orphans(app: FNDApp) -> None: """Remove texturings for files no longer on disk. Hashing the corpus to find what's live is slow, so run it off the UI thread and notify the result. Orphans are entries for files that no longer exist, so - removal is safe — re-indexing re-creates them only if a file returns.""" + removal is safe: re-indexing re-creates them only if a file returns.""" import contextlib import threading @@ -3196,7 +3196,7 @@ def _provider_filters(app: FNDApp) -> tuple[MenuItem, ...]: def _provider_root(_app: FNDApp) -> tuple[MenuItem, ...]: - """Root settings menu — a clean, short list of categories. No + """Root settings menu: a clean, short list of categories. No content piled on top of each other. Each drill-in row pushes its own sub-screen.""" return ( @@ -3303,7 +3303,7 @@ def build_root_items(app: FNDApp) -> tuple[MenuItem, ...]: return _provider_root(app) -# Section providers exposed by id — used by `open_settings_section` and +# Section providers exposed by id: used by `open_settings_section` and # the `?` shortcut to push a specific sub-screen directly. _SECTION_PROVIDERS: dict[str, Callable[[FNDApp], tuple[MenuItem, ...]]] = { SECTION_PREFERENCES: _provider_preferences, @@ -3385,7 +3385,7 @@ def walk_all_sections(app: FNDApp) -> Iterator[tuple[tuple[str, ...], MenuItem]] """Yield ``(breadcrumb, leaf)`` pairs for every selectable item across every section. The basis for cross-section search. - Headers are skipped. Per-collection sub-screens are NOT descended — + Headers are skipped. Per-collection sub-screens are NOT descended: finding a collection in search drills into its editor anyway. """ for section_id, label in _SECTION_LABELS.items(): diff --git a/fnd/tui/mermaid_render.py b/fnd/tui/mermaid_render.py index c52cad5c..1a186c73 100644 --- a/fnd/tui/mermaid_render.py +++ b/fnd/tui/mermaid_render.py @@ -22,7 +22,7 @@ _AVAILABLE = termaid is not None -# Above this the A* layout isn't worth the on-loop cost — fall back to source. +# Above this the A* layout isn't worth the on-loop cost: fall back to source. MAX_SOURCE_LINES = 200 diff --git a/fnd/tui/open_with_screen.py b/fnd/tui/open_with_screen.py index dc9ac5d3..c6a71979 100644 --- a/fnd/tui/open_with_screen.py +++ b/fnd/tui/open_with_screen.py @@ -1,4 +1,4 @@ -"""Shift-O "Open with…" modal — pick which app opens the focused hit. +"""Shift-O "Open with…" modal: pick which app opens the focused hit. Surfaced when the user presses ``O`` on a result. One job: let the user pick an app for this file and fire it. The picker is a plain @@ -12,7 +12,7 @@ row directly without moving the cursor. * Esc dismisses. -No descriptions, hierarchy explainers, or app notes live here — +No descriptions, hierarchy explainers, or app notes live here: that's settings territory. Resolution itself is exactly what Open uses; see ``[app_defaults]`` in config.toml for the rules. """ @@ -62,7 +62,7 @@ def eligible_apps( are available, then sort: resolved default first, then registry insertion order. - ``availability`` overrides the live ``app.available()`` probe — used + ``availability`` overrides the live ``app.available()`` probe: used by tests so we don't need to mock every individual ``_*_exists`` helper. """ @@ -116,7 +116,7 @@ def letter_shortcuts(rows: Iterable[Any]) -> dict[str, str]: chosen = ch break if chosen is None: - continue # 9+ rows past digit range; skip — modal cap + continue # 9+ rows past digit range; skip: modal cap used.add(chosen) out[row.id] = chosen return out @@ -179,7 +179,7 @@ def __init__( ) self._shortcuts = letter_shortcuts(self._rows) # Per-instance letter-shortcut bindings. ``show=False`` keeps - # them out of the chrome — they're visible inline next to each + # them out of the chrome: they're visible inline next to each # row already. for app_id, key in self._shortcuts.items(): self._bindings.bind(key, f"fire('{app_id}')", show=False) diff --git a/fnd/tui/preview/coverage.py b/fnd/tui/preview/coverage.py index 43f28d7e..7e3d95bd 100644 --- a/fnd/tui/preview/coverage.py +++ b/fnd/tui/preview/coverage.py @@ -2,13 +2,13 @@ What the preview MOUNTS ahead is decided by file size: everything below the landing when a file is inside ``FULLMOUNT_CHUNK_BUDGET``, and a ±7 window -otherwise. That has to stay — the in-file match count walks the mounted subtree, -so a file that stops being filled stops being counted in full — but it leaves the +otherwise. That has to stay: the in-file match count walks the mounted subtree, +so a file that stops being filled stops being counted in full, but it leaves the large files with nothing. A 1018-chunk PDF mounts a window and throws it away on the next jump, so every far jump rebuilds from source whether or not the target was visited moments earlier. -Coverage fills that gap without touching what is mounted — with one exception. +Coverage fills that gap without touching what is mounted, with one exception. A file the user has asked to warm WHOLE is mounted whole once its captures are in, up to ``FULLWARM_MOUNT_MAX_CHUNKS``: capturing every chunk is only half of what that promises, because the windowed fill adds three chunks per scroll event @@ -20,7 +20,7 @@ * coverage fills a CACHE, not the pane. Mounting scattered hit chunks would put chunk 12 directly above chunk 20 with the document between them silently - missing — the mounted set has to stay contiguous, because lazy mount only ever + missing: the mounted set has to stay contiguous, because lazy mount only ever fills at its edges. A capture costs 44.5 KB and NO arrange time, which is what lets the cache hold a scattered set spanning several files that the DOM could not. @@ -31,7 +31,7 @@ one off-screen host, measured at roughly six chunks a second. So order is the whole design, and it is strictly by what a navigation is most likely to need: -1. every planned file's HIT chunks — the current file first, nearest to the +1. every planned file's HIT chunks: the current file first, nearest to the cursor, then the neighbours outward, then the head of the result list; 2. one file WHOLE, if the user has asked for that (see ``PreviewPresenter.request_full_warm``); @@ -42,20 +42,20 @@ files, against 0.6s for all five in this order. A requested whole file sits between them: somebody is waiting on it, so it -outranks context — but not landings, which are what keeps every other file's +outranks context, but not landings, which are what keeps every other file's jump served. Measured, it costs nothing to be second: on a warmed session the request starts within a capture, and only on a cold store does it wait. A pass is only re-planned when it ENDS, so the margin walk also stands down for a file the cursor has moved towards that the running plan never listed. Without -that, the file two below the cursor waited 31.0s for its first capture — 26.3s +that, the file two below the cursor waited 31.0s for its first capture: 26.3s of it the previous plan's margins, over files whose hits were already captured. Whole-file coverage used to run automatically, after the rest drained: 438 captures over 80 seconds on one already-served file while 42 other result files had nothing, to save a scroll lazy mount already handles. Being last did not -stop it taking everything, which is why it is now asked for rather than assumed -— and why what it costs goes to the user, who can watch it and stop it. +stop it taking everything, which is why it is now asked for rather than assumed, +and why what it costs goes to the user, who can watch it and stop it. Moving the cursor re-plans; it does not demote. Tier 1's order is taken around the cursor, so a move within the file invalidates the order but not the file, diff --git a/fnd/tui/preview/decode_progress.py b/fnd/tui/preview/decode_progress.py index c23728a0..94bd65fc 100644 --- a/fnd/tui/preview/decode_progress.py +++ b/fnd/tui/preview/decode_progress.py @@ -1,7 +1,7 @@ """Live unit count for the flat-preview decode. The flat path (PDF, TXT) builds its whole document in one worker call, so -from the outside it is a single opaque step — and its duration varies by +from the outside it is a single opaque step, and its duration varies by more than an order of magnitude (measured on a real corpus: p25 226 ms, median 1081 ms, p75 3135 ms). Nothing observable at dispatch predicts that: file size scales the duration by roughly its fourth root, because @@ -13,7 +13,7 @@ Same shape as :mod:`fnd.tui.live_progress`, which does this for PDF page extraction: the worker writes, the UI polls a snapshot. Deliberately not -a callback into the app — this runs on a worker thread, and marshalling +a callback into the app: this runs on a worker thread, and marshalling per line would cost more than the work being measured. """ diff --git a/fnd/tui/preview/flat_view.py b/fnd/tui/preview/flat_view.py index 82e6ccdb..daa36b1e 100644 --- a/fnd/tui/preview/flat_view.py +++ b/fnd/tui/preview/flat_view.py @@ -48,7 +48,7 @@ def __init__(self, app: FNDApp) -> None: def ensure_shared_buffer(self) -> LineBufferPreview | None: """Lazy-mount the single hidden LineBufferPreview under #preview_pane. - Returns None when there is no pane to mount into — the screen is + Returns None when there is no pane to mount into: the screen is being torn down, or has not composed yet. This runs on the ``preview-load`` worker, so raising here surfaces as a WorkerFailed that takes the run down instead of a mount that quietly has nothing @@ -60,7 +60,7 @@ def ensure_shared_buffer(self) -> LineBufferPreview | None: buf = self.shared_buffer # ``is_live`` rather than ``parent is not None``: a buffer whose removal # is queued still reports a parent (Textual prunes on a message), so the - # old test handed back a widget that was about to be torn out — the doc + # old test handed back a widget that was about to be torn out: the doc # was installed into it and the flat preview painted nothing. A dead # buffer is replaced, and ``installed_key`` reset so the fresh widget # gets the document installed rather than short-circuiting as "already @@ -138,9 +138,9 @@ def focus_line_for_chunk(fv: FileView, chunk_id: int) -> int | None: def build_file_view(self, chunks: list[FileChunk]) -> FileView: """Convert decoded chunks into a :class:`FileView` for the flat path. Reuses the same word-level match-span helper the - structural renderer uses so highlight semantics — including + structural renderer uses so highlight semantics, including the per-word colour (yellow for exact matches, orange for - fuzzy ones) — agree across pipelines.""" + fuzzy ones): agree across pipelines.""" spec = self._app._effective_match_spec import os diff --git a/fnd/tui/preview/frozen.py b/fnd/tui/preview/frozen.py index 3ed57b3a..f477321b 100644 --- a/fnd/tui/preview/frozen.py +++ b/fnd/tui/preview/frozen.py @@ -1,7 +1,7 @@ """Freeze a built chunk: keep what it painted, drop the widgets that painted it. The preview's cost is not rendering markdown, it is holding widgets. Textual's -arrange is linear in widget count, and a chunk is tens of them — measured 42 per +arrange is linear in widget count, and a chunk is tens of them: measured 42 per chunk on a real PDF. Freezing keeps the rendered result and discards the tree: arrange, 400 chunks widget trees 41.7ms frozen 2.1ms @@ -12,7 +12,7 @@ the match highlighting all survive because none of them are re-derived. Capture drives a ``Compositor`` directly. ``Widget.render_lines`` renders only a -widget's OWN content — children are composed over it by the Screen — so +widget's OWN content, children are composed over it by the Screen, so capturing a container that way yields strips that are styled but EMPTY, which looks exactly like the technique not working. ``Compositor.render_strips`` takes an explicit size, so a chunk taller than the terminal captures in full. @@ -47,7 +47,7 @@ class FrozenChunk: """What a built chunk painted, plus the positions the preview navigates by. The row fields are captured while the widgets still exist, which is the point: - resolving a match — or a table cell — against a live tree is what races today + resolving a match, or a table cell, against a live tree is what races today (a DataTable's cell region is unresolvable until its rows lay out). A row number recorded at capture time cannot race. """ @@ -77,12 +77,12 @@ def outer_height(self) -> int: def freeze(chunk: Widget, chunk_seq: int) -> FrozenChunk | None: - """Capture ``chunk`` — built and laid out — as strips plus positions. + """Capture ``chunk``, built and laid out, as strips plus positions. ``None`` means "not capturable, leave it live": either it is not laid out yet, or it contains a widget that scrolls INSIDE the chunk. A nested - viewport cannot be flattened onto a run of strips — the capture would hold - only the rows currently on screen — so such chunks stay as widgets. In + viewport cannot be flattened onto a run of strips (the capture would hold + only the rows currently on screen) so such chunks stay as widgets. In practice that is now nothing: Textual's DataTable used to cap itself at one viewport height, and lifting that (see FNDMarkdownTableDT) took the share of unfreezable chunks on a real corpus from 3.4% to zero. The guard stays @@ -96,7 +96,7 @@ def freeze(chunk: Widget, chunk_seq: int) -> FrozenChunk | None: return None # Refuse anything not actually being PAINTED. A hidden widget keeps its # geometry, so the size check above passes, but the compositor renders it - # blank — the capture then holds a correctly-sized run of empty strips and + # blank: the capture then holds a correctly-sized run of empty strips and # nothing downstream can tell. The background fill hides widgets while it # works and restores them in a `finally` that runs AFTER the freeze sweep, # so this is reachable on every file whose tail is filled that way, and it @@ -116,7 +116,7 @@ def freeze(chunk: Widget, chunk_seq: int) -> FrozenChunk | None: node = node.parent if isinstance(node.parent, Widget) else None for dt in chunk.query(DataTable): # A table that holds rows but has NO geometry has not been laid out, and - # capturing it yields the border with none of the contents — an empty + # capturing it yields the border with none of the contents: an empty # box, indistinguishable downstream from a table that is genuinely # empty. The nested-scroll check below cannot see this: an unlaid table # measures 0 both ways, so `virtual > size` is `0 > 0` and passes. @@ -132,7 +132,7 @@ def freeze(chunk: Widget, chunk_seq: int) -> FrozenChunk | None: if isinstance(chunk, FNDMarkdown): inner = chunk.first_match_block # The chunk's own spec, so this asks exactly what enumerate_stop_rows - # asks of the live blocks — a block whose spans were cleared after it + # asks of the live blocks: a block whose spans were cleared after it # registered would otherwise get a scanned row live and row 0 frozen. spec = chunk.match_spec if inner is not None and (block_row := row_within(inner, chunk)) is not None: @@ -211,8 +211,8 @@ class FrozenChunkView(Widget): """One widget standing in for a frozen chunk's whole widget tree. Deliberately one widget PER CHUNK rather than one per document. Everything - the preview addresses is per chunk — ``chunk_widgets``, ``match_targets``, - the scroll strategy, lazy mount, prune — and all of it keeps working if a + the preview addresses is per chunk: ``chunk_widgets``, ``match_targets``, + the scroll strategy, lazy mount, prune, and all of it keeps working if a chunk stays a widget and merely stops being a *tree* of them. Measured at 400 chunks: 41.7ms arrange today, 2.1ms this way, 0.04ms as a single document-wide widget. The last is another 50x, but it needs every one of @@ -231,7 +231,7 @@ class WidthStale(Message): Posted by the view because the view is the only thing that knows. The pane-level resize hook runs from ``call_after_refresh``, which fires - BEFORE the re-layout — measured, the pane still reports its old width + BEFORE the re-layout: measured, the pane still reports its old width and every chunk its old size at that point, so nothing looks stale from there and no repair ever ran. A widget's own Resize arrives after it has been laid out, with the new size in hand. @@ -242,7 +242,7 @@ def __init__(self, view: FrozenChunkView) -> None: self.view = view def on_resize(self, event: Resize) -> None: - # Strips cannot be re-wrapped, so this cannot repair itself — it can + # Strips cannot be re-wrapped, so this cannot repair itself: it can # only say so, and let the presenter decide. # # Once per width, not once per event: a window drag emits a resize per @@ -258,7 +258,7 @@ def on_resize(self, event: Resize) -> None: def __init__(self, frozen: FrozenChunk, **kwargs: object) -> None: super().__init__(**kwargs) # type: ignore[arg-type] self.frozen = frozen - # A fixed height, equal to what the widget tree occupied — so swapping + # A fixed height, equal to what the widget tree occupied, so swapping # the tree for this moves nothing on screen and needs no scroll # compensation, unlike removing the chunk outright. The strips are the # chunk's whole box, padding rows included, so this widget carries no @@ -275,7 +275,7 @@ def __init__(self, frozen: FrozenChunk, **kwargs: object) -> None: def adopt(self, frozen: FrozenChunk) -> None: """Swap in strips cut at a different width, in place. - A resize changes presentation, not content — so the fix is to replace + A resize changes presentation, not content, so the fix is to replace the strips rather than the widget. Nothing is unmounted, so the pane cannot blank and the mounted run cannot develop a hole. """ diff --git a/fnd/tui/preview/frozen_store.py b/fnd/tui/preview/frozen_store.py index f153458b..d27f7a62 100644 --- a/fnd/tui/preview/frozen_store.py +++ b/fnd/tui/preview/frozen_store.py @@ -1,6 +1,6 @@ """Per-chunk cache of captured previews, harvested from the mount pipeline. -Freezing already happens on every file the user reads — the backfill sweep +Freezing already happens on every file the user reads: the backfill sweep captures each chunk outside the visible window and drops its widget tree. Those captures used to be thrown away with the container, so a jump back to a match cost exactly as much as reaching it the first time. Keeping them turns a revisit @@ -12,7 +12,7 @@ set costs nothing. Captures are width-locked, so the key carries the width the strips were rendered -at — the width a chunk actually lays out at, which is the pane's scrollable +at: the width a chunk actually lays out at, which is the pane's scrollable content width and therefore moves when a scrollbar appears. A capture cut for one width can never be served at another, so a width change is a miss by design; the resize sweep drops what it can, and see `invalidate_captures_on_resize` for the @@ -37,8 +37,8 @@ _PLAIN = "\x00plain" # Share of system memory the preview cache may hold. A document viewer is -# expected to spend memory on the document — PDF readers and editors of this -# kind sit in the hundreds of MB routinely — so this is not trying to be frugal, +# expected to spend memory on the document: PDF readers and editors of this +# kind sit in the hundreds of MB routinely, so this is not trying to be frugal, # only to leave the machine usable. CACHE_FRACTION = 0.05 @@ -59,7 +59,7 @@ def _total_system_bytes() -> int | None: """Physical RAM, or ``None`` when it cannot be determined. Cached: the answer cannot change while the process runs, and it is read - once per stored capture — two syscalls per chunk on the sweep's hot loop. + once per stored capture, two syscalls per chunk on the sweep's hot loop. """ import os import sys @@ -122,7 +122,7 @@ class ChunkCaptureStore: """Captures held per chunk, so a mount can serve one instead of building it. Deliberately sparse rather than a contiguous run. A run stacks chunks into a - single row space and so must be contiguous — a gap silently shifts every + single row space and so must be contiguous: a gap silently shifts every position after it. This holds a sparse SET, because what coverage captures is the matches scattered through a file, and nothing here does row arithmetic: each capture is looked up by its own chunk_seq and mounted as its own widget. @@ -134,7 +134,7 @@ class ChunkCaptureStore: the query that cut it. Bounded in rows against the same machine-scaled budget, evicting whole files - oldest-first — never the file just written to, which is the one on screen. + oldest-first: never the file just written to, which is the one on screen. """ def __init__(self) -> None: @@ -150,7 +150,7 @@ def get(self, parent_id: str, query_sig: str, width: int, chunk_seq: int) -> Fro if capture is not None: # Promote on READ, not only on write. Without this the order is # purely write order, and coverage writes the current file FIRST and - # its neighbours after — making the file on screen the OLDEST entry + # its neighbours after, making the file on screen the OLDEST entry # and so the first one evicted. A cache that drops what you are # reading in order to hold what you have not opened is worse than no # cache; this makes "least recently used" mean what it says. @@ -162,7 +162,7 @@ def has(self, parent_id: str, query_sig: str, width: int, chunk_seq: int) -> boo Probing readiness is not using a capture. The results list asks this for every file twice a second to paint the warmth arrows, and answering - through :meth:`get` re-ordered the whole store on results-list order — + through :meth:`get` re-ordered the whole store on results-list order, neutralising the read-promotion above and leaving the file on screen, usually the top result, as the first eviction victim. """ @@ -222,8 +222,8 @@ def clear(self) -> None: def drop_other_widths(self, width: int) -> int: """Forget captures cut for a different width; return how many files went. - They can never be served — the key carries the width, so a lookup at the - current width simply misses them — but they would sit here holding + They can never be served: the key carries the width, so a lookup at the + current width simply misses them, but they would sit here holding strips and evicting captures that ARE usable. """ stale = [k for k in self._files if k[2] != width] @@ -232,6 +232,6 @@ def drop_other_widths(self, width: int) -> int: return len(stale) def drop_file(self, parent_id: str) -> None: - """Forget one file — its content or its highlighting changed.""" + """Forget one file: its content or its highlighting changed.""" for key in [k for k in self._files if k[0] == parent_id]: self._rows -= sum(c.outer_height for c in self._files.pop(key).values()) diff --git a/fnd/tui/preview/lazy_mount.py b/fnd/tui/preview/lazy_mount.py index 493e5f86..d619567e 100644 --- a/fnd/tui/preview/lazy_mount.py +++ b/fnd/tui/preview/lazy_mount.py @@ -2,7 +2,7 @@ ``LazyMounter`` watches viewport position (via a debounced check) and mounts the next batch of chunks when the user approaches the boundary -of the mounted region — long files behave like a continuous document +of the mounted region: long files behave like a continuous document without the initial mount paying for everything. """ @@ -59,7 +59,7 @@ def __init__(self, app: FNDApp) -> None: # interpret their own scroll changes as user intent and fire a # competing mount that yanks the focused chunk off-screen. # Debounce timer so rapid scroll bursts collapse to a single - # check at the tail end — protects programmatic intermediate + # check at the tail end: protects programmatic intermediate # scrolls AND smooths user wheel/key scroll bursts. self.check_timer: object | None = None @@ -90,7 +90,7 @@ def schedule_check(self, *, user_initiated: bool = False) -> None: timer; only the *last* scroll in a burst actually runs the check. Coalesces programmatic anchor scrolls (which fire one or two watcher trips back-to-back) AND user wheel/key bursts down - to a single check at the tail end — no fighting between the + to a single check at the tail end: no fighting between the navigation's own scroll-to-widget and lazy-mount's compensate.""" import contextlib @@ -111,7 +111,7 @@ def schedule_check(self, *, user_initiated: bool = False) -> None: # scroll, which commits immediately inside the reconcile window. if user_initiated and not self._app._preview.reconciling: self._app._match_nav.on_manual_scroll() - # Any scroll — a user wheel/key, a reveal, or a warm-nav result switch — + # Any scroll (a user wheel/key, a reveal, or a warm-nav result switch) # can move matches across the fold, so re-measure the ▲/▼ view markers. # Settle-gated inside, so it never reads regions mid cold-nav settle. self._app._match_nav.on_preview_scrolled() @@ -124,15 +124,15 @@ def check(self) -> None: """Scroll watcher entry point (after debounce). Mounts the next batch of chunks in the scroll direction when the viewport approaches a boundary of the chunk currently under it. Looks - at the next *unmounted* chunk in document order — so gaps left + at the next *unmounted* chunk in document order, so gaps left behind when the user jumps between matches get filled progressively, not just the chunks past the absolute max/min mounted index.""" self.check_timer = None # Suppress lazy-mount only while a navigation is still settling (the # controller owns the position until its scroll commits). Once the - # reveal lands the gate opens, so user scrolls by ANY means — keyboard - # OR an unfocused mouse-wheel — extend the window. Gating on is_armed + # reveal lands the gate opens, so user scrolls by ANY means, keyboard + # OR an unfocused mouse-wheel, extend the window. Gating on is_armed # instead dead-ended wheel-scroll: the anchor stays armed across navs # and only release() (a focused user scroll) cleared it. if self._app._preview_scroll.is_settling: @@ -149,7 +149,7 @@ def check(self) -> None: # owns the window and will hand off once it settles. Only until it has # PAINTED, though: the same task then goes on to fill and freeze in the # background, and waiting for that walled upward scrolling off for - # seconds — mounting nothing at all until the housekeeping was done. + # seconds, mounting nothing at all until the housekeeping was done. if self._app._preview.mount_before_first_paint(): return task = self.task @@ -172,7 +172,7 @@ def check(self) -> None: margin = float(tuning.LAZY_MOUNT_TRIGGER_MARGIN) # Snapshot mounted chunks' virtual-y ranges so we can find the - # widgets covering viewport top + bottom in O(mounted) — small + # widgets covering viewport top + bottom in O(mounted): small # under realistic mount counts. chunk_ranges: list[tuple[int, int, int]] = [] for idx in sorted(container.mounted_indices): @@ -201,14 +201,14 @@ def _covering(y: float) -> tuple[int, int, int] | None: # A fully captured file fills the rest of itself DOWNWARD in the # background; upward is left to the scroll below, one compensated batch # at a time. An unattended above-fill jitters the viewport for as long - # as it runs — the constraint the presenter states for its own fill. + # as it runs: the constraint the presenter states for its own fill. if ( len(chunks) <= tuning.FULLWARM_MOUNT_MAX_CHUNKS and self._app._preview.file_warm_state(container.parent_doc_id) is WarmState.FULL ): # Its OWN handle: `self.task` gates the user-driven fills, and a # background fill holding it dropped every upward request for as - # long as it ran — the reader hit a wall and had to scroll back + # long as it ran: the reader hit a wall and had to scroll back # down to shake one loose. fill = self.fill_task if fill is None or fill.done(): # type: ignore[attr-defined] @@ -219,7 +219,7 @@ def _covering(y: float) -> tuple[int, int, int] | None: bottom_cover = _covering(viewport_bottom - 1) or chunk_ranges[-1] bottom_idx, _, bottom_chunk_y1 = bottom_cover # Only fire below if the IMMEDIATE next chunk in document order - # is unmounted — otherwise the user is mid-region and can + # is unmounted, otherwise the user is mid-region and can # scroll on through the contiguous mounted span without a wall. next_idx = bottom_idx + 1 if ( @@ -252,7 +252,7 @@ def _covering(y: float) -> tuple[int, int, int] | None: and ((scroll_y - top_chunk_y0) <= margin or approaching) ): # Sized by what is served: a warmed file clears a wall in one step - # instead of three chunks at a time — one batch per scroll event, + # instead of three chunks at a time: one batch per scroll event, # never an unattended loop. self.task = asyncio.create_task( self._mount_batch( @@ -288,14 +288,14 @@ async def fill_all(self, container: PreviewContainer, chunks: list[FileChunk]) - return # BELOW only. Inserting content above the viewport shoves it down # and the scroll can only re-pin it a layout later, so an unattended - # above-fill jitters the reader for as long as it runs — measured at + # above-fill jitters the reader for as long as it runs: measured at # 19 of 21 painted frames showing the wrong part of the document. # Scrolling up stays user-driven, one compensated batch per scroll. # Strictly AFTER the last mounted chunk. "Missing and above the top # of the window" is not the same thing: mount_chunk_into inserts in # document order, so filling an interior gap puts widgets ABOVE the - # viewport while the below branch — which does no scroll adjustment, - # on the assumption content grows downward — displaces the reader + # viewport while the below branch, which does no scroll adjustment, + # on the assumption content grows downward, displaces the reader # once per batch. Interior gaps are left to the scroll, which knows # which side of the viewport they are on. below = [i for i in missing if i > max(mounted)] if mounted else missing @@ -311,7 +311,7 @@ async def fill_all(self, container: PreviewContainer, chunks: list[FileChunk]) - ) if len(container.mounted_indices) <= before: # A batch that mounts nothing would spin. Something declined the - # mount — a condemned widget, a swapped container — and the + # mount (a condemned widget, a swapped container) and the # windowed path can pick the rest up on scroll. return await asyncio.sleep(0) @@ -322,7 +322,7 @@ def _served_batch( """The larger batch only when this run really is served from the store. FULL is counted over the CAPTURABLE chunks, so a file of mostly - flat-path pages reads FULL while most of it still has to be built — + flat-path pages reads FULL while most of it still has to be built, and the served size is ten times what a build batch can afford. """ span = [start + step * i for i in range(tuning.LAZY_MOUNT_BATCH_SERVED)] @@ -347,7 +347,7 @@ async def _mount_batch( because content grows downward. Above: hidden-prepend, await build, reveal. No scroll - compensate — anchor preservation via virtual_region delta + compensate: anchor preservation via virtual_region delta proved unreliable post-reveal (returns 0 on consecutive above-batches even after refresh), so newly-prepended chunks appear at the top of the visible area, which IS the right UX @@ -386,7 +386,7 @@ async def _mount_batch( # them hidden, then reveal AND scroll-compensate so the user's view stays # put. The anchor is the first already-mounted chunk just below the # prepend region; revealing the chunks above it shifts it DOWN by their - # combined height, so we scroll the pane by that delta — turning the old + # combined height, so we scroll the pane by that delta: turning the old # "wall, jump, scroll-down-to-retrigger" into a continuous upward scroll. # Measuring the delta reliably needs a SETTLED layout: the earlier code # read the delta as 0 pre-settle and gave up on compensation, leaving the @@ -416,7 +416,7 @@ async def _mount_batch( for w in hidden: # Re-checked here as well as in the mount loop: this awaits per # widget, and a query landing in that window detaches the - # container — the reveal below then changes no geometry and the + # container: the reveal below then changes no geometry and the # growth claim would strand, to be spent on the next document. if self._app._preview.active is not container: return @@ -447,7 +447,7 @@ async def _mount_batch( if hidden and isinstance(pane, MatchAwareScroll) and anchor_w is not None: # Anchored on the first chunk BELOW the prepend, at its position # now. The claim has to outlive this call: `display = True` lays - # nothing out — the arrange happens on a later timer tick — so a + # nothing out (the arrange happens on a later timer tick) so a # claim released before returning was never present when the # growth it claimed arrived. pane.absorb_anchor = (anchor_w, int(anchor_w.virtual_region.y)) @@ -456,7 +456,7 @@ async def _mount_batch( if absorb_pane is not None: # Suspend painting across the reveal AND the layout it causes. # The compensation lands inside that layout, but the compositor - # can still paint the pre-correction offset first — the reader + # can still paint the pre-correction offset first: the reader # sees the document a batch-height out of place for one frame. # Batched, the first frame anyone sees is the corrected one. revealed = list(hidden) @@ -466,7 +466,7 @@ async def _mount_batch( hidden.clear() # Held until the prepend has finished landing. The # signal is the revealed widgets themselves: each is - # laid out — and so contributes its height — at its own + # laid out, and so contributes its height, at its own # pace, which is why the growth arrives in instalments # 40-57ms apart, and why releasing on a timer released # just before the last one and let it paint @@ -494,7 +494,7 @@ async def _mount_batch( # Re-anchor: scroll by however far the anchor moved down, so the # prepended chunks extend the scrollable region UPWARD without moving - # the user's view — continuous scroll instead of a wall. + # the user's view: continuous scroll instead of a wall. if anchor_w is not None and before_y is not None and anchor_seq is not None: await self._app._preview.await_settled() # Re-resolve across the await. The freeze sweep can swap this diff --git a/fnd/tui/preview/liveness.py b/fnd/tui/preview/liveness.py index f60f5b6c..fe9a2de7 100644 --- a/fnd/tui/preview/liveness.py +++ b/fnd/tui/preview/liveness.py @@ -4,17 +4,17 @@ posts a ``Prune`` message and sets ``_pruning``; only when that message is processed does ``on_prune`` close the widget's message loop and detach it. In between, the widget still reports a ``parent`` and still turns up in -``app.query()`` — it looks healthy and is already dead. +``app.query()``: it looks healthy and is already dead. The preview pipeline used to ask ``widget.parent is None`` at each seam, which is blind to that window, so a condemned container sailed through every guard: the stranded-container sweep condemns it, the DOM-scan adopt picks it back up, the mount sees a parent and skips its ``pane.mount()``, activates it, and the -queued ``Prune`` then tears it out — leaving ``active`` pointing at a widget +queued ``Prune`` then tears it out, leaving ``active`` pointing at a widget that is not in the tree. Blank pane, no self-heal. Re-attaching such a widget is not a fix either: its message loop is already closing. -So the rule the whole subsystem now shares: **a condemned widget is dead — +So the rule the whole subsystem now shares: **a condemned widget is dead, never adopt it, never activate it, never cache it. Build a fresh one.** """ @@ -29,8 +29,8 @@ def is_condemned(widget: Widget | None) -> bool: - """True when ``widget`` is being torn down — its removal is queued or its - message loop is closing — even though it may still report a parent.""" + """True when ``widget`` is being torn down: its removal is queued or its + message loop is closing, even though it may still report a parent.""" if widget is None: return False return bool( @@ -46,7 +46,7 @@ def is_live(widget: Widget | None) -> bool: Stronger than ``widget.parent is not None`` in two ways that both bite here: it rejects a widget whose ``Prune`` is merely queued (see module - docstring), and — because ``is_attached`` walks to the DOM root — it also + docstring), and, because ``is_attached`` walks to the DOM root, it also rejects one whose *ancestor* was removed, e.g. a container still parented to a ``#preview_pane`` that a screen teardown has already detached. """ diff --git a/fnd/tui/preview/match_row.py b/fnd/tui/preview/match_row.py index 082e471f..72db3bbd 100644 --- a/fnd/tui/preview/match_row.py +++ b/fnd/tui/preview/match_row.py @@ -3,12 +3,12 @@ A block's top row and its match's row are different numbers as soon as the block wraps: measured on a PDF contents page, a 63-row paragraph carries its match on row 32, so anchoring on the block top drops the match a screenful below the fold. -A block taller than the viewport also holds more than one match — a 166-row -fence carried 12 — so the stop set needs every row, not just the first. +A block taller than the viewport also holds more than one match (a 166-row +fence carried 12) so the stop set needs every row, not just the first. -Both substrates resolve the rows here — the live scroll +Both substrates resolve the rows here: the live scroll (:mod:`fnd.tui.preview_scroll`) and the frozen capture -(:mod:`fnd.tui.preview.frozen`) — so a chunk lands in the same place whether or +(:mod:`fnd.tui.preview.frozen`), so a chunk lands in the same place whether or not it has been captured yet. """ @@ -58,7 +58,7 @@ def _expanded_col(marks: list[tuple[int, int, int]], offset: int) -> int: def region_at_row(region: Region, row: int) -> Region: - """``region`` trimmed to begin ``row`` rows down — a block's match rather + """``region`` trimmed to begin ``row`` rows down: a block's match rather than the block itself.""" if row <= 0: return region @@ -66,7 +66,7 @@ def region_at_row(region: Region, row: int) -> Region: def block_plain(widget: Widget) -> str | None: - """A widget's rendered text — ``_content`` for every ``MarkdownBlock``, and + """A widget's rendered text: ``_content`` for every ``MarkdownBlock``, and ``.code`` for the non-block descendants ``_fallback_match_target`` scans.""" try: plain = widget._content.plain # type: ignore[attr-defined] @@ -102,7 +102,7 @@ def _rows_for_offsets(plain: str, offsets: list[int], width: int, height: int) - neither model reproduces ``height``. Wrapping only ADDS rows, so a height equal to the source-line count proves - nothing wrapped — and the unwrapped model costs nothing against 4.9ms for + nothing wrapped, and the unwrapped model costs nothing against 4.9ms for the wrap engine on a paragraph at the structural build cap. Every offset is resolved in the one walk, so a block with many matches costs the same ``divide_line`` pass as a block with one. @@ -157,7 +157,7 @@ def _match_rows(block: Widget, spec: MatchSpec | None) -> list[int]: if not offsets: return [] # Cached per block: ``enumerate_stop_rows`` asks every mounted match block - # on every n/b press — 56 wrapped 4,000-char paragraphs cost 13.8ms cold and + # on every n/b press: 56 wrapped 4,000-char paragraphs cost 13.8ms cold and # 0.03ms after. Geometry is in the key, so a re-wrap recomputes. key = (content.width, content.height, len(plain), tuple(offsets)) cached: tuple[object, list[int] | None] | None = getattr(block, "_fnd_match_row_cache", None) @@ -174,7 +174,7 @@ def _match_rows(block: Widget, spec: MatchSpec | None) -> list[int]: def rows_to_first_match(block: Widget, spec: MatchSpec | None = None) -> int: """Rendered rows from ``block``'s top down to its first match's row, or - ``0`` when that cannot be established — the block's top is the safe anchor.""" + ``0`` when that cannot be established: the block's top is the safe anchor.""" rows = _match_rows(block, spec) return rows[0] if rows else 0 diff --git a/fnd/tui/preview/prefetch.py b/fnd/tui/preview/prefetch.py index d7f4f0ff..47419dfc 100644 --- a/fnd/tui/preview/prefetch.py +++ b/fnd/tui/preview/prefetch.py @@ -74,10 +74,10 @@ def prefetch_top_results(self, *, anchor_parent_id: str | None = None) -> None: Parallelism bounded by ``preview_decode_workers``. ``anchor_parent_id`` centres the window around the cursor's position - in the result list instead of starting from the top — lets the + in the result list instead of starting from the top: lets the buffer follow the user when they navigate past the initial range. """ - # Discard mount jobs queued for the previous anchor — stale + # Discard mount jobs queued for the previous anchor: stale # work would otherwise keep the drainer (and the asyncio loop) # busy across navigation. q = self.sink_queue @@ -132,7 +132,7 @@ def prefetch_top_results(self, *, anchor_parent_id: str | None = None) -> None: # Filter by preview_cache (widget tree ready), not chunk_cache: # a file whose chunks are cached but whose mount got drained # by a prior cursor move must be re-queued. Also skip if it's - # the active preview — that one's owned by the user-side path. + # the active preview: that one's owned by the user-side path. in_preview = ( self._app._preview.preview_cache.get(g.parent_id, query_sig_for_filter) is not None ) @@ -166,9 +166,9 @@ def prefetch_top_results(self, *, anchor_parent_id: str | None = None) -> None: pane_widget = self._app.query_one("#preview_pane", VerticalScroll) # Floor of 20 mirrors the cold-load + md-flat paths. Without # it, if the pane isn't laid out at prefetch time (content - # width 0–1) every flat-path file gets pre-rendered to + # width 0 to 1) every flat-path file gets pre-rendered to # 1-cell strips and paints as a single vertical column on - # first reveal — the "PDF only shows a single line" symptom. + # first reveal: the "PDF only shows a single line" symptom. measured = pane_widget.content_size.width - 1 estimated_wrap_width = max(20, measured) if measured > 0 else 0 except Exception: @@ -180,7 +180,7 @@ def _prefetch_one(parent_id: str, focus_seq: int) -> None: import time as _time t0 = _time.perf_counter() - # Reuse cached chunk data if present — only the mount got dropped, + # Reuse cached chunk data if present: only the mount got dropped, # not the decode. Avoids re-running PDF/docx extraction. cached_chunks = app._preview.chunk_cache.get(parent_id) if cached_chunks is not None: @@ -238,7 +238,7 @@ def _prefetch() -> None: with ThreadPoolExecutor(max_workers=workers) as ex: futures = [ex.submit(_prefetch_one, pid, focus) for pid, focus in targets] for f in as_completed(futures): - # Drop everything on query change — _run_query has cleared + # Drop everything on query change: _run_query has cleared # caches and any in-flight work here is stale. if query_sig != app._search.query_signature(): for other in futures: @@ -300,7 +300,7 @@ async def _mount_flat_async( doc: RenderedDocument, focus_chunk_seq: int, ) -> None: - """Stash the prefetched RenderedDocument in the value cache. No mount — + """Stash the prefetched RenderedDocument in the value cache. No mount: user activation installs into the shared widget on click.""" _ = focus_chunk_seq # focus is recomputed at install time if query_sig != self._app._search.query_signature(): @@ -326,13 +326,13 @@ def mount_structural( No-op while the preview cache holds a single file. The pre-mount only pays off if the container it builds is still cached when the user gets there, and ``PREVIEW_CACHE_MAX_FILES == 1`` means the very next ``put`` - evicts it — so every container built here is thrown away unused. It was + evicts it, so every container built here is thrown away unused. It was not free: measured on a real corpus, one Down inside a single file re-mounted the same four *neighbouring files* on every keypress (13 prefetch passes over 11 presses, each of the four rebuilt 12-13 times), all of it on the event loop the navigation's own scroll is waiting on. - The decode and the flat bundles above are kept — those are cached by + The decode and the flat bundles above are kept: those are cached by ``chunk_cache`` / ``prebuilt_cache``, which are not bounded to one file and do survive to be used. """ @@ -529,7 +529,7 @@ async def drain_sinks(self) -> None: job = await q.get() self._app._diag_log(f"drainer JOB pulled qsize={q.qsize()}") wait_iters = 0 - # Cooperative wait — user-side mount always preempts. + # Cooperative wait: user-side mount always preempts. while self._app._preview.user_mount_in_flight(): wait_iters += 1 await asyncio.sleep(0.05) diff --git a/fnd/tui/preview/presenter.py b/fnd/tui/preview/presenter.py index dfde6da8..3ce312de 100644 --- a/fnd/tui/preview/presenter.py +++ b/fnd/tui/preview/presenter.py @@ -65,7 +65,7 @@ class _CoveragePass: plan: list[str] width: int width_now: Callable[[], int] - #: The plan as it stands NOW — what the cursor has moved to since. + #: The plan as it stands NOW, what the cursor has moved to since. live_plan: Callable[[], list[str]] @@ -74,8 +74,8 @@ def target_from_node_data(data: Any) -> tuple[str, int] | None: the preview should be showing for it, or None if it isn't a result row. The single definition of that mapping. It used to be written out three - times — here, in :meth:`PreviewPresenter.rerender_current`, and in - ``FNDApp._load_result_node`` — with the loader deciding what to mount + times: here, in :meth:`PreviewPresenter.rerender_current`, and in + ``FNDApp._load_result_node``, with the loader deciding what to mount through one copy and the settle-time paint check deciding what *should* be mounted through another. A divergence between the copies (a new node ``kind``, a different fallback for an empty ``hits`` list) would have them @@ -100,7 +100,7 @@ async def decode_abandonable(fn: Callable[..., Any], *args: Any) -> Any: """Run ``fn`` off the loop on a thread the process may exit without. ``asyncio.to_thread`` uses the loop's default executor, and the loop drains - that executor before the process can exit — so quitting while a neighbour's + that executor before the process can exit, so quitting while a neighbour's chunks are being decoded means waiting for a decode whose result is about to be thrown away. Measured on a real corpus: 0.46s to quit with no neighbour covering against 0.77s with it. @@ -122,7 +122,7 @@ def _relay(target_loop: Any, settle_cb: Any, apply: Any) -> None: """Hand the result back, or drop it if the loop has gone. The thread is deliberately not joined so a quit need not wait for a - decode — but that is exactly the case where the loop is closed before + decode, but that is exactly the case where the loop is closed before the decode finishes, and an unguarded `call_soon_threadsafe` then raises on a thread with no handler, so `threading.excepthook` prints a traceback onto the terminal Textual has just restored. Nothing is lost @@ -187,7 +187,7 @@ def __init__(self, app: FNDApp) -> None: # cannot express. See fnd/tui/preview/coverage.py. self.capture_store = ChunkCaptureStore() self._warm_host = WarmHost(app) - # Convenience aliases that point into the active container — + # Convenience aliases that point into the active container: # legacy code paths (_scroll_preview_to_chunk, etc.) read from # these instead of poking at the container directly. # Widgets here may be either per-line ``Static``s (PDF / TXT @@ -205,14 +205,14 @@ def __init__(self, app: FNDApp) -> None: self.mount_task: object | None = None # The in-flight chunk-decode worker, if any. A cold load cancels the # mount task and the debounce timer before handing off to this worker, - # so without tracking it the pipeline looks idle for the whole decode — + # so without tracking it the pipeline looks idle for the whole decode: # precisely while the pane is still (deliberately) showing the previous # file. See pipeline_busy. self.decode_worker: object | None = None # Prebuilt flat-buffer bundles keyed by (parent_id, query_sig). - # Cleared on query change — highlight spans are baked in at build time. + # Cleared on query change: highlight spans are baked in at build time. self.prebuilt_cache: dict[tuple[str, str], RenderedDocument] = {} - # Debounced preview load — latest target + Timer. + # Debounced preview load: latest target + Timer. from typing import Any as _Any self.load_timer: _Any | None = None @@ -224,7 +224,7 @@ def __init__(self, app: FNDApp) -> None: self.pending_heading: int = -1 # True while the cursor is moving via Option/Alt + arrow ("scan" mode): # schedule_load skips its leading fire so fast browsing doesn't mount - # every row — the preview only loads once the sweep settles (trailing). + # every row: the preview only loads once the sweep settles (trailing). # Set by the app's alt-arrow handler, cleared on a load or a plain move. self._scan_move: bool = False # The (parent_id, focus_chunk_seq) of the render currently in @@ -238,7 +238,7 @@ def __init__(self, app: FNDApp) -> None: # Bumped on every full preview reset (new query / scope clear / highlight # rerender). A mount task captures it at start; if it has moved by the # time the task's `finally` runs, that reset cleared the caches + DOM - # under it — so the task must NOT resurrect its now-stale container + # under it, so the task must NOT resurrect its now-stale container # (re-cache it / leave it mounted). cancel_mount_task only *requests* # cancellation; the finally runs a tick later and would otherwise race # the reset and re-pollute it ("stuck mid-mount after a new query"). @@ -291,7 +291,7 @@ def is_painted(self) -> bool: and carrying displayed content.""" # A pane showing neither a container nor a flat buffer is genuinely # unpainted; anything else here would rebuild a preview that is on - # screen and correct — measured at 19 spurious rebuilds in a + # screen and correct: measured at 19 spurious rebuilds in a # 14-navigation run, each one clearing the cache and re-dispatching. buf = self._app._flat.active_buffer if buf is not None: @@ -314,16 +314,16 @@ def showing_parent(self) -> str | None: def pipeline_busy(self) -> bool: """True while a decode / load / mount / finalise for the preview is - still in flight — i.e. the pane not yet showing the cursor's file is + still in flight: i.e. the pane not yet showing the cursor's file is expected right now, not a strand.""" if self.load_timer is not None: return True # The decode is the easiest one to miss: a cold load cancels the mount # task and the debounce timer BEFORE handing the file to this worker, so - # all the other signals read idle for its whole duration — while the pane + # all the other signals read idle for its whole duration, while the pane # is still deliberately showing the previous file. Left untracked, the # check would call that a strand and spend its one repair, and the repair - # re-enters render_full_doc, whose worker group is exclusive — restarting + # re-enters render_full_doc, whose worker group is exclusive: restarting # the very decode that was about to finish. worker = self.decode_worker if worker is not None: @@ -345,7 +345,7 @@ def pipeline_busy(self) -> bool: def cursor_target(self) -> tuple[str, int] | None: """The (parent_id, focus_chunk_seq) the results cursor currently points - at — the file the preview is supposed to be showing.""" + at: the file the preview is supposed to be showing.""" try: tree = self._app.query_one("#results_pane", Tree) except Exception: @@ -385,14 +385,14 @@ def _verify_painted(self) -> None: exists to catch. The question is always the same one the user is asking: *is the pane showing the file I have selected?*""" self._paint_check = None - # Option/Alt scan mode moves the cursor WITHOUT loading, by design — + # Option/Alt scan mode moves the cursor WITHOUT loading, by design: # a divergence here is the feature working, not a strand. if self._scan_move: return target = self.cursor_target() if target is None: - return # no selection (empty results, non-result node) — nothing owed - # Still legitimately working (a big file can mount for a while) — look + return # no selection (empty results, non-result node): nothing owed + # Still legitimately working (a big file can mount for a while): look # again rather than pre-empting a healthy navigation. Bounded, so a # wedged pipeline still gets its one repair. if self.pipeline_busy() and self._paint_check_rearms < tuning.PAINT_CHECK_MAX_REARMS: @@ -445,7 +445,7 @@ def _landing_heading(self, parent_id: str, focus_chunk_seq: int) -> int: return -1 def _drop_stale_landing_intent(self, parent_id: str, focus_chunk_seq: int) -> None: - """Forget a request the reader has navigated away from — the end of the + """Forget a request the reader has navigated away from: the end of the navigation it was set for, and the only thing that clears it. An outline jump never outlives the next load that did not ask for it.""" pending = self.pending_landing_intent @@ -477,7 +477,7 @@ def schedule_load( self.pending_heading = heading if self._scan_move: # Option/Alt+arrow scan: browse the results without mounting anything. - # Record where the cursor is but DON'T load and DON'T arm a timer — so + # Record where the cursor is but DON'T load and DON'T arm a timer, so # even pausing mid-sweep never mounts. The preview loads only when the # user presses a normal key (the app clears _scan_move, and the next # schedule_load takes the leading-edge path below). Release detection @@ -501,7 +501,7 @@ def schedule_load( self._app._lazy.cancel() # The cancelled mount will never reach settle to clear the in-flight # coalescing latch. If that latched target differs from where the - # cursor is now heading, drop it — otherwise returning to it later + # cursor is now heading, drop it, otherwise returning to it later # (an overshoot-and-correct sweep) hits the dedup guard as "already # in flight" and the remount is suppressed, stranding the preview # mid-mount until an unrelated nav resets the latch. @@ -521,7 +521,7 @@ def schedule_load( self.fire_pending_load() return if self.load_timer is None: - # Leading edge: the cursor was settled, so load NOW — a deliberate + # Leading edge: the cursor was settled, so load NOW; a deliberate # single jump shouldn't wait out the window (that wait was ~150ms of # every nav's perceived lag). Then open a coalescing window so a rapid # arrow-sweep that follows only loads its FINAL row (the trailing @@ -553,7 +553,7 @@ def fire_pending_load(self) -> None: # With the debounce pinned to 0 these don't merge; the 2nd+ then # warm-resume and cancel the 1st's still-building mount, orphaning # the focus chunk's build_done and losing the match scroll. If the - # exact same render is already in flight, skip — it will land it. + # exact same render is already in flight, skip: it will land it. if self.inflight_target == (parent_id, focus_chunk_seq): return self.inflight_target = (parent_id, focus_chunk_seq) @@ -604,7 +604,7 @@ def render_full_doc(self, parent_id: str, *, focus_chunk_seq: int) -> None: Hybrid load: 1. Look up the cached :class:`PreviewContainer` for this file + - query. If complete, activate (O(1) class flip) and scroll — + query. If complete, activate (O(1) class flip) and scroll: done. 2. If partial (resume case), activate, scroll, dispatch a task that mounts only the un-mounted indices. @@ -619,7 +619,7 @@ def render_full_doc(self, parent_id: str, *, focus_chunk_seq: int) -> None: _perf.mark("click_to_display_start", parent_id=parent_id, focus_seq=focus_chunk_seq) - # Any pending debounce timer is now moot — we're committing to + # Any pending debounce timer is now moot: we're committing to # a load. Cancel so a late-firing timer can't race the current # dispatch and clobber it with a stale target. self.cancel_pending_load() @@ -632,7 +632,7 @@ def render_full_doc(self, parent_id: str, *, focus_chunk_seq: int) -> None: # own scroll, so call order can no longer change where the preview lands. # Glide smoothly only when the target match is ALREADY mounted (the # content between is on screen, so the scroll is over real rows). A - # fresh file — or a same-file match outside the mounted window — is + # fresh file, or a same-file match outside the mounted window, is # revealed via an atomic swap (cut) instead: animating over an unmounted # gap would be lumpy, and prepending an out-of-window window above the # current match slides the view (reflow). Consistent rule: glide when @@ -654,19 +654,19 @@ def render_full_doc(self, parent_id: str, *, focus_chunk_seq: int) -> None: ) ) # One progress session spans the whole navigation, opened here because - # arming is the single event every navigation passes through — so the + # arming is the single event every navigation passes through, so the # line is up before any of the work below starts. The tracker samples # this pipeline and closes the session once the match has landed; no # stage below has to remember to hide anything. # Suppressed: the line is cosmetic, and this sits between arming the # scroll anchor and arming the paint check. A raise here would strand - # the preview with an armed anchor and no repair timer — a decorative + # the preview with an armed anchor and no repair timer: a decorative # subsystem must never be able to do that. with contextlib.suppress(Exception): self._app._nav_progress.begin(parent_id) # Re-plan what to capture ahead around the NEW cursor position. Driven # from here rather than from the mount because most navigations never - # reach a mount — a target already on screen returns early — and those + # reach a mount (a target already on screen returns early) and those # are precisely the moments with time to spare for capturing ahead. # Suppressed for the same reason as the progress session above: this # sits between arming the scroll anchor and arming the paint check, and @@ -676,26 +676,26 @@ def render_full_doc(self, parent_id: str, *, focus_chunk_seq: int) -> None: self.start_coverage(parent_id, focus_chunk_seq) except Exception as exc: # Suppressed but SAID. A raise here would leave the preview with an - # armed anchor and no repair timer, so it cannot propagate — but a + # armed anchor and no repair timer, so it cannot propagate, but a # genuine bug in coverage would then disable warming for the # session with nothing recorded. Same policy as WarmHost.ensure. self.diag_log(f"coverage failed to start: {exc!r}") # Every navigation is checked once it should have settled (see - # _arm_paint_check) — the single place that verifies the OUTCOME rather + # _arm_paint_check): the single place that verifies the OUTCOME rather # than one mechanism, so no seam can strand the pane indefinitely. self._arm_paint_check() # Refresh the preview's border indicator for the NEW target. Arming is # the one event common to every navigation; the indicator was otherwise # driven only by a document mount (``refresh_match_scrollbar`` → # ``MatchNavigator.rebuild``) and by scroll events, so stepping between - # section rows of the SAME file refreshed nothing — and a row whose + # section rows of the SAME file refreshed nothing, and a row whose # chunk has no match doesn't even scroll, so the border kept displaying # the previous row's state. self._app.call_after_refresh(self._app._refresh_preview_match_indicator) chunks = self.chunk_cache.get(parent_id) if chunks is not None: - # We have decoded data — go to the mount path. If the + # We have decoded data: go to the mount path. If the # prefetch worker (or an earlier load) has already built # the flat-path bundle for this (file, query) pair, pass # it through so the dispatcher skips the main-thread @@ -738,7 +738,7 @@ def render_full_doc(self, parent_id: str, *, focus_chunk_seq: int) -> None: # paint) ``_rebuild_after_layout`` will catch it. try: pane_widget = self._app.query_one("#preview_pane", VerticalScroll) - # Floor of 20 — see prefetch _prefetch_top_results for the + # Floor of 20: see prefetch _prefetch_top_results for the # PDF-single-column rationale. measured = pane_widget.content_size.width - 1 estimated_wrap_width = max(20, measured) if measured > 0 else 0 @@ -756,9 +756,9 @@ def _load() -> None: except Exception as e: app.call_from_thread(app._preview.on_load_failed, e, decode_token) return - # For the flat-buffer path (PDF / TXT) the FileView build — + # For the flat-buffer path (PDF / TXT) the FileView build, # which computes per-chunk match spans and stitches the - # global line buffer — and the per-line ``rich.Console.render`` + # global line buffer, and the per-line ``rich.Console.render`` # pass are pure-Python data work that easily dominate the # main-thread cost on large documents. Do both here in the # worker so the UI stays responsive during the decode + @@ -771,7 +771,7 @@ def _load() -> None: wrap_width = estimated_wrap_width if estimated_wrap_width > 0 else 0 # Report the render as it goes. This is the only countable # work the flat path exposes, and its duration varies by - # more than 10x with nothing observable predicting it — so + # more than 10x with nothing observable predicting it, so # the progress line is told rather than left to guess. decode_progress.begin(decode_token, len(fv.lines)) prebuilt = build_rendered_document( @@ -805,7 +805,7 @@ def sweep_stranded_containers(self, *, keep: PreviewContainer | None = None) -> Must run on EVERY navigation, not only cross-file ones. The same-file out-of-window path builds a fresh container and returns early, and used - to rely on "the old container is swept on the next navigation" — which + to rely on "the old container is swept on the next navigation", which never happened while the user stayed inside one file, the common case in a large document. Measured over 30 in-file navigations: 23 containers, 270 mounted chunks and 14,551 widgets left in the pane, with navigation @@ -820,7 +820,7 @@ def sweep_stranded_containers(self, *, keep: PreviewContainer | None = None) -> # ``outgoing`` is the previous file, deliberately held on screen so # the incoming one can build behind it and swap in without a blank # frame. It is neither active nor cached, so it looks exactly like a - # strand — removing it mid-swap would blank the pane for the whole + # strand: removing it mid-swap would blank the pane for the whole # build. if ( stranded in cached @@ -874,7 +874,7 @@ def prune_active_to_window(self, margin: int = 3) -> float: if i >= len(chunks): # Unreachable today: mounted_indices is built from THIS # _chunk_cache list, each file decodes once per query, and a - # query change clears the chunk AND preview caches together — so + # query change clears the chunk AND preview caches together, so # a cached container always matches its chunks. Cheap crash-guard # only, in case that coupling is ever broken; there is no live # stale state here to normalise. @@ -887,7 +887,7 @@ def prune_active_to_window(self, margin: int = 3) -> float: vr = w.virtual_region # type: ignore[attr-defined] ranges.append((i, w, float(vr.y), float(vr.y + vr.height))) except Exception: - return 0.0 # geometry not ready — skip rather than risk a bad scroll + return 0.0 # geometry not ready: skip rather than risk a bad scroll visible = [i for (i, _w, y0, y1) in ranges if y1 > vtop and y0 < vbot] if not visible: return 0.0 @@ -903,7 +903,7 @@ def prune_active_to_window(self, margin: int = 3) -> float: if not to_remove: return 0.0 # Removal is DEFERRED by Textual, so compensating here scrolls against a - # virtual size that still counts the chunks — measured 439-680 rows out. + # virtual size that still counts the chunks: measured 439-680 rows out. # The anchor is the reader's own content: absorbing its movement needs no # prediction, and prices the frozen stand-ins for free. The topmost # VISIBLE chunk: a claim above the fold prices nothing below itself. @@ -928,8 +928,8 @@ def prune_active_to_window(self, margin: int = 3) -> float: # Freeze rather than discard where we can. Removing the chunk # frees its widgets but throws away the render, so scrolling back # to it rebuilds from markdown; a frozen stand-in keeps what it - # painted for one widget instead of ~42, and — because it is - # given the exact height the tree occupied — swapping it in moves + # painted for one widget instead of ~42, and, because it is + # given the exact height the tree occupied: swapping it in moves # nothing on screen, so it needs none of the scroll compensation # below. captured = freeze(w, seq) if freeze_on_prune else None @@ -948,7 +948,7 @@ def prune_active_to_window(self, margin: int = 3) -> float: container.match_targets.pop(seq, None) frozen_count += 1 # display:none leaves the arrange immediately; remove() then frees - # it. (Keeping ~1000s of display:none widgets alive is worse — they + # it. (Keeping ~1000s of display:none widgets alive is worse; they # still get walked by settle and inflate the next mount.) with contextlib.suppress(Exception): w.display = False @@ -997,7 +997,7 @@ def dispatch_mount( query_sig = self._app._search.query_signature() # Same file + same query already active. Two sub-cases: - # (a) target chunk widget exists — just scroll, no remount. + # (a) target chunk widget exists: just scroll, no remount. # (b) target chunk not yet mounted (still loading the file # and the user clicked a result above the load front): # cancel the in-flight task and resume the SAME container @@ -1022,7 +1022,7 @@ def dispatch_mount( ) return # Same-file, target match OUTSIDE the mounted window. Resuming the - # SAME container would mount the new window in document order — + # SAME container would mount the new window in document order, # which, for an upward jump, prepends above the current match and # slides the visible content (the "flash wrong content, then land" # reflow). Instead build a FRESH container at the new focus and @@ -1035,7 +1035,7 @@ def dispatch_mount( self.hide_progress_bar() # Sweep and prune BEFORE building the replacement. This branch used # to do neither, on the assumption that the old container would be - # "swept on the next navigation" — but the sweep lives on the + # "swept on the next navigation", but the sweep lives on the # cross-file path below, which this early return never reaches. Stay # inside one file (the common case in a large document) and nothing # ever reclaimed anything: 30 navigations left 23 containers, 270 @@ -1062,7 +1062,7 @@ def dispatch_mount( self.cancel_mount_task() # Option C hardening: the outgoing file may be FULL-mounted (~1000s of # widgets). It stays on screen while the incoming file builds, and - # Textual's arrange scales with total DOM — so a big outgoing container + # Textual's arrange scales with total DOM, so a big outgoing container # inflates the new file's mount several-fold. Prune it to its visible # window now (flash-free) so the incoming mount is cheap. self.prune_active_to_window() @@ -1073,12 +1073,12 @@ def dispatch_mount( # ``remove()`` is deferred (the widget keeps a parent and stays in # ``query()`` until its Prune is processed) the adopt happily picked up # a container the sweep had just condemned. The mount then saw a parent, - # skipped its ``pane.mount()``, built into it and activated it — and the + # skipped its ``pane.mount()``, built into it and activated it, and the # queued Prune detached it: ``active`` left pointing outside the tree, # pane blank, nothing to heal it. Choosing first, then sweeping # everything except the choice, makes that self-condemnation impossible. cached = self.preview_cache.get(parent_id, query_sig) - # A cache entry that is no longer LIVE is not a valid hit — that covers + # A cache entry that is no longer LIVE is not a valid hit: that covers # both an already-detached widget and one whose removal is merely queued # (see fnd/tui/preview/liveness.py). Purge it and rebuild fresh. if cached is not None and not is_live(cached): @@ -1087,7 +1087,7 @@ def dispatch_mount( del self.preview_cache._cache[key] cached_containers.discard(cached) # Don't leave self.active dangling on the purged container through - # the async rebuild — a fresh one is coming. + # the async rebuild: a fresh one is coming. if self.active is cached: self.active = None self.diag_log(f"cache hit not live parent={parent_id[:8]}, rebuilding fresh") @@ -1098,7 +1098,7 @@ def dispatch_mount( # ``_prefetch_task``, live or done). Prefetch re-anchors on the # cursor and continually mounts/evicts/removes its own containers # for the hot file; adopting one for a user mount races that - # removal — the adopted widget is detached mid-mount and the + # removal: the adopted widget is detached mid-mount and the # finalise reveals a zero-region ghost (blank pane). Build a # fresh, user-owned container instead, which prefetch won't touch # (and eviction can't drop while it is the protected active one). @@ -1211,13 +1211,13 @@ def dispatch_flat_mount( ) -> None: """Flat-buffer mount: resolve doc (cache > prebuilt > main-thread build), install into the shared widget, activate.""" - # Stop the structural pipeline we are replacing — the symmetric step the + # Stop the structural pipeline we are replacing: the symmetric step the # structural branch of dispatch_mount does for itself. Without it an # in-flight structural mount kept building and, on completion, # ``activate_container`` hid every LineBufferPreview and showed its own # container: the preview displayed the file the user had just navigated - # away from. ``schedule_load``'s navigate-away cancel doesn't cover this - # — it compares against ``self.active``, which the flat path sets to + # away from. ``schedule_load``'s navigate-away cancel doesn't cover this: + # it compares against ``self.active``, which the flat path sets to # None, so with a flat preview on screen there is nothing to compare. self.cancel_mount_task() self._app._lazy.cancel() @@ -1247,7 +1247,7 @@ def dispatch_flat_mount( # matched but carries no highlightable span (e.g. a tree-rebuild # cursor echo lands on chunk 0). Scrolling there parks the view at # the chunk's top with nothing highlighted, and a second racing - # dispatch then clobbers the correct match scroll — last writer + # dispatch then clobbers the correct match scroll: last writer # wins, non-deterministically. Resolve to the file's first matching # chunk so every dispatch for this (file, query) lands on the same # match regardless of arrival order. Genuine match chunks (real @@ -1276,7 +1276,7 @@ def dispatch_flat_mount( self._app._flat.installed_key = cache_key self._app._flat.activate(buf) # Route the flat match scroll through the controller: arm with the - # resolved focus chunk and reconcile (idempotent — re-applies the + # resolved focus chunk and reconcile (idempotent: re-applies the # install's scroll; for intra-file nav it IS the scroll). The 25% # context margin matches the structural path. self._app._preview_scroll.arm( @@ -1309,7 +1309,7 @@ def show_progress_bar( These three methods no longer drive the progress line. A navigation's session is opened once by :meth:`render_full_doc` and advanced by :class:`fnd.tui.progress.operations.PreviewProgressTracker`, which reads - this pipeline's own signals — so a stage that forgets to call these can + this pipeline's own signals, so a stage that forgets to call these can no longer strand the line, and a stale one can no longer retire a successor's. The arguments are kept because the sixteen call sites are deliberately left as they are; they describe work the tracker measures @@ -1331,7 +1331,7 @@ def hide_progress_bar(self) -> None: def update_progress_bar(self, progress: int) -> None: """No-op. The tracker reads ``mounted_indices`` itself, and against the - mount window rather than the file's chunk count — the old denominator + mount window rather than the file's chunk count: the old denominator that left the line reading ~1% on a thousand-chunk PDF.""" del progress @@ -1376,7 +1376,7 @@ def activate_container( ) -> None: """Make ``container`` the only visible preview. With ``pre_reveal=True`` the container is laid out but invisible - (opacity:0) until the scroll lands — no flash to file-top before + (opacity:0) until the scroll lands; no flash to file-top before the jump-to-match. With ``keep_outgoing=True`` the previously-active container stays visible (so the pane never blanks) until the atomic reveal swaps to ``container`` (see :meth:`_swap_reveal_target`).""" @@ -1386,7 +1386,7 @@ def activate_container( # Hold the outgoing preview on screen while the incoming one builds # invisibly; the reveal swap hides it and shows the new one in one tick. # Only keep a genuinely-visible prior container (not one left invisible - # by a superseded mount) — otherwise the pane would blank anyway. + # by a superseded mount), otherwise the pane would blank anyway. prior = self.active outgoing = ( prior @@ -1456,7 +1456,7 @@ async def _reattach_active_if_detached(self, container: PreviewContainer) -> Non if container is not self.active or is_live(container): return if is_condemned(container): - # Its message loop is already closing — ``pane.mount()`` cannot + # Its message loop is already closing: ``pane.mount()`` cannot # revive it, and the queued Prune would tear it out again. Drop the # active reference instead so the reveal no-ops and the settle-time # paint check (:meth:`_verify_painted`) rebuilds from scratch. @@ -1526,8 +1526,8 @@ def active_flat_buffer(self) -> LineBufferPreview | None: def invalidate_captures_on_resize(self) -> None: """Drop captures that no longer match the width chunks lay out at. - Captured strips cannot be re-wrapped — re-deriving them means rebuilding - the markdown tree, which is the cost capturing avoids — so a width change + Captured strips cannot be re-wrapped: re-deriving them means rebuilding + the markdown tree, which is the cost capturing avoids, so a width change invalidates them. Height-only changes must NOT invalidate: a vertical resize leaves every @@ -1536,7 +1536,7 @@ def invalidate_captures_on_resize(self) -> None: Written to be idempotent and timing-independent rather than to fire once on the "right" tick. ``call_after_refresh`` can still read the PRE-layout - width — measured, it does — so a one-shot "has the width changed since + width (measured, it does) so a one-shot "has the width changed since last time?" guard latches the old value and then never fires again. Dropping everything that does not match the width measured right now is correct whenever it runs, and harmless to run repeatedly. @@ -1554,13 +1554,13 @@ def invalidate_captures_on_resize(self) -> None: # It runs on a terminal resize and on the Reading View toggle. A scrollbar # appearing inside the pane changes the width too and does not fire this, # so captures taken before the bar appeared are left unreachable until - # the next real resize. They are a miss rather than a fault — the wrong - # width can never be served — but they do hold row budget. + # the next real resize. They are a miss rather than a fault (the wrong + # width can never be served) but they do hold row budget. self.capture_store.drop_other_widths(width) # The MOUNTED captures are repaired elsewhere, and deliberately: this # runs from `call_after_refresh`, which fires before the re-layout, so # nothing looks stale from here. Each `FrozenChunkView` reports its own - # staleness once it has been laid out — see `on_stale_strips`. + # staleness once it has been laid out: see `on_stale_strips`. def on_stale_strips(self) -> None: """A frozen chunk is painting strips cut for a width it no longer has. @@ -1612,7 +1612,7 @@ async def _repair_stale_strips(self, attempt: int = 0) -> None: # # `virtual_region`, never `region`: region is relative to the SCREEN and # is NULL_REGION for anything the compositor has culled, so every - # off-screen chunk reads as y=0 — which sorted culled chunks ahead of + # off-screen chunk reads as y=0, which sorted culled chunks ahead of # visible ones and made the drift predicate count nine chunks BELOW the # fold as above it, scrolling the pane 18 rows for nothing. # `prune_active_to_window` uses virtual_region for the same reason. @@ -1629,7 +1629,7 @@ async def _repair_stale_strips(self, attempt: int = 0) -> None: # Bound the pass to the window. Re-capturing the whole mounted set cost # 349 captures and 13.8s for one twelve-column drag; pruning first and # repairing what survives costs 25 and 0.76s. Lazy mount refills the - # rest at the current width when the user scrolls there — the same path + # rest at the current width when the user scrolls there: the same path # that already serves never-mounted chunks. `prune_active_to_window` is # scroll-compensated and flash-free, and a no-op on small files. if len(stale) > tuning.VISIBLE_FIRST_ABOVE + tuning.VISIBLE_FIRST_BELOW: @@ -1637,7 +1637,7 @@ async def _repair_stale_strips(self, attempt: int = 0) -> None: with contextlib.suppress(Exception): pruned = self.prune_active_to_window() # `is_live`, not `is_mounted`: Textual defers removal, so a widget - # the prune just dropped still reports itself mounted — measured, + # the prune just dropped still reports itself mounted: measured, # 46 of 46 pruned widgets passed `is_mounted`, so this filter # removed nothing and the pass re-captured the whole stale set, # which is the cost the prune exists to avoid. @@ -1646,8 +1646,8 @@ async def _repair_stale_strips(self, attempt: int = 0) -> None: ] # And move the viewport sample with what the prune removed above # the fold: the chunk positions below read post-removal, so leaving - # the sample where it was classifies chunks at the fold as above it - # — measured, the drift correction came out as the growth of the + # the sample where it was classifies chunks at the fold as above it, + # measured, the drift correction came out as the growth of the # whole document, 126 rows applied where 9 was right. viewport_top = max(0, pane.scroll_offset.y - int(pruned)) @@ -1659,12 +1659,12 @@ async def _repair_stale_strips(self, attempt: int = 0) -> None: held = None # `reconciling` is NOT held across the loop. It gates lazy mount's # release of the scroll anchor and the match-nav burst reset, so holding - # it for a pass — measured at 2.8s, and 6s on heavy content — means a + # it for a pass (measured at 2.8s, and 6s on heavy content) means a # genuine user scroll does neither. That is the shape of the lazy-load # dead-end an uncleared gate produced before. The adopts do not scroll; # only the correction below does. for seq, view in stale: - # Abandon the moment anything the pass assumed stops holding — the + # Abandon the moment anything the pass assumed stops holding: the # width moved again mid-drag, the user navigated, the query changed. if ( self.active is not container @@ -1681,7 +1681,7 @@ async def _repair_stale_strips(self, attempt: int = 0) -> None: capture = await self._warm_host.capture(chunk, width, match_spec=spec) if capture is None: # Clear the latch. A view reports once per width, so leaving - # it set means this chunk never asks again — measured with + # it set means this chunk never asks again: measured with # injected failures, 55 chunks stayed cropped and a round # trip to another width and back did not clear them. `freeze` # refuses on purpose (unlaid DataTable, nested scroll), so @@ -1691,7 +1691,7 @@ async def _repair_stale_strips(self, attempt: int = 0) -> None: self.capture_store.put(parent_id, query_sig, capture.width, capture) before = view.frozen.outer_height # A chunk above the viewport moves what the reader is looking at; - # accumulate and correct once, less what the claim absorbs — it + # accumulate and correct once, less what the claim absorbs: it # prices what moves ITS anchor, so that much would land twice. bottom = view.virtual_region.y + before absorbed_above = None if held is None else held.virtual_region.y @@ -1713,9 +1713,9 @@ async def _repair_stale_strips(self, attempt: int = 0) -> None: if repaired: self.diag_log(f"stale strips repaired={repaired} width={width} drift={drift}") # Re-arm explicitly. Reports arriving while a pass runs are dropped and a - # view reports once per width, so without this a pass that abandoned — + # view reports once per width, so without this a pass that abandoned, # including one that abandoned BEFORE its first success, which is why - # this is not gated on `repaired` — would leave the tail cropped until + # this is not gated on `repaired`: would leave the tail cropped until # the next width change or navigation. # # Termination comes from the attempt bound rather than from progress: a @@ -1737,19 +1737,19 @@ def swap_reveal_target( self, target: Widget, margin: int, anchor_region: Region | None = None ) -> bool: """Atomic preview swap: hide the outgoing container, position the - incoming one so ``target`` sits ``margin`` rows down, and reveal it — + incoming one so ``target`` sits ``margin`` rows down, and reveal it: all in one tick. Returns True when a swap happened, False when there is no outgoing container (the caller then scrolls + reveals normally). The outgoing container stayed on screen through the whole build, so the first frame the user sees after this is the new preview already at its - match — no blank, no scroll-into-place. The anchor's offset is taken + match: no blank, no scroll-into-place. The anchor's offset is taken relative to the incoming container's top, which is scroll-independent and so survives the outgoing container leaving the layout. ``anchor_region`` (screen space) overrides ``target.region`` when given: a match inside a table renders as one full-height DataTable, so the - matched cell is not its own widget — the caller resolves the cell's + matched cell is not its own widget; the caller resolves the cell's region and passes it here so the swap lands on the matched row, not the table top.""" outgoing = self.outgoing @@ -1765,13 +1765,13 @@ def swap_reveal_target( # Replace the claim, which is usually held on a chunk of the OUTGOING # container: once that container leaves the layout its widgets report # ``virtual_region`` y=0, and the pane absorbs the difference as real - # movement — measured 117-187 rows backwards. + # movement, measured 117-187 rows backwards. anchor = self._app._preview_scroll.anchor self._reseat_absorb_claim(new, anchor.focus_chunk_seq if anchor else None) self.diag_log(f"scroll site=swap y={target_y}") set_preview_visibility(new, pre_reveal=False) new.has_painted = True - # Same event as reveal()'s, on the atomic-swap path — see the note there. + # Same event as reveal()'s, on the atomic-swap path: see the note there. self.diag_log(f"first_paint parent={new.parent_doc_id[:8]} path=swap") self.outgoing = None return True @@ -1791,12 +1791,12 @@ def _reseat_absorb_claim(self, container: PreviewContainer, seq: int | None) -> def reveal(self, container: PreviewContainer) -> None: """Reveal ``container`` and drop any still-held outgoing preview. Fallback for paths where :meth:`swap_reveal_target` did not run (no - match resolved, or no outgoing) — a no-op for the class already lifted + match resolved, or no outgoing); a no-op for the class already lifted by the swap. Guard: a finalise/reveal callback is queued via ``call_after_refresh`` and runs a tick later. If a newer navigation superseded this mount in - the meantime, ``container`` is no longer ``_active_preview`` — revealing + the meantime, ``container`` is no longer ``_active_preview``: revealing it would surface the wrong file and clobber the new nav's outgoing reference. Detached finalise tasks aren't cancelled, so this staleness check (not task cancellation) is the single point that makes a @@ -1810,14 +1810,14 @@ def reveal(self, container: PreviewContainer) -> None: set_preview_visibility(container, pre_reveal=False) container.has_painted = True # The moment the new result becomes visible. Logged because it is the - # only exact answer to "when did the user first see this?" — inferring + # only exact answer to "when did the user first see this?": inferring # it by diffing captured frames measures how MUCH of the pane changed, # not when it changed, and mis-ranks any design that paints early and # fills in behind it. self.diag_log(f"first_paint parent={container.parent_doc_id[:8]}") - # Revealed — the bounded-time backstop is no longer needed. + # Revealed: the bounded-time backstop is no longer needed. self._cancel_reveal_watchdog() - # The new result is now positioned — re-measure the ▲/▼ view markers. + # The new result is now positioned: re-measure the ▲/▼ view markers. # This is the authoritative switch event: it fires even when the reveal # scroll doesn't move (which the scroll-watcher trigger would miss, # leaving the previous result's markers stale). @@ -1828,7 +1828,7 @@ def reveal_active(self) -> None: """Invariant backstop: the active container must not stay ``-pre-reveal`` (invisible) once its navigation has settled. Reveal is normally driven by one specific finalise task, but rapid navigation can cut that task short - before it reveals — and the scroll-only resume path + before it reveals, and the scroll-only resume path (:meth:`_settled_instant_scroll`) never reveals at all. Either leaves the active container built-but-invisible: the "preview blank until I select a different result and come back" strand. Calling this whenever a @@ -1874,7 +1874,7 @@ async def _finalise_via_lock( The body reveals by scheduling ``reconcile`` at its very end; if rapid navigation CANCELS this task at any earlier await (build/settle wait), it never gets there and the still-active container would stay ``-pre-reveal`` - (invisible) — the strand. The ``finally`` backstop reveals the active + (invisible): the strand. The ``finally`` backstop reveals the active container in that cut-short case so it can't stay hidden. On the normal path the body returns having scheduled the reveal, so the backstop is a no-op (and never reveals early, preserving the no-flash-at-top scroll).""" @@ -1946,7 +1946,7 @@ async def _finalise_via_lock_body( # ``display=False`` widgets take no part in the arrange, so they cannot # move the match and there is nothing to wait for. Skipping them is also # load-bearing for ``await_match_settled``, which requires every watched - # height to be > 0 — a hidden widget measures 0 forever and would hold + # height to be > 0: a hidden widget measures 0 forever and would hold # the settle to its round limit. above_widgets: list[FNDMarkdown] = [ w @@ -1966,7 +1966,7 @@ async def _finalise_via_lock_body( # says the markdown rendered; the compositor's arrange (which fixes every # chunk's region AND the pane's scroll extent) runs over several more # refreshes. The old region.height>0 poll only checked the focus chunk - # and raced the chunks above it still flowing — so a deep match scrolled + # and raced the chunks above it still flowing, so a deep match scrolled # against a half-settled layout and clamped off-screen. _await_preview_settled # is Textual's own message-drain signal (what Pilot waits on): it returns # only once every widget has processed its pending layout, so the geometry @@ -1979,7 +1979,7 @@ async def _finalise_via_lock_body( ) import os as _os - # Option B: scoped settle is the default — wait only on the geometry the + # Option B: scoped settle is the default, wait only on the geometry the # scroll reads (focus + above-window heights), not a full-pane drain. # _FND_FULL_SETTLE=1 restores the old behaviour as an escape hatch. if _os.environ.get("_FND_FULL_SETTLE") == "1": @@ -2003,10 +2003,10 @@ def _reveal_when_landed() -> None: # instead of a zero-region ghost (blank pane). await self._reattach_active_if_detached(container) # Scroll while the container is still invisible (opacity:0), then reveal - # once it lands — so the match never flashes at the file top first. The + # once it lands, so the match never flashes at the file top first. The # layout is settled, so this is a single deterministic scroll. self._app._preview_scroll.reconcile(_reveal_when_landed) - # This render has settled — release the in-flight coalescing + # This render has settled: release the in-flight coalescing # latch so a later genuine re-render of the same target can run. self.inflight_target = None self._app._diag_log( @@ -2057,7 +2057,7 @@ def _reveal_when_landed() -> None: f"compose_done={compose_done}" ) - # Wait for the screen to fully settle, THEN scroll once + reveal — same + # Wait for the screen to fully settle, THEN scroll once + reveal: same # deterministic settle the cold path uses (see _finalise_via_lock). The # warm reveal is sync, so run the await in a task. import asyncio as _asyncio @@ -2069,7 +2069,7 @@ async def _settled_reconcile() -> None: await self._reattach_active_if_detached(container) self._app._preview_scroll.reconcile(_reveal_when_landed) - # Cancel a prior settle-await on this container before replacing it — a + # Cancel a prior settle-await on this container before replacing it: a # rapid re-nav would otherwise leave it running, burning a full DOM-drain # and a redundant (generation-guarded) reconcile. Safe to cancel: this # task does no cleanup, so CancelledError just unwinds the await. Held on @@ -2085,7 +2085,7 @@ async def await_settled(self, max_rounds: int = 10) -> None: Drain = Textual's own settle mechanism (what ``Pilot.pause`` / ``_wait_for_screen`` use): schedule a callback on every widget via - ``call_later`` and wait for them all to fire — i.e. every widget has + ``call_later`` and wait for them all to fire, i.e. every widget has processed the messages queued now. One drain settles the current wave; the reflow it triggers posts a follow-up wave, so loop until the screen reports no pending layout/repaint/recompose (its ``_on_idle`` condition), @@ -2108,14 +2108,14 @@ async def await_settled(self, max_rounds: int = 10) -> None: reason = "no_screen" return # Drain only what bears on the preview's geometry: the app + screen - # (which run the arrange) and the preview pane's own subtree — NOT the + # (which run the arrange) and the preview pane's own subtree, NOT the # whole screen (results tree, sidebars), which is irrelevant here and # makes the per-round callback count scale with the unrelated DOM. try: pane = self._app.query_one("#preview_pane", VerticalScroll) children = [self._app, screen, *pane.walk_children(with_self=True)] except Exception: - # No pane yet — fall back to the screen-wide drain. + # No pane yet: fall back to the screen-wide drain. children = [self._app, *screen.walk_children(with_self=True)] count = 0 done = asyncio.Event() @@ -2138,7 +2138,7 @@ def _dec(_done: asyncio.Event = done) -> None: except TimeoutError: reason = "timeout" return - # Stop once the screen has no pending layout work — the geometry is + # Stop once the screen has no pending layout work: the geometry is # now final. (These are the flags Screen._on_idle itself checks.) if not ( getattr(screen, "_layout_required", False) @@ -2163,11 +2163,11 @@ async def await_match_settled( above_widgets: list[FNDMarkdown], max_rounds: int = 12, ) -> None: - """Option B — targeted settle. The full-pane drain waits for the WHOLE + """Option B: targeted settle. The full-pane drain waits for the WHOLE screen to stop reflowing; but the only geometry the scroll reads is the focus chunk's virtual_y, which is fixed once the above-window chunk heights stop changing. So drain only [app, screen, focus, above] and - exit when those heights are stable for two consecutive rounds — far + exit when those heights are stable for two consecutive rounds: far fewer callbacks/round than walking every block in the pane, and an earlier exit than the screen-global flags allow. Stability is judged on the SPECIFIC heights that move the match, not a generic region poll, so @@ -2180,7 +2180,7 @@ async def await_match_settled( _t0 = _time.perf_counter() watch: list[Widget] = [w for w in [header, *above_widgets] if w is not None] - # Nothing to track (no focus/above widgets, or no resolvable match) — + # Nothing to track (no focus/above widgets, or no resolvable match): # fall back to the full-pane drain rather than scroll against unknown # geometry. The scoped path only buys us anything when there ARE heights # to watch settle. @@ -2190,7 +2190,7 @@ async def await_match_settled( try: screen = self._app.screen except Exception: - return # no screen (teardown / transition) — nothing to settle + return # no screen (teardown / transition): nothing to settle targets = [self._app, screen, *watch] # App + Screen + widgets all have call_later def _sig() -> tuple[int, ...]: @@ -2252,7 +2252,7 @@ async def _settled_instant_scroll( self, container: PreviewContainer, parent_id: str, focus_chunk_seq: int ) -> None: """Option C: the target chunk is already mounted, so scroll straight to - it — but settle the focus + nearest-above heights first (cheap, ~2 rounds + it, but settle the focus + nearest-above heights first (cheap, ~2 rounds when the file is idle) so heavy table/fence geometry is final and the match lands on-screen instead of clamping off.""" from fnd.tui import _perf @@ -2265,14 +2265,14 @@ async def _settled_instant_scroll( await self.await_match_settled(header, above) _perf.mark("click_to_display_end", parent_id=parent_id, path="already_active_scroll_only") # Reveal-on-settle: this resume path is taken for an already-mounted - # target, which is USUALLY already visible — but if a prior rapid-nav + # target, which is USUALLY already visible, but if a prior rapid-nav # cancel left it ``-pre-reveal`` (invisible), scrolling alone would strand # it. ``reveal_active`` lifts it once the scroll lands (no-op otherwise). self._app._preview_scroll.reconcile(self.reveal_active) # Re-freeze whatever this navigation left live outside the new window. # The sweep after the initial fill is not enough on its own: lazy mount # and each subsequent navigation mount more chunks live, so DOM creeps - # back over a reading session — measured 402 widgets after the fill, + # back over a reading session, measured 402 widgets after the fill, # 805 after twenty navigations. Freezing has to recur for the same # reason the fill does. await self._refreeze_around(container, focus_chunk_seq) @@ -2289,7 +2289,7 @@ async def _refreeze_around(self, container: PreviewContainer, focus_chunk_seq: i # was the first cut and it buys nothing: both policies settle at the same # DOM (263 widgets on a 99-chunk file), because each navigation re-freezes # whatever the last one left behind. Freezing the window as well simply - # gets there sooner — 401 widgets after the initial fill against 688. + # gets there sooner: 401 widgets after the initial fill against 688. win_start = focus_idx win_end = focus_idx + 1 await self._freeze_chunks_outside_window(container, chunks, win_start, win_end) @@ -2297,7 +2297,7 @@ async def _refreeze_around(self, container: PreviewContainer, focus_chunk_seq: i # run on cross-file dispatch and the same-file rebuild alone, so a # container stranded by an earlier navigation survived indefinitely while # the user stayed in one file: measured twelve in-window navigations - # later, one strand still held 6 live chunks and 169 widgets — more DOM + # later, one strand still held 6 live chunks and 169 widgets, more DOM # than the 92 frozen chunks of the file actually being read. self.sweep_stranded_containers() @@ -2306,7 +2306,7 @@ def bump_reset_generation(self) -> None: preview caches + DOM (new query, scope clear, highlight rerender): the cancelled mount's deferred `finally` sees the changed generation and drops its now-stale container instead of re-caching it. Navigation / - resume paths that cancel a mount but keep the cache must NOT call this — + resume paths that cancel a mount but keep the cache must NOT call this: their partial container is still wanted for a later resume.""" self.reset_generation += 1 self._supersede_decode() @@ -2317,7 +2317,7 @@ def bump_reset_generation(self) -> None: self.pending_landing_intent = None # Stop warming immediately rather than at the next batch boundary. Its # captures carry the OLD query's highlighting, so every chunk it builds - # from here is work whose result can never be served — the store key + # from here is work whose result can never be served: the store key # carries the query signature, so it would be stored and never read. # Captures for a superseded query can never be served; holding them # only spends the row budget that the new query's captures need. The @@ -2332,7 +2332,7 @@ def bump_reset_generation(self) -> None: # cancelling a warm the user cannot see. self._full_warm_target = None self._retire_full_warm_session(completed=False) - # And any unconsumed prepend claim — the container it was made against + # And any unconsumed prepend claim: the container it was made against # is about to be replaced, and the next document's mount would be # absorbed as though it were content added above the reader. with contextlib.suppress(Exception): @@ -2399,7 +2399,7 @@ def on_chunks_loaded( # rendering. Same key as ``_flat_buffer_cache``. self.prebuilt_cache[(parent_id, self._app._search.query_signature())] = prebuilt if not chunks: - # Empty file — hide bar, leave pane blank. + # Empty file: hide bar, leave pane blank. self.hide_progress_bar() self.parent_id = parent_id self._app._refresh_status() @@ -2419,8 +2419,8 @@ def mount_before_first_paint(self) -> bool: """A mount that has not yet put anything on screen. Narrower than ``user_mount_in_flight`` on purpose. A mount task stays - alive well past the reveal — it still has the background fill and the - freeze sweep to do — and treating that whole tail as "a mount is + alive well past the reveal (it still has the background fill and the + freeze sweep to do) and treating that whole tail as "a mount is happening" is what walls scroll-driven lazy mount off for seconds at a time, so scrolling up through a file mounts nothing until the housekeeping finishes. Slicing the sweep lengthened that tail and made @@ -2447,7 +2447,7 @@ def above_window_start(self, chunks: list[FileChunk], focus_idx: int, viewport_h Counted in ROWS, not chunks. Everything mounted above the focus has to finish building before the match can be revealed, so this is the part of - the mount the user waits on — and a fixed chunk count prices it wrongly + the mount the user waits on, and a fixed chunk count prices it wrongly for every format at once. A PDF chunk is a page (30-60 rows), so seven of them is several screens of content nobody asked for; a markdown chunk is one heading's section (often 2-3 rows), so seven of them is less than the @@ -2455,7 +2455,7 @@ def above_window_start(self, chunks: list[FileChunk], focus_idx: int, viewport_h So: walk up until roughly a screenful of content is covered, bounded by ``VISIBLE_FIRST_ABOVE`` chunks either way. Estimated from the source line - count rather than measured geometry — nothing is laid out yet, and the + count rather than measured geometry: nothing is laid out yet, and the estimate only has to be good enough to stop one chunk early or late. """ rows_wanted = max(1, int(viewport_h * tuning.VISIBLE_FIRST_ABOVE_SCREENS)) @@ -2492,7 +2492,7 @@ async def _freeze_chunks_outside_window( ``freeze`` rightly refuses it. Attempting it inline failed on all 72 chunks of a real file. - The visible window is left live — those chunks are being read, and the + The visible window is left live: those chunks are being read, and the focused one is what the scroll resolves against. On by default; ``_FND_NO_FREEZE=1`` opts out. Measured on a real @@ -2523,7 +2523,7 @@ async def _freeze_chunks_outside_window( # Never capture from a container that is not being PAINTED. The incoming # container builds invisibly (`-pre-reveal`, opacity 0) and is revealed # only once its scroll lands, so a sweep that runs before the reveal - # captures correctly-sized, completely BLANK strips — and nothing + # captures correctly-sized, completely BLANK strips, and nothing # downstream can tell, because the geometry is right. That is the # end-of-file blank: the tail is captured pre-reveal, then served empty. # Wait for the reveal; a later landing re-freezes via _refreeze_around. @@ -2554,7 +2554,7 @@ async def _freeze_chunks_outside_window( break slice_start = time.perf_counter() # Only the VISIBLE window stays live. Skipping everything before - # win_end — as the first cut did — left every chunk ABOVE the focus + # win_end, as the first cut did, left every chunk ABOVE the focus # live too, which is most of them on a file the user has read down # through: 11 live chunks holding 329 widgets against 72 frozen # holding 72. @@ -2574,11 +2574,11 @@ async def _freeze_chunks_outside_window( continue # Keep it. This capture used to die with the container, so a jump # outside the mounted window rebuilt chunks that had already been - # built and captured moments earlier — which is why revisiting a + # built and captured moments earlier, which is why revisiting a # match cost exactly as much as visiting it the first time. # # Keyed on the width chunks lay out at, which is what these strips - # were cut at — they come off an on-screen widget. The 63-against-64 + # were cut at: they come off an on-screen widget. The 63-against-64 # gap this comment used to blame on container padding was the # scrollbar: PreviewContainer has no padding, and `capture_width` # accounts for the bar. @@ -2647,7 +2647,7 @@ def stop_background_work(self) -> None: """Cancel everything this presenter runs in the background. Called on app exit. Without it, coverage keeps capturing while Textual - is trying to shut down — and since a capture holds the event loop for + is trying to shut down, and since a capture holds the event loop for the whole of its build, the quit has to wait its turn. Measured on a real corpus: 0.49s to quit with coverage off against 1.09s with it on, of which pacing the captures recovered about half and this the rest. @@ -2660,7 +2660,7 @@ def navigation_order(self) -> list[tuple[str, int]]: """The targets Down/Up actually walk, in order. Neighbours used to be taken from ``_search.groups``, which is the - RANKING order — but the user moves through the results TREE, where a + RANKING order, but the user moves through the results TREE, where a file's hits are expanded inline and collapsed nodes are skipped. The two orders diverge, so coverage warmed files the cursor never reached: measured, the store held four files while every lookup asked for a @@ -2709,12 +2709,12 @@ def start_coverage(self, parent_id: str, focus_chunk_seq: int) -> None: with a debounce and then waits out the landing, so arrows arriving every few hundred milliseconds killed each attempt before it captured anything. Measured over a full navigation session, `mount served=0 - built=53` — the capture path was dead exactly when it was needed. + built=53`: the capture path was dead exactly when it was needed. So a navigation only moves the ANCHOR. The runner finishes the pass it is on, then picks up wherever the cursor has got to, and keeps going until the anchor stops moving. Cancellation is reserved for the case - where the work is genuinely worthless — a new query, handled by + where the work is genuinely worthless: a new query, handled by ``bump_reset_generation``. """ import os as _os @@ -2726,8 +2726,8 @@ def start_coverage(self, parent_id: str, focus_chunk_seq: int) -> None: # `cancelling()` as well as `done()`: cancel() only REQUESTS # cancellation and the task does not settle until a later loop # iteration, so a caller that cancels and restarts in the same - # synchronous block — `rerender_current` does exactly that via - # `bump_reset_generation` — saw a task that was neither done nor + # synchronous block: `rerender_current` does exactly that via + # `bump_reset_generation`, saw a task that was neither done nor # replaced, and coverage stayed dead until the next navigation. That is # the moment the cache most needs refilling, because the same call just # emptied it. @@ -2750,7 +2750,7 @@ def request_full_warm(self, parent_id: str, focus_chunk_seq: int) -> None: async def full_warm_estimate(self, parent_id: str) -> tuple[int, int]: """Chunks a whole-file warm would capture, and their character total. - Zero means the off-screen builder can take nothing here — a wholly + Zero means the off-screen builder can take nothing here: a wholly flat-path file, which the capture store never serves. """ chunks = await self._coverage_chunks(parent_id) @@ -2763,7 +2763,7 @@ def cancel_full_warm(self, parent_id: str) -> bool: Named rather than global: the key toggles, and an unnamed cancel meant pressing it on a second file stopped the first and warmed neither. - Clearing the target is the whole abort — every capture is preceded by + Clearing the target is the whole abort: every capture is preceded by the test that reads it, so the walk returns through its ordinary path within one capture. """ @@ -2820,8 +2820,8 @@ async def _run_coverage(self, parent_id: str, focus_chunk_seq: int) -> None: """Capture what navigation will reach for next, in priority order. Captures land in ``capture_store``, never in the pane: the mounted set - has to stay CONTIGUOUS — lazy mount only fills at its edges, so a hole - in the middle would be a stretch of the document silently missing — + has to stay CONTIGUOUS, lazy mount only fills at its edges, so a hole + in the middle would be a stretch of the document silently missing, while what navigation needs covered is the matches, scattered right through the file and across its neighbours. A capture costs 44.5 KB and no arrange time, which is what lets the cache hold a set the DOM cannot. @@ -2834,7 +2834,7 @@ async def _run_coverage(self, parent_id: str, focus_chunk_seq: int) -> None: return # Settle first. Coverage is armed at the START of a navigation and # re-planned on every one, so a held-down arrow key would otherwise - # spawn a plan per keypress — each decoding neighbouring files off the + # spawn a plan per keypress: each decoding neighbouring files off the # loop before being cancelled by the next. Sleeping first means a # superseded plan does NO work at all: the cancel lands here. await asyncio.sleep(tuning.PREVIEW_WARM_DELAY) @@ -2848,7 +2848,7 @@ async def _run_coverage(self, parent_id: str, focus_chunk_seq: int) -> None: return # WAIT for a measurable width rather than giving up on one. Coverage is # armed at the START of a navigation, which on a cold start is before - # the pane has been laid out — and since nothing re-triggers it until + # the pane has been laid out, and since nothing re-triggers it until # the cursor moves again, returning here meant the first file of a # session was never covered at all. width = 0 @@ -2914,7 +2914,7 @@ def width_now() -> int: if here >= 0: plan += neighbour_order(ids, here, tuning.COVERAGE_NEIGHBOUR_FILES) # Then the head of the result list, where the opening moves of a - # search are — appended, so it never delays the files beside the + # search are: appended, so it never delays the files beside the # cursor. plan += ids[: tuning.COVERAGE_SEED_FILES] seen_plan: set[str] = set() @@ -2929,7 +2929,7 @@ def width_now() -> int: live_plan=current_plan, ) # Landings everywhere, then the file the user asked for whole, - # then context — see fnd/tui/preview/coverage.py for why, and for + # then context: see fnd/tui/preview/coverage.py for why, and for # what taking them in any other order cost. captured += await self._cover_plan(ctx, margin=0) if (wanted := self._full_warm_target) is not None: @@ -2963,7 +2963,7 @@ def warm_states(self) -> dict[str, WarmState] | None: query signature and the pane width once amortises what would otherwise be two lookups per row. - Answered from the results list and the store, with no decode — a hit + Answered from the results list and the store, with no decode: a hit carries its chunk_seq, which is exactly the store's key. Same property that lets `_file_needs_coverage` re-plan on every cursor move. """ @@ -2975,7 +2975,7 @@ def warm_states(self) -> dict[str, WarmState] | None: if width <= 0: # Before first layout there is no width, so every store lookup # would miss. None means "cannot answer", which is NOT the same as - # an empty map meaning "no files" — conflating them let a caller + # an empty map meaning "no files": conflating them let a caller # treat unavailable as authoritative. return None groups = getattr(self._app._search, "groups", None) or [] @@ -3045,7 +3045,7 @@ def listed_hit_seqs(self, parent_id: str) -> list[int] | None: The distinction matters: an empty list means "listed, nothing to jump to", which is READY. None means the question does not apply, and - collapsing the two reported READY — the cheapest possible plan — for + collapsing the two reported READY (the cheapest possible plan) for any file the results do not contain. The same set ``_file_needs_coverage`` asks about and the same one the @@ -3085,7 +3085,7 @@ def _file_needs_coverage(self, parent_id: str, query_sig: str, width: int) -> bo return False def _landing_index(self, parent_id: str, chunks: list[FileChunk]) -> int: - """Where a jump INTO this file would land — its first hit AS LISTED. + """Where a jump INTO this file would land: its first hit AS LISTED. As listed, not the earliest in the document. The results tree adds a file's hits in ``group.hits`` order, which is by score, so arriving at a @@ -3093,7 +3093,7 @@ def _landing_index(self, parent_id: str, chunks: list[FileChunk]) -> int: document order, and taking its first element pointed coverage at the top of the file: measured on a 1018-chunk PDF, the store held thirteen captures at the right width and query while every lookup asked for chunk - 679 and missed. Neighbour warming captured real chunks the whole time — + 679 and missed. Neighbour warming captured real chunks the whole time: just never the ones navigation would ask for. """ groups = getattr(self._app._search, "groups", None) or [] @@ -3126,7 +3126,7 @@ async def _coverage_chunks(self, parent_id: str) -> list[FileChunk]: A neighbour has usually never been opened, so its chunks are not in the cache. Decoding a large file's chunks is not something to do on the - event loop — that is what froze the indexer portal. + event loop: that is what froze the indexer portal. """ cached = self.chunk_cache.get(parent_id) if cached: @@ -3142,7 +3142,7 @@ async def _coverage_chunks(self, parent_id: str) -> list[FileChunk]: # Keep it, in the same cache the mount path reads. A neighbour decoded # here would otherwise be decoded AGAIN on every later coverage run, and # a THIRD time when the user actually navigates to it. Caching makes it - # once per file per query — which is also what stops an in-flight decode + # once per file per query, which is also what stops an in-flight decode # sitting between the user and a quit: measured 0.80s to quit against a # 0.49s baseline while decodes were still being repeated. if chunks: @@ -3217,7 +3217,7 @@ async def _cover_plan(self, ctx: _CoveragePass, margin: int) -> int: continue # Ordered around the neighbour's own first hit, where a jump into - # it lands — so only leaving the plan makes it pointless. Treating + # it lands, so only leaving the plan makes it pointless. Treating # it like tier 1 lost 54 of 61 neighbour passes to cursor moves. def still_wanted(pid: str = pid) -> bool: return pid in ctx.live_plan() and not (margin and self._context_yields(ctx)) @@ -3239,7 +3239,7 @@ def init_coverage_state(self) -> None: """Every field the coverage scheduler owns, in one place. Called from ``__init__``, and by any harness that drives coverage - against a presenter it did not fully construct — this state grows, and + against a presenter it did not fully construct: this state grows, and a caller mirroring it by hand goes stale silently. """ # What coverage is capturing right now, for the stall watch to name @@ -3255,14 +3255,14 @@ def init_coverage_state(self) -> None: self._coverage_task: asyncio.Task[None] | None = None # Per file, the HIT chunks the off-screen builder cannot take: a # plain-layout kind, or one over the renderer's size cap. Per hit, not - # per file — a 37-hit PDF with 36 captured reported cold forever over + # per file: a 37-hit PDF with 36 captured reported cold forever over # one 46,266-character chunk against the 40,000 cap. self._unservable: dict[str, set[int]] = {} # Per file, how many chunks the off-screen builder can capture. Same # lifetime as the store: a new query re-establishes it. self._capturable_total: dict[str, int] = {} # The file the user asked to warm whole, if any, and whether that walk - # is the one running — the duty cycle differs for work being watched. + # is the one running: the duty cycle differs for work being watched. self._full_warm_target: str | None = None self._full_warm_active = False self._full_warm_session: ProgressSession | None = None @@ -3274,7 +3274,7 @@ async def _warm_whole_file( Deliberately not bounded by ``COVERAGE_CHUNK_BUDGET``: a half-warmed file delivers none of what was asked for. Stopping is a flag rather - than a cancellation — ``_capture_file_targets`` tests before every + than a cancellation: ``_capture_file_targets`` tests before every capture, so an abort lands within one and unwinds through the ordinary return path. """ @@ -3296,7 +3296,7 @@ async def _warm_whole_file( # for calibration to learn from. # # Counted in CAPTURES against the file's capturable chunks. Counting - # targets walked instead mixed two units — a file whose chunks are half + # targets walked instead mixed two units: a file whose chunks are half # flat-path reached 100% halfway through and then sat motionless. total = max(1, self._capturable_total.get(parent_id, len(targets))) held = self.capture_store.count(parent_id, ctx.query_sig, width) @@ -3335,7 +3335,7 @@ def tick(processed: int, captures: int) -> None: finally: self._full_warm_active = False self.coverage_parent = None - # Outside the finally so it runs only on the path that finished — a + # Outside the finally so it runs only on the path that finished: a # `finally` also fires on the cancelled and failed ones, where mounting # a half-warmed file is wrong. # @@ -3353,8 +3353,8 @@ def _session_for_warm(self, parent_id: str) -> ProgressSession | None: The facility holds a single AMBIENT slot and ``begin`` retires whatever is in it, so opening one unconditionally killed a running index's line. This yields: no line while the index owns the slot, and one taken when - it is free. The reverse is not prevented — an index begun mid-warm does - evict the warm — so the session is re-read per report and dropped once + it is free. The reverse is not prevented (an index begun mid-warm does + evict the warm) so the session is re-read per report and dropped once retired, rather than reported into for the rest of the run. """ from fnd.tui.progress.operations import WARM_WHOLE_FILE @@ -3373,7 +3373,7 @@ def mount_warmed_file(self, parent_id: str, chunks: list[FileChunk]) -> None: Capturing every chunk is only half of what a warmed file promises: the windowed mount still fills three chunks at a time, and only when a - scroll event fires the debounced check — so at the top of the mounted + scroll event fires the debounced check, so at the top of the mounted region the user has to scroll DOWN to get more content ABOVE. Bounded, because a mounted frozen chunk costs arrange time whether or @@ -3432,7 +3432,7 @@ def _context_yields(self, ctx: _CoveragePass) -> bool: """Whether margin work should stand down right now. Margins are context. They yield to a landing the cursor has moved - towards, and to a whole-file warm the user has asked for — the two + towards, and to a whole-file warm the user has asked for: the two things somebody is actually waiting on. """ return self._full_warm_target is not None or self._unplanned_landings(ctx) @@ -3446,8 +3446,8 @@ def _unplanned_landings(self, ctx: _CoveragePass) -> bool: Scoped to files OUTSIDE this pass's plan, which is what makes yielding progress rather than spin: a planned file has already had its hits - attempted this pass, so yielding for one still uncaptured — a failed - decode, a capture the host refused — would starve margins entirely. It + attempted this pass, so yielding for one still uncaptured (a failed + decode, a capture the host refused) would starve margins entirely. It also means this can only fire once the cursor has moved, since a plan built from an unchanged anchor is the plan being walked. """ @@ -3463,13 +3463,13 @@ async def _cover_current_file( ) -> int: """Tier 1: capture the current file's hits, re-planning as the cursor moves. - A move inside the file changes only the ORDER — nearest-first is taken - around the cursor — so the plan is rebuilt around the new position and + A move inside the file changes only the ORDER, nearest-first is taken + around the cursor, so the plan is rebuilt around the new position and the same file carries on. Returning instead put the file the user is reading behind every neighbour in the plan, which is the one ordering the design rules out. - Ends when the cursor stops moving inside the file, or leaves it — which + Ends when the cursor stops moving inside the file, or leaves it, which IS a reason to stand down, because the pass planned around the file it went to supersedes this one. @@ -3524,7 +3524,7 @@ async def _capture_targets( """Capture ``targets`` of one file, standing down for anything on screen.""" # Held for the WHOLE file, not per capture. This is what the results # arrows read as "being warmed right now", and a capture is ~60 ms with - # a yield of at least as long after it — so an attribute set only + # a yield of at least as long after it, so an attribute set only # around the capture call was unset more often than set, and a poll # twice a second almost never caught it. Files went cold straight to # ready with the warming marker never appearing. It means "the file @@ -3552,7 +3552,7 @@ async def _capture_file_targets( for processed, index in enumerate(targets, 1): # The plan was ordered around where the cursor WAS. A pass can run # for hundreds of chunks, so without this it keeps warming the part - # of the file the user has already left — measured, it captured + # of the file the user has already left: measured, it captured # chunks 4-15 of a 1018-chunk PDF while every lookup asked for 679, # which is why the store was full and nothing was ever served. if not still_wanted(): @@ -3597,14 +3597,14 @@ async def _capture_file_targets( self.coverage_activity = None if capture is not None: # Filed under the capture's OWN width, not the width requested. - # They should be equal — that is the point of the jig above — + # They should be equal (that is the point of the jig above) # but a capture cut at one width and filed under another is # exactly the defect this branch set out to remove, and reading # it off the capture makes the two impossible to diverge. self.capture_store.put(parent_id, query_sig, capture.width, capture) captured += 1 # Hand the loop back for longer than we just held it. Without this - # coverage ran flat out — measured at 84% of the event loop — and a + # coverage ran flat out, measured at 84% of the event loop, and a # UI that only gets a sixth of its own loop is a UI that freezes. if on_capture is not None: on_capture(processed, captured) @@ -3626,7 +3626,7 @@ async def _mount_chunks_async( Stage 1 (sync, fast): mount the focused chunk plus :data:`tuning.VISIBLE_FIRST_ABOVE` chunks above and - :data:`tuning.VISIBLE_FIRST_BELOW` below — a window roughly matching + :data:`tuning.VISIBLE_FIRST_BELOW` below; a window roughly matching the typical viewport. The user sees the relevant content instantly. @@ -3662,7 +3662,7 @@ async def _mount_chunks_async( # A container whose Prune is queued is dead: ``on_prune`` closes its # message loop, so re-mounting it does not bring it back. Dispatch # resolves reuse candidates through ``is_live`` precisely so we never - # get here with one — this is the backstop that keeps a future caller + # get here with one: this is the backstop that keeps a future caller # from silently building a preview into a widget that is on its way out. if is_condemned(container): self.diag_log( @@ -3683,7 +3683,7 @@ async def _mount_chunks_async( # The try MUST cover the early awaits below (container mount, # cancel_task_on): they run BEFORE the detached finalise task is # spawned, and a cancellation here would otherwise skip the finally - # entirely — stranding the progress bar with no task left to hide it. + # entirely, stranding the progress bar with no task left to hide it. try: if not is_live(container): await pane.remove_children("#placeholder") @@ -3718,7 +3718,7 @@ async def _mount_chunks_async( # The progress line needs the size of THIS window as its mount # denominator. It is chosen by rows, so a tall-chunk format (a PDF # page is 30-60 rows) can select two chunks where the tunables - # would suggest fifteen — and a denominator of fifteen means the + # would suggest fifteen, and a denominator of fifteen means the # mount phase can never read as finished. container.mount_window = max(0, win_end - win_start) @@ -3745,7 +3745,7 @@ async def _mount_chunks_async( # the task mid-await (RUF006). Cleared once it completes. # The above-window chunks stage 1b will mount. finalise must # wait for THESE to exist + build, not just whatever is in - # chunk_widgets when it first looks — a prefetched focus chunk + # chunk_widgets when it first looks: a prefetched focus chunk # has build_done already set, so finalise would otherwise run # before stage 1b mounts the window and scroll to a stale # (focus-at-top) position, then settle-scroll once they land. @@ -3766,16 +3766,16 @@ async def _mount_chunks_async( # Stage 1b: mount the visible window, ABOVE the focus first and # closest-to-focus first within each side. # - # The finalise waits only on the chunks ABOVE the focus — they are - # what decides where the match lands — so mounting those first lets + # The finalise waits only on the chunks ABOVE the focus (they are + # what decides where the match lands) so mounting those first lets # the reveal stop waiting sooner. The ones below can arrive whenever; # interleaving the two sides made the reveal wait on roughly twice # the work it needed. # # Mounted in ONE pass. Awaiting each chunk's build before mounting - # the next looks attractive — Textual pumps a Markdown's blocks + # the next looks attractive: Textual pumps a Markdown's blocks # through the message loop, so builds started together interleave - # and each reports ~755ms where one alone takes ~22-30ms — but it is + # and each reports ~755ms where one alone takes ~22-30ms, but it is # a trap under real use. Serialising means a navigation's mount must # finish before the next one can get going, and over a sustained # Down sweep on a 1018-chunk PDF that took the finalise's build wait @@ -3822,11 +3822,11 @@ async def _mount_chunks_async( w.display = False hidden_widgets.append(w) self.update_progress_bar(progress=len(container.mounted_indices)) - # Wall-clock yield — see prefetch loop. + # Wall-clock yield: see prefetch loop. await asyncio.sleep(0.002) # Reveal + anchor in one synchronous block so Textual - # folds both layout changes into a single paint — no + # folds both layout changes into a single paint: no # visible "shift down then scroll back up" sequence. if hidden_widgets: for w in hidden_widgets: @@ -3835,7 +3835,7 @@ async def _mount_chunks_async( if not skip_internal_scrolls and focus_chunk_seq in container.chunk_widgets: # Revealing the above-window chunks shifted the layout, so # the focus chunk must be re-anchored. Scroll to the MATCH - # (first_match_block), not the chunk's top edge — anchoring + # (first_match_block), not the chunk's top edge: anchoring # to the top pushes a match deep inside the chunk off-screen # the moment the background fill completes (the cold-load # "wrong position until expanded" symptom). @@ -3852,7 +3852,7 @@ async def _mount_chunks_async( # bails the instant the user navigates away. self.mount_phase = "await-first-paint" # Wait for finalise to actually reveal (first paint) before adding - # any DOM — otherwise this fill runs on the same coroutine and + # any DOM, otherwise this fill runs on the same coroutine and # starves the finalise task, delaying first paint several-fold. _ft = getattr(container, "_finalise_task", None) if _ft is not None: @@ -3863,7 +3863,7 @@ async def _mount_chunks_async( if len(chunks) <= tuning.FULLMOUNT_CHUNK_BUDGET: batch_size = 6 # Fill BELOW only: appending in document order grows content - # DOWNWARD, so the match the user is reading never moves — no + # DOWNWARD, so the match the user is reading never moves: no # flicker. Downward match-jumps land on a mounted chunk (instant). # We deliberately DON'T pre-fill ABOVE: inserting content above the # viewport shoves it down, and the scroll can only re-pin a frame @@ -3878,7 +3878,7 @@ async def _mount_chunks_async( # replace this. # Empty-guard (degenerate mount); and bail the moment the user # takes scroll control (a user scroll clears is_armed) so upward - # lazy-mount isn't walled behind this background below-fill — + # lazy-mount isn't walled behind this background below-fill: # once we stop, _preview_mount_task completes and lazy-mount # handles both directions on demand. i = ( @@ -3898,7 +3898,7 @@ async def _mount_chunks_async( if i % batch_size == 0: await asyncio.sleep(0.006) # Freeze AFTER the fill, not during it. A just-mounted chunk has - # not been laid out — size.height is 0 — and a capture of an + # not been laid out, size.height is 0, and a capture of an # unlaid-out widget is correctly refused, which is what made a # per-chunk attempt here fail every single time (72 of 72). self.mount_phase = "4-freeze-sweep" @@ -3922,7 +3922,7 @@ async def _mount_chunks_async( # DOM while this mount was in flight, then cancelled us. Re- # caching or leaving this container mounted would re-pollute the # just-cleared pane with the previous query's half-built widget - # tree — the "stuck mid-mount after a new query" bug. Drop it. + # tree: the "stuck mid-mount after a new query" bug. Drop it. self.diag_log( f"mount superseded gen={my_generation}->{self.reset_generation} " f"parent={container.parent_doc_id[:8]}, dropping stale container" @@ -3948,7 +3948,7 @@ async def _mount_chunks_async( # DOM while this (now-cancelled) mount was in flight. Same-query # nav doesn't bump reset_generation, so the ``superseded`` branch # above doesn't catch it. Caching a dead container hands the next - # visit a cache-HIT on a widget that isn't in the tree — the warm + # visit a cache-HIT on a widget that isn't in the tree: the warm # path then activates + scrolls a zero-region ghost and reveals a # blank pane ("blank until I select another result and come # back"). ``is_live`` also rejects a container whose removal is @@ -3987,19 +3987,19 @@ async def _mount_chunks_async( and (self.mount_task is None or self.mount_task is asyncio.current_task()) and self.inflight_target in (None, (parent_id, focus_chunk_seq)) ): - # Ended in the early-await phase — before the detached finalise + # Ended in the early-await phase: before the detached finalise # task (the ONLY thing that hides the bar + releases the in-flight - # latch on success) was spawned — and no successor took over the + # latch on success) was spawned, and no successor took over the # loading state. Three ownership checks, all required: # * ``_finalise_task is None``: no detached finalise will clear it. # * ``mount_task is None`` (cancel_mount_task nulled it) OR # ``is current_task()`` (exception / other-path end, where it - # still points at this now-dead task) — a successor MOUNT would + # still points at this now-dead task): a successor MOUNT would # have overwritten it. # * ``inflight_target`` is still THIS target (or already clear): # the uncached decode path cancels us and opens a NEW # "decoding…" session WITHOUT assigning mount_task, so the - # latch already points at the successor — don't hide its bar. + # latch already points at the successor, don't hide its bar. # Otherwise the bar stays "loading" until an unrelated navigation # dispatches a fresh load. Hide + release so a cancelled (or # failed) cold mount can't strand the preview. @@ -4027,8 +4027,8 @@ def mount_chunk_into( implied by its index. Updates the container's ``mounted_indices`` / ``chunk_widgets`` / ``match_targets`` bookkeeping.""" - # Find the smallest already-mounted index greater than this one - # — we mount BEFORE that widget so chunks stay in document + # Find the smallest already-mounted index greater than this one: + # we mount BEFORE that widget so chunks stay in document # order regardless of which phase mounts them. before_widget: Widget | None = None next_mounted = min( @@ -4041,7 +4041,7 @@ def mount_chunk_into( # A capture, if coverage has one. This is where covering pays: mounting a # capture is one widget and no build, against parsing markdown and - # waiting on Textual to pump a block tree through the message loop — + # waiting on Textual to pump a block tree through the message loop: # measured at 400-1274ms for the focus chunk alone. Every mount path # comes through here, so a cold mount, a rebuild and a lazy-mount batch # all serve captures without knowing about them. @@ -4064,7 +4064,7 @@ def capture_width(pane: VerticalScroll) -> int: """The width a chunk lays out at, which is what a capture must be cut to. ``scrollable_content_region`` is the only one of the pane's widths that - the scrollbar moves, and it is the one children are laid out against — + the scrollbar moves, and it is the one children are laid out against: `size` and `content_size` are the same value as each other and neither shrinks by the bar. Building a capture at anything else produces strips wrapped for a width they are never displayed at: measured on an 80-column @@ -4074,7 +4074,7 @@ def capture_width(pane: VerticalScroll) -> int: Filing key and render width are deliberately the SAME number. They were briefly separated on the theory that a scrollbar-stable key would stop - captures being orphaned when the bar appears — but a capture cut for a + captures being orphaned when the bar appears, but a capture cut for a width it cannot be displayed at is not worth finding, and one key covering two real widths is worse than a miss. """ @@ -4136,7 +4136,7 @@ def _plain_stand_in(self, parent_id: str, chunk: FileChunk, width: int) -> Froze @property def scrollbar_markers_enabled(self) -> bool: - """In-development scrollbar match highlighting — off unless the + """In-development scrollbar match highlighting: off unless the user opts in via ``[defaults] scrollbar_match_highlight``.""" return bool(self._app._config and self._app._config.defaults.scrollbar_match_highlight) @@ -4145,7 +4145,7 @@ def refresh_match_scrollbar(self, chunks: list[FileChunk]) -> None: scrollbar so markers sit where the matches actually render. Earlier this fed a bool-per-chunk map placed by chunk ordinal, - which ignored chunk size — a match in a short chunk after a long + which ignored chunk size: a match in a short chunk after a long one landed near the top instead of far down. ``structural_match_ lines`` weights by each chunk's line count instead. On large markdown the lazy-mounted track spans only part of the file, so @@ -4168,7 +4168,7 @@ def refresh_match_scrollbar(self, chunks: list[FileChunk]) -> None: return # structural_match_lines scans every source line; a no/sparse-match # query on a large doc was measured in seconds (multi-minute worst - # case). Run it off the event loop and apply when ready — stale + # case). Run it off the event loop and apply when ready: stale # results (a newer nav bumped the generation) are dropped. token = self._markers_seq spec = self._app._effective_match_spec @@ -4228,18 +4228,18 @@ def _mount_plain_chunk( before: Widget | None = None, ) -> None: """Per-line layout for non-markdown chunks. Each body line becomes - its own Static so ``scroll_to_widget`` can target the first matched - line, and the match-row gets a subtle accent overlay. - - We deliberately don't mount the locator header (``p. 351 · ...``) - — that information lives on the sidebar result row; repeating it - per-chunk in the preview is just visual clutter. The first body - widget of each chunk gets a ``chunk-first`` class so a small top - gap still marks the chunk boundary. - - ``before`` (if supplied) makes every widget mount immediately - before that anchor — used by background-fill prepending so - chunks land in document order even when mounted out of sequence. + its own Static so ``scroll_to_widget`` can target the first matched + line, and the match-row gets a subtle accent overlay. + + We deliberately don't mount the locator header (``p. 351 · ...``): + that information lives on the sidebar result row; repeating it + per-chunk in the preview is just visual clutter. The first body + widget of each chunk gets a ``chunk-first`` class so a small top + gap still marks the chunk boundary. + + ``before`` (if supplied) makes every widget mount immediately + before that anchor: used by background-fill prepending so + chunks land in document order even when mounted out of sequence. """ _, pieces = render_chunk_pieces( c, query=self._app._search.current_query, match_spec=self._app._effective_match_spec @@ -4248,7 +4248,7 @@ def _mount_plain_chunk( first_match: Static | None = None for line_text, has_match in pieces: line_w = PlainStatic(line_text, classes="chunk-line") - # Store the PLAIN string (not the rich Text) — the match-count / + # Store the PLAIN string (not the rich Text): the match-count / # stop-region scans regex over ``fnd_text`` and re needs a str. line_w.fnd_text = line_text.plain # type: ignore[attr-defined] if has_match: @@ -4282,7 +4282,7 @@ def _mount_structured_chunk( """Structural markdown rendering for formats whose extractor populated ``body_md`` (md / docx / pptx). - Mounts a single :class:`FNDMarkdown` widget per chunk — + Mounts a single :class:`FNDMarkdown` widget per chunk, Textual builds out the per-block widget tree (headings, paragraphs, tables, fenced code, lists, blockquotes) and our highlight-aware subclasses overlay match-only spans on the diff --git a/fnd/tui/preview/tuning.py b/fnd/tui/preview/tuning.py index e31d496d..ec667f8d 100644 --- a/fnd/tui/preview/tuning.py +++ b/fnd/tui/preview/tuning.py @@ -6,7 +6,7 @@ """ # Preview widget cache. Repeat visits to a previously-loaded file -# should be instant — keep the mounted widget tree alive in a per-file +# should be instant: keep the mounted widget tree alive in a per-file # Container; switching files is then a single class-toggle. LRU-bounded. # Option A: only the active file stays mounted. Cached inactive containers # stayed in the pane and inflated every mount/settle (measured net-negative: @@ -14,11 +14,11 @@ # model, while taxing the active path). 1 = no stale DOM behind the active file. PREVIEW_CACHE_MAX_FILES = 1 PREVIEW_CACHE_MIN_CHUNKS = 1 -# Visible-first mount window — chunks are decoded already, mounting +# Visible-first mount window: chunks are decoded already, mounting # focused ± these counts synchronously gives the user instant viewport # feedback before the background fill starts. # -# ``VISIBLE_FIRST_ABOVE`` is a CAP, not a count — see +# ``VISIBLE_FIRST_ABOVE`` is a CAP, not a count: see # ``PreviewPresenter.above_window_start``. The reveal cannot happen until every # chunk above the focus has built (they decide where the match lands, see # _finalise_via_lock_body), so each one is paid for on the critical path, while @@ -34,7 +34,7 @@ # 1457 -> 848ms, the reconcile-to-scroll gap 731 -> 517ms. # How long the freeze sweep waits for its container to be revealed before # giving up (ticks of 50ms). Capturing from a `-pre-reveal` container yields -# blank strips with correct geometry — invisible to every guard, and served +# blank strips with correct geometry: invisible to every guard, and served # later as an empty preview. FREEZE_REVEAL_WAIT_TICKS = 40 @@ -46,7 +46,7 @@ # How long the freeze sweep may hold the event loop before yielding, in seconds. # The sweep swaps every out-of-window chunk's widget tree for its capture, and it -# used to do the whole file in ONE synchronous loop — so the cold-to-frozen +# used to do the whole file in ONE synchronous loop, so the cold-to-frozen # transition was a hard stall of however long that took. Measured on synthetic # six-line chunks: 221ms at 57 mounted, 452ms at 147, 559ms at 237, and real # chunks (PDF pages, rich markdown) cost 46-384ms EACH rather than ~2ms. @@ -60,49 +60,49 @@ # How long to wait after a frozen chunk reports a stale width before re-cutting # its strips. A window drag emits a resize per column and every mounted frozen # chunk reports independently, so this coalesces a whole gesture into one repair -# pass — and a drag that returns to its original width costs nothing at all. +# pass, and a drag that returns to its original width costs nothing at all. STALE_STRIP_REPAIR_DELAY = 0.25 # How many repair passes may chain before giving up. A pass abandons whenever the # width moves again mid-drag, and reports arriving during a pass are dropped, so -# it must be able to re-arm itself — but a chunk whose capture keeps failing +# it must be able to re-arm itself, but a chunk whose capture keeps failing # (`freeze` refuses an unlaid DataTable or a nested scroll region) would re-arm # forever without a bound. Three is enough for a drag that settles; beyond that # the next navigation rebuilds correctly anyway. STALE_STRIP_MAX_PASSES = 3 # Debounce before a coverage pass starts, so a held-down arrow key does not -# spawn a plan per keypress — a superseded plan then does no work at all. +# spawn a plan per keypress: a superseded plan then does no work at all. PREVIEW_WARM_DELAY = 0.35 # How many NEIGHBOURING files each side of the cursor coverage captures ahead. # Between-file navigation is the slow case: within a file the target is already # mounted or served, but arriving at a file with nothing captured pays the full # build. Only the neighbours' HIT chunks are captured, so the cost per file is -# its hit count rather than its size — a 1000-chunk neighbour with eight hits +# its hit count rather than its size: a 1000-chunk neighbour with eight hits # costs eight captures, and the buffer re-plans around the cursor on every move. COVERAGE_NEIGHBOUR_FILES = 2 # How many files from the TOP of the result list to warm regardless of where the # cursor is. The cursor window is relative, so on a fresh query it can only warm -# the first file's immediate neighbours — yet the opening moves of a search are +# the first file's immediate neighbours, yet the opening moves of a search are # the most likely to be made. Seeding the head covers them. Beyond it the window # takes over: a cursor past the seed warms its own neighbours as before, and the # seeded files cost a store lookup each to skip. COVERAGE_SEED_FILES = 8 # The structural renderer's size cap lives in fnd.tui.preview_dispatcher as -# MARKDOWN_MAX_CHARS — that module is what every path asks, and it sits below +# MARKDOWN_MAX_CHARS: that module is what every path asks, and it sits below # this package, so it cannot import from here. # How long coverage must stand idle after each capture, as a multiple of what # that capture cost. A capture builds a real markdown widget, and Textual pumps -# its blocks through the SAME event loop the UI runs on — so a capture in flight +# its blocks through the SAME event loop the UI runs on, so a capture in flight # is a frozen UI, and the yield between captures is the only place to give the # loop back. # # Measured without this, sitting still for 12s after opening a file: 154 -# captures, median 52ms each, 10.1 SECONDS of the 12 spent inside a capture — +# captures, median 52ms each, 10.1 SECONDS of the 12 spent inside a capture, # 84% of the loop, with stalls up to 393ms. Responsive enough to open a file and # then find that changing focus or stepping to the next match takes a second or # more to answer. @@ -125,12 +125,12 @@ # Duty cycle for a warm the USER asked for and is watching on the progress # line. Background coverage must never be felt, so it yields four times what it # takes; a foreground warm is the thing being waited on, and the earlier sweep -# put p95 keypress at 10.2ms here against 8.7ms at 4.0 — indistinguishable, +# put p95 keypress at 10.2ms here against 8.7ms at 4.0: indistinguishable, # while a 33s file finishes in ~100s instead of ~167s. COVERAGE_FOREGROUND_IDLE_RATIO = 2.0 # Above this many capturable chunks, a requested whole-file warm asks first. -# Cost is not predictable from anything known in advance — measured across real -# files, time per chunk varies 6x and memory per chunk 9x — so the honest move +# Cost is not predictable from anything known in advance: measured across real +# files, time per chunk varies 6x and memory per chunk 9x, so the honest move # is to show the estimate and let the user decide, not to guess a gate. FULL_WARM_CONFIRM_CHUNKS = 500 # Capture bytes per 1000 characters of chunk text, for that estimate. Observed @@ -142,7 +142,7 @@ # How long warming waits for an in-flight landing before giving up its turn # (ticks of 50ms). Warming must never compete with the scroll the user is -# waiting on — that competition cost 170ms -> 579ms median on a real corpus. +# waiting on: that competition cost 170ms -> 579ms median on a real corpus. PREVIEW_WARM_YIELD_TICKS = 40 VISIBLE_FIRST_ABOVE = 7 @@ -167,7 +167,7 @@ # match count walks the mounted subtree (``MatchNavigator._count_stops``), so a # file that stops being filled stops being counted in full. Measured marginal # cost of a frozen chunk on a fast machine: 37us of arrange, 78us of a far -# scroll, 0.9ms to mount — linear, no cliff, with the knee for "a navigation +# scroll, 0.9ms to mount, linear, no cliff, with the knee for "a navigation # still feels instant" around 1000-2000 mounted chunks. FULLMOUNT_CHUNK_BUDGET = 250 # How many chunks of a file coverage will CAPTURE ahead, so a jump outside the @@ -178,7 +178,7 @@ # The cap is on CAPTURES, not on mounted widgets: captures cost memory (44.5 KB # each, so 500 is ~22 MB against a machine-scaled cache budget) and the build # time to make them, while costing no arrange time at all. What stays MOUNTED is -# still the contiguous window — see fnd/tui/preview/coverage.py for why coverage +# still the contiguous window: see fnd/tui/preview/coverage.py for why coverage # must not put its scattered set into the DOM. COVERAGE_CHUNK_BUDGET = 500 # Chunks captured either side of a hit, so a landing has context above and below @@ -224,14 +224,14 @@ LAZY_MOUNT_ABSORB_QUIET = 7 LAZY_MOUNT_BATCH = 3 # Batch size for a run already in the capture store, which only ``fill_all`` -# asks for — and it checks per batch, because FULL does not imply it. +# asks for, and it checks per batch, because FULL does not imply it. # The scroll-driven path keeps LAZY_MOUNT_BATCH: it cannot know the next chunks # are served without checking, and a cold batch this size is a freeze. A mount # that is served is a # blit; one that is built is a parse and a message-pump round, and the batch # above is sized for the latter. Measured on a real file, an above-batch costs -# 0.07s at 3 and 0.14s at 32 when served — the cost is the settle and the -# re-anchor, not the chunks — against 5.5s at 32 when it has to build them. +# 0.07s at 3 and 0.14s at 32 when served (the cost is the settle and the +# re-anchor, not the chunks) against 5.5s at 32 when it has to build them. LAZY_MOUNT_BATCH_SERVED = 32 # Above this many chunks a fully warmed file stays windowed. Mounted frozen # chunks cost arrange time whether or not they are on screen: measured, 727 @@ -244,7 +244,7 @@ # Reveal watchdog: an active container mounts invisible (``-pre-reveal``) and is # revealed by its finalise task once the layout settles. Under rapid navigation # that task can be cancelled before it reveals, or hang for seconds awaiting -# above-window chunks a cancelled mount never mounted — leaving the container +# above-window chunks a cancelled mount never mounted, leaving the container # invisible ("preview blank until I select a different result and come back"). # This is the bounded-time backstop: if a container is still ``-pre-reveal`` this # long after it became active, reveal it regardless. Set well above a normal diff --git a/fnd/tui/preview/visibility.py b/fnd/tui/preview/visibility.py index 73dc7a18..f5f30a8f 100644 --- a/fnd/tui/preview/visibility.py +++ b/fnd/tui/preview/visibility.py @@ -2,7 +2,7 @@ ``add_class``/``remove_class`` call ``App.update_styles``, which walks EVERY descendant of the node and re-runs selector matching on each one. A preview -container holds a mounted document — hundreds of widgets — and it is toggled on +container holds a mounted document, hundreds of widgets, and it is toggled on every activation and every swap. Measured on the real corpus, sampling the main thread whenever the event loop @@ -16,8 +16,8 @@ 1. The class must never appear as an ANCESTOR in a selector (``.-hidden Foo``), or descendants really would match differently. 2. It must not change the geometry descendants are laid out against. Styling and - layout are not separable here — the walk is also what re-arranges the subtree. -3. It must not change a rule descendants' cached styles are computed from — + layout are not separable here: the walk is also what re-arranges the subtree. +3. It must not change a rule descendants' cached styles are computed from, and ``-pre-reveal`` does, so this one is handled rather than relied on. Textual computes a widget's ``visual_style`` by walking its ANCESTORS for @@ -25,10 +25,10 @@ widget's OWN cache key, which an ancestor-only restyle never moves. Under ``opacity: 0`` an ancestor contributes no background at all, so a callout built behind ``-pre-reveal`` kept painting its text on the bare pane colour for the -life of the container, while the blockquote around it — restyled directly — +life of the container, while the blockquote around it, restyled directly, showed the tint. Reveal therefore clears those caches itself: a cache clear and a repaint per descendant, no selector matching. Measured over real navigations, -once per reveal — 40 widgets / 0.9ms median, 571 / 12.9ms worst. +once per reveal: 40 widgets / 0.9ms median, 571 / 12.9ms worst. ``-hidden`` and ``-pre-reveal`` satisfy the first two: each is declared only on ``PreviewContainer`` itself, and each toggles the container's own visibility @@ -37,7 +37,7 @@ ``is-loading`` failed the second condition until it was changed to match: it used to set ``scrollbar-size-vertical: 0``, removing the gutter and so changing the pane's content width, and shortcutting it left tables laid out at the old -width — the match coordinate inside a tall table could not be resolved and the +width; the match coordinate inside a tall table could not be resolved and the off-screen indicator never flipped. It now hides the bar by COLOUR instead, which leaves the geometry alone, so it qualifies. Condition 2 is what that episode cost, and ``test_shortcut_classes_change_visibility_only`` is what stops diff --git a/fnd/tui/preview/warm_host.py b/fnd/tui/preview/warm_host.py index d89ba39c..af9d71cb 100644 --- a/fnd/tui/preview/warm_host.py +++ b/fnd/tui/preview/warm_host.py @@ -4,7 +4,7 @@ that in the on-screen container is what made warming compete with navigation, and hiding that container is not an option either: -* ``display: none`` zeroes the layout, so ``freeze`` correctly refuses — a +* ``display: none`` zeroes the layout, so ``freeze`` correctly refuses; a capture needs real geometry; * ``opacity: 0`` keeps the geometry but blends every foreground into the background. Measured, the capture carries all 241 characters with fg == bg on @@ -17,8 +17,8 @@ A screen that is installed but not current has none of those problems. Textual's compositor walks the active screen only, so a suspended screen costs nothing per tick, while its widgets stay alive and laid out. Measured against a visible -capture: identical palette, correct ink, and an arbitrary capture width — -36/60/96 columns for 40/64/100 requested — with the live screen untouched. +capture: identical palette, correct ink, and an arbitrary capture width, +36/60/96 columns for 40/64/100 requested, with the live screen untouched. """ from __future__ import annotations @@ -44,7 +44,7 @@ # Height handed to the off-screen layout pass. Only an upper bound: freeze # captures at max(size.height, virtual_size.height), so a taller chunk is still -# captured whole — this just has to be generous enough that the layout does not +# captured whole; this just has to be generous enough that the layout does not # constrain a normal chunk. _LAYOUT_HEIGHT = 400 @@ -59,7 +59,7 @@ def __init__(self, app: FNDApp) -> None: self._screen: Screen[None] | None = None self._container: VerticalScroll | None = None # One capture at a time. The screen and the container are shared, and - # the width is set on the SCREEN — so two callers interleaving at any + # the width is set on the SCREEN, so two callers interleaving at any # of the awaits below would lay each other's widget out at the wrong # width. Coverage and the stale-strip repair are independent tasks and # nothing else serialises them: the repair waits on `pipeline_busy`, @@ -76,7 +76,7 @@ async def ensure(self) -> VerticalScroll | None: # Reuse an already-installed screen. The name is fixed, so a retry # after a partial failure (install succeeded, `mount` below raised) # would otherwise raise on the duplicate name, be swallowed by the - # `except`, and disable warming for the rest of the session — with + # `except`, and disable warming for the rest of the session, with # no error anywhere: captures simply stop and every lookup misses. screen = self._screen if screen is None or not self._app.is_screen_installed(_SCREEN_NAME): @@ -89,7 +89,7 @@ async def ensure(self) -> VerticalScroll | None: # # The push is NOT awaited, and the pop follows immediately in # the same tick. Awaiting it lets this empty screen become - # current and PAINT — measured live, the whole app went blank + # current and PAINT: measured live, the whole app went blank # for 35 consecutive frames (~1.75s) the first time warming ran, # sidebar included. self._app.push_screen(_SCREEN_NAME) @@ -98,7 +98,7 @@ async def ensure(self) -> VerticalScroll | None: # No scrollbar. This container is a measuring jig, not something # anyone scrolls: a chunk taller than `_LAYOUT_HEIGHT` overflows it, # it grows a bar, and the chunk then lays out a column narrower than - # the width asked for — so the capture is cut for one width and + # the width asked for, so the capture is cut for one width and # filed under another. Measured, the boundary is exactly the layout # box: 300 rendered rows captured at 76, 420 rows at 75. container.styles.scrollbar_size_vertical = 0 @@ -120,7 +120,7 @@ async def capture( """Build ``chunk`` off-screen at ``width`` and capture it. The widget is constructed exactly as the on-screen path constructs it, - because the capture IS that widget's own output — any divergence would + because the capture IS that widget's own output: any divergence would show up as a preview differing from the widget path's. ``match_spec`` should be the caller's SNAPSHOT, not the app's live one. @@ -152,13 +152,13 @@ async def _capture_locked( async with asyncio.timeout(tuning.WARM_BUILD_TIMEOUT): await widget.build_done.wait() # The screen is not current, so Textual will not lay it out on its - # own — Screen._on_timer_update gates relayout on is_current. Drive + # own: Screen._on_timer_update gates relayout on is_current. Drive # it explicitly at the width the preview pane will paint at. # # TWICE, with a yield to the message pump in between, and the yield # is the part that matters. A DataTable sizes itself in response to # its own posted refresh, so after a single pass it holds its rows - # with NO geometry at all — measured rows=3, size=0, virtual=0 — and + # with NO geometry at all (measured rows=3, size=0, virtual=0) and # the capture comes back with the table's border and none of its # contents. That is the "tables stopped being formatted" report: # an empty box where the table was. A second pass alone does not fix @@ -176,7 +176,7 @@ async def _capture_locked( finally: # NOT awaited, and suppressing BaseException rather than Exception. # Cancellation is the normal way warming ends, CancelledError is a - # BaseException, and it lands ON the await — so an awaited removal + # BaseException, and it lands ON the await, so an awaited removal # here is skipped exactly when it is needed. Measured: 12 whole # widget trees (~28 widgets each) stranded across 29 cancellations, # unbounded over a session and invisible to the row budget. diff --git a/fnd/tui/preview/warmth.py b/fnd/tui/preview/warmth.py index 87c5aaf8..5506d453 100644 --- a/fnd/tui/preview/warmth.py +++ b/fnd/tui/preview/warmth.py @@ -2,8 +2,8 @@ Coverage captures chunks ahead of the cursor so that navigating to a match is a blit rather than a build (see :mod:`fnd.tui.preview.coverage`). That -makes navigation cost bimodal — a served jump is ~50 ms, an unserved one -can be seconds — and nothing on screen said which you were about to get. +makes navigation cost bimodal (a served jump is ~50 ms, an unserved one +can be seconds) and nothing on screen said which you were about to get. One vocabulary, read by two places that must not drift: the results tree paints it, and the progress line uses it to pick the plan for the @@ -13,7 +13,7 @@ Deliberately three states and not two. READY vs not is the fact that changes what a jump costs, but the captures run SERIALLY on one -off-screen host, so exactly one file is ever WARMING — which makes it a +off-screen host, so exactly one file is ever WARMING, which makes it a single marker walking outward from the cursor rather than noise across the list, and it is what tells the user their wait is buying something. """ @@ -29,13 +29,13 @@ class WarmState(Enum): """Whether jumping into a file will be served from the capture store.""" - #: Nothing captured yet, and nothing being captured — this jump will build. + #: Nothing captured yet, and nothing being captured; this jump will build. COLD = "cold" #: Being captured right now. At most one file is ever in this state. WARMING = "warming" #: Every listed hit is captured; a jump to any of them is a blit. READY = "ready" - #: Every capturable chunk is captured, not just the hits — so a scroll + #: Every capturable chunk is captured, not just the hits, so a scroll #: anywhere in the file is a blit too, not only a jump to a match. FULL = "full" @@ -61,7 +61,7 @@ def warm_state( """Classify one file. ``hit_seqs`` is the chunk sequence of every hit the results list can - navigate to — the listed hits, not every textual match, because those are + navigate to: the listed hits, not every textual match, because those are exactly what Down/Up step through and what coverage targets. A file with no listed hits is READY: there is nothing to jump to, so @@ -70,20 +70,20 @@ def warm_state( Readiness is judged on the HITS alone, never on whole-file coverage. The chunks around a hit are captured too, but a jump lands on the hit and lazy - mount fills the rest — counting them would leave a file showing cold for + mount fills the rest: counting them would leave a file showing cold for idle work the user cannot see. ``unservable`` holds hits coverage cannot capture at all: a plain-layout kind, or a chunk over the markdown renderer's size cap. Both take the flat path, which has no off-screen builder, so "captured" is unreachable for them and judging readiness over them can never come true. An excluded hit reports - READY for the same reason a file with no hits does — there is nothing here + READY for the same reason a file with no hits does: there is nothing here for a jump to wait on. Per HIT, not per file: a 37-hit PDF with 36 hits captured reported cold indefinitely over one 46,266-character chunk against the 40,000 cap. - FULL outranks READY because it is strictly stronger — every hit is covered + FULL outranks READY because it is strictly stronger: every hit is covered by every chunk being covered. It is counted against the CAPTURABLE total, never the chunk count: a file whose chunks all take the flat path has no off-screen builder, and measuring it against zero would paint it fully warm diff --git a/fnd/tui/preview_dispatcher.py b/fnd/tui/preview_dispatcher.py index f8c9fdce..01508af8 100644 --- a/fnd/tui/preview_dispatcher.py +++ b/fnd/tui/preview_dispatcher.py @@ -2,12 +2,12 @@ The redesign keeps two parallel renderers: -* **Flat buffer** (:class:`fnd.tui.line_buffer.LineBufferPreview`) — +* **Flat buffer** (:class:`fnd.tui.line_buffer.LineBufferPreview`), one ScrollView per file, Strips cached per visual line. Used for ``.pdf`` and ``.txt`` files where there's no structure to render beyond plain text + match highlights. The win is that the post-load DOM is O(1) widgets per file regardless of file size. -* **Structural** (per-block Markdown widget tree) — used for ``.md``, +* **Structural** (per-block Markdown widget tree): used for ``.md``, ``.docx``, and ``.pptx``. These files index a markdown serialisation of their structure; the existing pipeline renders that through Textual's Markdown widget which honours headings, lists, fences, @@ -31,7 +31,7 @@ class PreviewBody(Protocol): Both :class:`fnd.query.FileChunk` (what the preview mounts) and :class:`fnd.query.Hit` (what the results pane lists) satisfy it, so a - caller holding either can ask which text the user will end up reading — + caller holding either can ask which text the user will end up reading, see :mod:`fnd.tui.match_evidence`. Read-only properties, since both implementations are frozen dataclasses. """ @@ -47,7 +47,7 @@ def body_text(self) -> str: ... # Kinds whose extractor can emit a Markdown serialisation in ``body_md``; # these route through the structural Markdown widget when the chunk actually # has ``body_md`` populated (see the ``bool(c.body_md)`` guard below). Derived -# from the central registry — PDF is included because the optional +# from the central registry: PDF is included because the optional # ``pdf-structure`` extra populates ``body_md`` on PDF chunks; without it # ``body_md`` stays empty and the predicate keeps PDFs on the flat path. _MARKDOWN_RENDERED_KINDS: frozenset[str] = MARKDOWN_RENDERED_KINDS @@ -57,7 +57,7 @@ def body_text(self) -> str: ... # Largest chunk, in markdown characters, that the structural renderer will build. # Measured on a real 727-chunk PDF: a 120,123-character chunk took 4,424ms to -# build into 7,184 rendered rows, against a 5.3ms median — a multi-second freeze, +# build into 7,184 rendered rows, against a 5.3ms median; a multi-second freeze, # observed live at 8.4s, because Textual builds the widget on the event loop. # Every ordinary chunk in that file was under 6,000 characters, so this sits an # order of magnitude clear of normal content. @@ -67,13 +67,13 @@ def body_text(self) -> str: ... # applies only where there is no match, and the chunk you navigate to has one), # and mounting those is superlinear. Measured head-to-head on a synthetic # 120,036-character table, 1,596 lines: flat 224ms to render plus 725ms to mount -# and settle, against 979ms structural — near parity at that shape. The win on +# and settle, against 979ms structural, near parity at that shape. The win on # the real chunk comes from its structural cost being far above linear (4,424ms), # not from the flat path being fast. Do not raise this cap expecting the flat # path to absorb it for free. # # Lives here rather than in preview.tuning because this module is what every -# path asks and it sits BELOW the preview package — importing tuning from here +# path asks and it sits BELOW the preview package: importing tuning from here # is an import cycle. MARKDOWN_MAX_CHARS = 40_000 @@ -90,7 +90,7 @@ def uses_markdown_renderer(c: PreviewBody) -> bool: """ if c.kind not in _MARKDOWN_RENDERED_KINDS or not c.body_md: return False - # And it must be small enough to BUILD — see MARKDOWN_MAX_CHARS above for + # And it must be small enough to BUILD: see MARKDOWN_MAX_CHARS above for # the measurements and for what the flat path over the cap actually costs. # What is lost over the cap is the table structure of something that was # never going to be usable as a 7,184-row widget anyway. Gating here rather diff --git a/fnd/tui/preview_markers.py b/fnd/tui/preview_markers.py index 46fda71e..a72d18e0 100644 --- a/fnd/tui/preview_markers.py +++ b/fnd/tui/preview_markers.py @@ -7,7 +7,7 @@ The earlier feed was a bool per chunk, mapped to the bar by chunk *ordinal* (cell ``i`` lit if ``floor(i * n / size)`` matched). That ignores chunk size: a match in a short chunk right after a long one was drawn near the top while -the match actually sat ~2/3 down — off by up to half the bar. +the match actually sat ~2/3 down, off by up to half the bar. Here each chunk is weighted by its rendered line count, taken from the same source the renderer mounts (``body_md`` else reconstructed block text), so a @@ -42,7 +42,7 @@ def structural_match_lines(chunks: list[FileChunk], spec: MatchSpec) -> tuple[li A chunk whose match exists only in text the renderer does NOT mount contributes nothing. It used to contribute a marker at the chunk's top line, - which put a mark on the track pointing at a place with no highlight — the + which put a mark on the track pointing at a place with no highlight: the scrollbar promising a match the pane could never show. """ match_lines: list[int] = [] @@ -54,7 +54,7 @@ def structural_match_lines(chunks: list[FileChunk], spec: MatchSpec) -> tuple[li prime(spec, (w for text in texts for w in DOC_WORD_RE.findall(text))) for text in texts: # splitlines() (not split("\n")) so a trailing newline doesn't add a - # phantom line and an empty source counts as 0 lines, not 1 — both keep + # phantom line and an empty source counts as 0 lines, not 1: both keep # the total / fractions closer to the rendered row count. lines = text.splitlines() local = next( diff --git a/fnd/tui/preview_scroll.py b/fnd/tui/preview_scroll.py index 39a62405..138d746d 100644 --- a/fnd/tui/preview_scroll.py +++ b/fnd/tui/preview_scroll.py @@ -28,17 +28,17 @@ # Consecutive refreshes the content above the match must keep the same height -# before the scroll commits. One is not enough — layout arrives in bursts, so a +# before the scroll commits. One is not enough: layout arrives in bursts, so a # single unchanged sample lands on a plateau mid-growth. _ABOVE_STABLE_TICKS = 2 # Floor on the shared retry budget (30) below which the settle gate stops -# waiting and commits anyway. Without it a chunk whose layout keeps moving — -# a heavy table/fence page — could hold the scroll for the whole budget, and a +# waiting and commits anyway. Without it a chunk whose layout keeps moving ( +# a heavy table/fence page) could hold the scroll for the whole budget, and a # match that lands late is worse than one that lands a few rows off: measured # an 864ms tail before this bound. # # Deliberately tight. The gate needs three iterations to see _ABOVE_STABLE_TICKS -# consecutive stable heights, so four deferrals is one spare — anything more is +# consecutive stable heights, so four deferrals is one spare: anything more is # latency the settle waits on, and _settled gates lazy-mount and prefetch as well # as the scroll. A looser bound delayed both far enough to fail CI (a Reading # View re-wrap, which re-lays out every chunk, and a prefetch waiting on settle). @@ -46,7 +46,7 @@ # Refreshes ``scroll_to_location`` keeps re-anchoring for once the layout has # stopped changing. The re-wrap lands in bursts, so the position can look stable -# for a stretch and then jump — hence a tail rather than an early exit. +# for a stretch and then jump, hence a tail rather than an early exit. _RESTORE_TAIL_REFRESHES = 12 # Hard bound on the whole re-anchor loop, including the top-ups it takes while a # mount is still in flight. ~1.5s at 60fps: longer than the slowest measured @@ -77,7 +77,7 @@ class ScrollAnchor: @dataclass(frozen=True, slots=True) class ViewportLocation: - """A restorable reading position in the preview — the read-counterpart of + """A restorable reading position in the preview: the read-counterpart of a scroll target. ``locate()`` produces one; ``scroll_to_location()`` consumes it (Memento). Structural previews use ``chunk_seq`` + ``offset`` (signed rows from that chunk's top, negative when the viewport sits above @@ -141,10 +141,10 @@ def __call__(self) -> None: class PreviewScrollController: """Owns the active anchor and whether it is authoritative (armed). - arm() — navigation sets the desired target. - reconcile() — idempotently scroll to the target via the active + arm(): navigation sets the desired target. + reconcile(): idempotently scroll to the target via the active strategy; no-op when released. - release() — user took scroll control; stop reconciling. + release(): user took scroll control; stop reconciling. """ def __init__(self, select_strategy: Callable[[], ScrollStrategy | None]) -> None: @@ -154,12 +154,12 @@ def __init__(self, select_strategy: Callable[[], ScrollStrategy | None]) -> None # True once the armed anchor's scroll has committed. arm() clears it; # a committed reconcile() sets it. ``is_settling`` (armed & not settled) # is the window during which scroll-driven lazy mount must stay out of - # the controller's way — see is_settling. + # the controller's way: see is_settling. self._settled = False # Monotonic navigation epoch. The single anchor stopped the many-inline- # scroll-sites race, but the strategy's retry chain reschedules itself # across refreshes, so rapid navigation spawns OVERLAPPING chains (each - # pinned to a captured chunk seq) that all commit a scroll — last writer + # pinned to a captured chunk seq) that all commit a scroll: last writer # wins. arm() bumps this; the active chain captures it and bails (no # scroll, no reschedule, no settled-flip) the moment it's superseded. self._generation = 0 @@ -167,7 +167,7 @@ def __init__(self, select_strategy: Callable[[], ScrollStrategy | None]) -> None self._pending_settled: _Once | None = None # Outstanding reflow-restores. A restore re-applies its scroll across # many refreshes as the re-wrap lands, and unlike a reconcile it never - # arms the anchor — so ``is_settling`` says nothing about it. See + # arms the anchor, so ``is_settling`` says nothing about it. See # is_restoring. self._restoring = 0 # Restores finished since start-up. ``is_restoring`` alone cannot say @@ -194,15 +194,15 @@ def is_restoring(self) -> bool: """A reflow-restore is still re-applying its scroll. Widening the pane (Reading View, a resize) re-wraps the content over an unknown number of refreshes, so the restore keeps re-scrolling until the layout stops - moving. Anything that needs the final reading position — a measurement, - a test assertion — must wait for this to clear; ``is_settling`` does not + moving. Anything that needs the final reading position (a measurement, + a test assertion) must wait for this to clear; ``is_settling`` does not cover it, because a restore never arms the anchor.""" return self._restoring > 0 @property def restores_completed(self) -> int: """How many reflow-restores have finished. Monotonic, so a waiter can - read it before triggering a restore and wait for it to exceed that — + read it before triggering a restore and wait for it to exceed that: the only way to tell "not started yet" from "already landed".""" return self._restores_completed @@ -229,7 +229,7 @@ def reconcile(self, on_settled: Callable[[], None] | None = None) -> None: # ``on_settled`` fires EXACTLY ONCE once the scroll has committed (or # immediately when there is nothing to scroll). Cold/warm reveal paths # pass the container un-hide here so it is revealed AFTER the scroll - # lands — never before — which keeps the match from flashing at the file + # lands, never before, which keeps the match from flashing at the file # top then jumping. The contract is fire-once: a dropped call strands # the container hidden; a double call would re-run a reveal the callback # assumes happens once. We hand the strategy a one-shot latch, so even @@ -247,7 +247,7 @@ def fire() -> None: if epoch != self._epoch and gen == self._generation: return # The scroll has committed. Honour the one-shot reveal either way (a - # dropped call strands the container hidden) — but only flip + # dropped call strands the container hidden), but only flip # ``_settled`` (opening the lazy-mount gate) when this is STILL the # current navigation. A superseded chain landing late must not open # the gate for the newer nav still in flight. @@ -272,7 +272,7 @@ def fire() -> None: raise def locate(self) -> ViewportLocation | None: - """Read the viewport's current top position — the counterpart to + """Read the viewport's current top position: the counterpart to scrolling to one. Survives a width reflow (e.g. toggling Reading View re-wraps the content). Delegates to the active strategy; pass the result straight back to :meth:`scroll_to_location`. Best-effort: a @@ -433,7 +433,7 @@ def reconcile( # A frozen chunk is skipped for a different reason: the band paints the # widget's BACKGROUND, and a capture's strips are opaque, so the band # can never tint the content. All it can reach is the one padding row - # the strips do not cover — which is the stray amber bar sitting above + # the strips do not cover, which is the stray amber bar sitting above # the text. It was invisible while a stand-in was sized to its strips # alone, and appeared the moment the stand-in started carrying the # padding it is supposed to have. @@ -458,7 +458,7 @@ def reconcile( def _superseded(self, generation: int, current_generation: Callable[[], int] | None) -> bool: """This scroll chain was started for ``generation`` but a newer - navigation has since bumped the controller's generation — so it must not + navigation has since bumped the controller's generation, so it must not scroll or reschedule (last-writer-wins avoidance).""" return current_generation is not None and generation != current_generation() @@ -479,7 +479,7 @@ def _do_scroll_to_chunk( from fnd.tui.widgets.markdown import FNDMarkdown # Generation guard (entry): a superseded retry chain dies on its next - # tick — no scroll, no reschedule — but still fires on_done so the + # tick (no scroll, no reschedule) but still fires on_done so the # one-shot reveal floor holds (the reveal itself is identity-guarded). if self._superseded(generation, current_generation): if on_done is not None: @@ -581,7 +581,7 @@ def _do_scroll_to_chunk( # Content ABOVE the match decides where it ends up on screen, and it is # still arriving when the scroll first runs: chunk widgets mount at ~0 # height and grow as their markdown lays out. Committing early lands - # correctly and then slides — ~18 rows on the AWS guide's p.24, leaving + # correctly and then slides: ~18 rows on the AWS guide's p.24, leaving # the match two thirds down the pane instead of a quarter. # # Two distinct things have to settle, and neither implies the other: @@ -591,7 +591,7 @@ def _do_scroll_to_chunk( # currently mounted sees nothing pending (the trap # ``_finalise_via_lock``'s ``expected_above_seqs`` exists to avoid); # * their HEIGHT has to stop changing. ``build_done`` is not that - # signal — measured on p.24, the seven chunks above were all mounted + # signal: measured on p.24, the seven chunks above were all mounted # with build_done set and still grew 142 → 159 rows afterwards. # # So: ask the host about existence, and watch the measured height until @@ -620,7 +620,7 @@ def _wait(ticks: int) -> None: if self._host.above_window_pending(focus_chunk_seq): # Content that will sit above the match doesn't exist yet, so - # its position is about to change. Wait — bounded only by the + # its position is about to change. Wait: bounded only by the # shared retry budget, like every other "not laid out yet" # condition here. Bounding this more tightly gave up on a slow # mount and landed the match off screen entirely. @@ -630,7 +630,7 @@ def _wait(ticks: int) -> None: if any(w.region.height == 0 for w in above): # An unlaid-out chunk measures 0 and keeps measuring 0, so # the stability check reads three zeroes as "settled" and - # commits — then the chunk lays out and pushes the match + # commits, then the chunk lays out and pushes the match # down. Zero height is the absence of a measurement, not a # stable one. The floor still bounds this, so a genuinely # empty chunk can't stall navigation. @@ -638,7 +638,7 @@ def _wait(ticks: int) -> None: return # Mounted and built; now wait for the measured height to hold # still, since build_done is not a height-settled signal. THIS - # is what the floor bounds — a page whose layout never quite + # is what the floor bounds: a page whose layout never quite # stops moving still has to land. ticks = stable_ticks + 1 if measured == above_height else 0 if ticks < _ABOVE_STABLE_TICKS: @@ -665,10 +665,10 @@ def _wait(ticks: int) -> None: ) # A match inside a table renders as one full-height DataTable (no # per-cell widgets, no internal scroll), so the matched cell is not its - # own widget — resolve the cell's region as the scroll anchor instead of + # own widget: resolve the cell's region as the scroll anchor instead of # the table's own (which is the table top). While the rows are still # mounting the cell region isn't laid out; retry rather than committing a - # scroll to the table top — the race that stranded deep-table matches at + # scroll to the table top: the race that stranded deep-table matches at # the top on a cold mount. ``_anchor_region`` returns ``target.region`` # unchanged for non-table targets. # A backward hand-over enters at the section's END. Applied here, over a @@ -714,7 +714,7 @@ def _wait(ticks: int) -> None: ) return if anchor is None: - # Retries exhausted with the cell still unresolved — fall back to the + # Retries exhausted with the cell still unresolved: fall back to the # matched DataTable's own region (its top), logged so the regression # is visible. anchor is None only in the table branch, so match_table # is set; the wrapper target.region can be a zero-height/offset region. @@ -733,7 +733,7 @@ def _wait(ticks: int) -> None: anchor = region_at_row(anchor, row) # Generation guard (immediately before the commit): the resolution above # spanned refreshes, during which a newer navigation may have superseded - # this chain. Re-check freshness right before the side effect — the + # this chain. Re-check freshness right before the side effect: the # cooperative-cancellation rule. A superseded chain bails without # scrolling (on_done still fires the reveal floor; identity-guarded). if self._superseded(generation, current_generation): @@ -745,7 +745,7 @@ def _wait(ticks: int) -> None: pane = self._host.preview_pane() # Drop the match ~a quarter down the viewport so the lines above it - # give context, instead of pinning it to the top line — but only when + # give context, instead of pinning it to the top line, but only when # we actually landed on a match (not a bare chunk-top navigation). margin = ( int(pane.size.height * margin_from) @@ -763,7 +763,7 @@ def _wait(ticks: int) -> None: # resolved anchor to the host so it can hide the old one, position # this one, and reveal it in a single tick (no blank between # previews). The anchor (screen space) is the matched table cell - # for a table match, so the swap lands on the matched row too — + # for a table match, so the swap lands on the matched row too, # not the table top. When there is no outgoing container this is a # no-op and we scroll the already-visible pane normally. landed_y: int | None = None @@ -773,7 +773,7 @@ def _wait(ticks: int) -> None: # Map the anchor's screen region into the pane's scrollable # content space and scroll there in one shot. (Reading # scroll_offset back after scroll_to_widget to apply the margin - # races a cold render — the offset isn't committed yet, so the + # races a cold render: the offset isn't committed yet, so the # nudge lands on a stale, wrong position.) region = anchor.translate( pane.scroll_offset - pane.scrollable_content_region.offset @@ -781,7 +781,7 @@ def _wait(ticks: int) -> None: # ``scroll_to_region`` clamps its delta against ``max_scroll_y``, # which is 0 until the pane recomputes ``virtual_size`` for the # laid-out content: the scroll lands nowhere and reports success. - # ``focus_chunk_seq`` 0 arrives with no settle spent — every gate + # ``focus_chunk_seq`` 0 arrives with no settle spent: every gate # above needs chunks ABOVE the match, and there are none. unscrollable = ( retries > 0 and region.y + region.height > pane.virtual_size.height @@ -842,7 +842,7 @@ def _scroll_pane_to_match_region( ) -> int: """Scroll ``pane`` so ``region`` (already in the pane's scrollable- content space) sits ``margin`` rows down from the top, giving the match - some context above it. One ``scroll_to_region`` call — no reading the + some context above it. One ``scroll_to_region`` call: no reading the offset back, so nothing races a cold render's deferred layout. ``animate`` glides the scroll (between-match nav within a file) instead of jumping (a fresh file's reveal, which lands instantly).""" @@ -883,12 +883,12 @@ def _anchor_region(self, target: Widget, table: DataTable[Any] | None) -> Region """Screen-space region the scroll should land on. For a plain widget that's its own ``region``. For a match inside a - table — one full-height DataTable with no per-cell widgets and no - internal scroll — it's the matched *cell's* region, so a match in a + table: one full-height DataTable with no per-cell widgets and no + internal scroll; it's the matched *cell's* region, so a match in a lower row is actually revealed instead of the table top. Returns ``None`` when ``target`` is a table whose cell region is not - resolvable yet (rows unmounted / not sized — ``_get_cell_region`` raises + resolvable yet (rows unmounted / not sized: ``_get_cell_region`` raises or yields a zero-height region). The caller retries rather than committing a scroll to the table top, which is the race that stranded deep-table matches at the top after a cold mount.""" @@ -907,7 +907,7 @@ def _anchor_region(self, target: Widget, table: DataTable[Any] | None) -> Region def _last_match_pending(self, chunk_md: Widget | None) -> bool: """Whether a matching table cell of this chunk has yet to resolve its - region — the same race ``_anchor_region`` returns ``None`` for on the + region: the same race ``_anchor_region`` returns ``None`` for on the first-match path.""" from textual.widgets import DataTable @@ -926,7 +926,7 @@ def _last_match_pending(self, chunk_md: Widget | None) -> bool: def _last_match_entry( self, focus_chunk_seq: int, target: Widget, chunk_md: Widget | None ) -> tuple[Widget, int] | None: - """``(widget, row within it)`` a backward hand-over enters at — the LAST + """``(widget, row within it)`` a backward hand-over enters at: the LAST match of the chunk, by the row it PAINTS on, never by ``match_blocks`` order (see :func:`~fnd.tui.preview.match_row.chunk_stop_rows`).""" from fnd.tui.preview.match_row import chunk_stop_rows @@ -1009,13 +1009,13 @@ def _scroll_proxy_for(self, inner: Widget, *, chunk: FNDMarkdown) -> Widget: """Resolve a scroll target for an ``FNDMarkdown.first_match_block``. Most blocks (Paragraph / H#, ListItem, BlockQuote) have valid - regions — use them directly. Table cells (TH/TD) carry the + regions: use them directly. Table cells (TH/TD) carry the highlight bookkeeping but never get laid out: the parent ``MarkdownTable`` composes a ``MarkdownTableContent`` whose ``MarkdownTableCellContents`` children render in a grid. For that case, find the cell widget that holds the matched ``Content`` and scroll to it directly. Bounded by the number - of cells in the chunk's tables — no full descendant walk. + of cells in the chunk's tables: no full descendant walk. """ from fnd.tui.widgets.markdown import FNDMarkdownTableDT @@ -1167,7 +1167,7 @@ def _restore_structural( ) -> None: # A width reflow re-wraps the chunks above over several refreshes, # which keeps sliding the target chunk's content position. Track that - # content position (``virtual_region.y`` — scroll-independent) and + # content position (``virtual_region.y``: scroll-independent) and # re-apply until it stops moving; watching ``scroll_y`` instead settles # early (the offset repeats while the layout is still flowing) and # lands a chunk off. Scroll numerically to the chunk's content top plus @@ -1199,7 +1199,7 @@ def _restore_structural( # one, so the restore ran out of refreshes and left the target off # screen. Top the budget back up while the preview pipeline is still # working, so the tail refreshes are spent on a layout that has actually - # stopped changing. Bounded by ``cap`` — a wedged pipeline must not hold + # stopped changing. Bounded by ``cap``: a wedged pipeline must not hold # a re-anchor loop open forever. if cap > 0 and self._host.pipeline_busy(): retries = max(retries, _RESTORE_TAIL_REFRESHES) @@ -1208,7 +1208,7 @@ def _restore_structural( self._restore_structural, seq, delta, retries - 1, vy, done, cap - 1 ) else: - # Exhausting the budget (or the cap) still ENDS the restore — the + # Exhausting the budget (or the cap) still ENDS the restore: the # flag must not outlive the loop that set it. done() @@ -1322,7 +1322,7 @@ class FlatScrollStrategy: own first-match / chunk-top fallback handles the rest. Deliberately tiny next to :class:`StructuralScrollStrategy`: the buffer is - one widget that already knows every row, so the scroll is synchronous — no + one widget that already knows every row, so the scroll is synchronous; no retry chain, no settle barrier, nothing to wait on a build for. """ @@ -1362,7 +1362,7 @@ def reconcile( start = view.address_of_chunk(seq) row = None if address is None or start is None else address - start self.landing = Landing(anchor, seq, row) - # The view scrolls synchronously, so it has already landed — reveal + # The view scrolls synchronously, so it has already landed: reveal # immediately. if on_settled is not None: on_settled() @@ -1393,7 +1393,7 @@ def hold_location(self, location: ViewportLocation) -> None: absorb.""" def locate(self) -> ViewportLocation | None: - """The address at the viewport top — exact across a width reflow, which + """The address at the viewport top: exact across a width reflow, which a raw visual row would not be (the flat buffer re-wraps; a frozen document re-freezes).""" view = self._view() @@ -1412,7 +1412,7 @@ def scroll_to_location( view = self._view() if view is None: return - # Exact (no context margin) — restore the *reading* position, not a + # Exact (no context margin): restore the *reading* position, not a # match drop. The view re-renders for the new width first. view.scroll_to_address(location.line, context_fraction=0.0) finally: diff --git a/fnd/tui/preview_scrollbar.py b/fnd/tui/preview_scrollbar.py index a8169bca..5eea4c57 100644 --- a/fnd/tui/preview_scrollbar.py +++ b/fnd/tui/preview_scrollbar.py @@ -7,13 +7,13 @@ This module wires three thin subclasses: -* :class:`MatchAwareScrollBarRender` — overrides Textual's renderer to +* :class:`MatchAwareScrollBarRender`, overrides Textual's renderer to swap the blank track glyph for an accent ``▌`` block at every row that carries a match. -* :class:`MatchAwareScrollBar` — passes per-instance match data into the +* :class:`MatchAwareScrollBar`, passes per-instance match data into the custom renderer (Textual's default ``ScrollBar.render`` instantiates the renderer class without per-instance args). -* :class:`MatchAwareScroll` — a ``VerticalScroll`` whose vertical scrollbar +* :class:`MatchAwareScroll`; a ``VerticalScroll`` whose vertical scrollbar is a :class:`MatchAwareScrollBar`. Two marker-mapping modes coexist: @@ -24,14 +24,14 @@ so a single big chunk and many tiny ones each get a marker at the right visual position. Used by the flat-buffer :class:`LineBufferPreview` pipeline. -* **Chunk-uniform** (legacy) — driven by ``set_match_map(match_map)`` +* **Chunk-uniform** (legacy): driven by ``set_match_map(match_map)`` where ``match_map[i]`` flags chunk ``i`` as match-bearing. Each cell on the track resolves back to a chunk index proportionally and paints if that chunk matches. Used by the structural Markdown / docx / pptx preview path. -A container built behind ``-pre-reveal`` is in the layout — that is how the pane -scrolls to its match before revealing it — so its rows reach the bar as well and +A container built behind ``-pre-reveal`` is in the layout (that is how the pane +scrolls to its match before revealing it) so its rows reach the bar as well and resized the thumb on every navigation, measured 5 cells to 3 and back. :attr:`MatchAwareScroll.staged_rows` reports them and :meth:`MatchAwareScrollBar._render_bar` subtracts them, but only while the @@ -76,11 +76,11 @@ _MARKER_COLOR = "#ffd866" _MARKER_GLYPH = "▌" -# The thumb is one box-drawing line per cell — the same weight as the pane's +# The thumb is one box-drawing line per cell: the same weight as the pane's # ``round`` border, so the bar reads as part of the frame rather than a fat # block. Vertical bars use ``│``, horizontal bars the matching ``─``. Gappy on # Apple Terminal / SF Mono (font-determined, same class as the box-border -# issue) — a documented "use a modern terminal" case. +# issue): a documented "use a modern terminal" case. _THUMB_GLYPH = "│" _THUMB_GLYPH_HORIZONTAL = "─" @@ -93,15 +93,15 @@ class ThinScrollBarRender(ScrollBarRender): code fences, settings lists) matches: * The thumb is a single box-drawing line (``│`` vertical, ``─`` horizontal) - per cell in the bar colour over a transparent track — the pane border's + per cell in the bar colour over a transparent track: the pane border's line weight, not a reverse-video full-cell block. * The thumb is a **constant** integer number of cells. The stock renderer packs sub-cell precision into partial-block end caps, so its cell count flickers ±1 as you scroll (the thumb visibly resizes). Here the count is derived once from the window/content ratio and only the position moves. - Covers both orientations so every Textual scrollbar — including the - horizontal bars on wide tables and unwrapped code fences — reads the same. + Covers both orientations so every Textual scrollbar, including the + horizontal bars on wide tables and unwrapped code fences: reads the same. """ def _thin_segments( @@ -113,7 +113,7 @@ def _thin_segments( vertical: bool = True, ) -> list[Segment]: glyph = _THUMB_GLYPH if vertical else _THUMB_GLYPH_HORIZONTAL - # ``back_color`` may be None (transparent track) — don't fabricate a bg. + # ``back_color`` may be None (transparent track): don't fabricate a bg. track = RichStyle(bgcolor=back_color) if size <= 0: return [] @@ -123,7 +123,7 @@ def _thin_segments( # Whole content visible (or degenerate): blank track, no thumb. return [Segment(" ", track) for _ in range(size)] # Constant thumb height from the window/content ratio (≥1 cell). Depends - # only on window/virtual/size — not position — so scrolling never resizes + # only on window/virtual/size, not position, so scrolling never resizes # it; only ``top`` moves. thumb = max(1, min(size, round(window * size / virtual))) max_top = size - thumb @@ -224,7 +224,7 @@ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderR marker_cells = self._marker_cells(size) if marker_cells: - # Thumb cells carry the ``@mouse.down: grab`` meta — leave those + # Thumb cells carry the ``@mouse.down: grab`` meta, leave those # untouched so the current scroll position still reads as a thumb. marker_color = RichColor.parse(_MARKER_COLOR) for i, seg in enumerate(segments): @@ -333,7 +333,7 @@ def _render_bar(self, scrollbar_style: Any) -> Any: def _is_displayed(widget: Widget) -> bool: - """Whether ``widget`` still occupies layout — an ancestor with + """Whether ``widget`` still occupies layout: an ancestor with ``display: none`` takes it out without detaching it.""" from textual.widget import Widget as _Widget @@ -367,7 +367,7 @@ class MatchAwareScroll(VerticalScroll): # scroll is essentially always 0 in practice). Overrides the parent # ``scroll_left`` binding so a single Left exits the pane instead of # paging horizontally into empty space. - BINDINGS = [ # noqa: RUF012 — Textual widget BINDINGS expects a class-level list + BINDINGS = [ # noqa: RUF012, Textual widget BINDINGS expects a class-level list Binding("left", "bridge_left", "Back to the sidebar", show=False), # The footer advertises `j/k Scroll` here and the rest of the app speaks # vi keys (the settings list binds `up,k` / `down,j`), but Textual's @@ -378,7 +378,7 @@ class MatchAwareScroll(VerticalScroll): # Lines advanced per scroll-key press in Reading View. Reading View turns # off mouse reporting (so native select/copy/TTS work), which makes the - # terminal translate each wheel notch into a *stream* of arrow keys — one + # terminal translate each wheel notch into a *stream* of arrow keys: one # 1-line scroll + full repaint each. Advancing several lines per key lets a # wheel/momentum burst cover ground in far fewer repaints, so scrolling # feels responsive instead of crawling. Non-animated: during a burst, @@ -472,12 +472,12 @@ def _absorb_prepend(self, arrangement: DockArrangeResult, size: Size) -> None: # Measured from the ANCHOR, not from the height change: only content # inserted above it moves it, so a background fill appending BELOW is # correctly ignored. And the stored position advances each time, so a - # prepend arriving in instalments — built chunks resolve their heights - # over successive layouts — is absorbed in full rather than once. + # prepend arriving in instalments, built chunks resolve their heights + # over successive layouts, is absorbed in full rather than once. widget, before_y = claim anchor = cast("Widget", widget) # A widget whose container has left the layout reports virtual_region - # ``Region()`` — y=0, no exception — so a stranded claim would read as a + # ``Region()`` (y=0, no exception) so a stranded claim would read as a # prepend of -before_y and really scroll the pane: measured 117-187 rows # backwards on a cross-file navigation. Liveness is the only thing that # tells that apart from a genuine y=0. @@ -502,8 +502,8 @@ def _absorb_prepend(self, arrangement: DockArrangeResult, size: Size) -> None: # An in-flight animation is RETARGETED, not stopped: it holds # start and end values in pre-prepend coordinates and would # otherwise drive scroll_y back and undo the absorb, while - # force-stopping COMPLETES it — Textual sets the attribute to - # the end value — teleporting the reader to their destination. + # force-stopping COMPLETES it, Textual sets the attribute to + # the end value, teleporting the reader to their destination. with contextlib.suppress(Exception): animator = self.app.animator animation = animator._animations.get((id(self), "scroll_y")) @@ -539,7 +539,7 @@ def _anchor_row(self, arrangement: DockArrangeResult, anchor: Widget) -> int | N Same coordinate as ``virtual_region.y``, which cannot be used here: it reports the map this pass is about to replace. Asking the parent costs one container arrange whenever Textual measured it at its own height - instead (its all-dynamic-height branch) — 0.26ms at 40 chunks. + instead (its all-dynamic-height branch): 0.26ms at 40 chunks. """ from textual.widget import Widget as _Widget @@ -560,7 +560,7 @@ def _set_absorbed_scroll(self, value: float, max_y: int) -> None: ``validate_scroll_y`` clamps against the ``virtual_size`` this arrangement is about to replace, so at the end of a document a plain - write drops the absorb entirely — measured 116 where 216 was wanted. + write drops the absorb entirely: measured 116 where 216 was wanted. """ from textual.reactive import Reactive from textual.widget import Widget as _Widget @@ -587,7 +587,7 @@ def watch_scroll_y(self, old_value: float, new_value: float) -> None: # usable in isolation. super().watch_scroll_y(old_value, new_value) try: - # Call the LazyMounter directly — the app-level delegation shim + # Call the LazyMounter directly: the app-level delegation shim # (_schedule_preview_lazy_mount_check) was removed when the # delegation layer was dropped; this watcher kept calling the old # name and silently no-op'd, severing scroll-driven lazy mount. @@ -612,7 +612,7 @@ def watch_has_focus(self, has_focus: bool) -> None: None of this pane's descendants have CSS rules that depend on the pane's ``:focus`` state, so we apply styles to just this - node — the focus-indicator border on the pane itself. + node; the focus-indicator border on the pane itself. """ with contextlib.suppress(Exception): self.app.stylesheet.apply(self) diff --git a/fnd/tui/progress/__init__.py b/fnd/tui/progress/__init__.py index 7f52ba82..139fd5d2 100644 --- a/fnd/tui/progress/__init__.py +++ b/fnd/tui/progress/__init__.py @@ -2,13 +2,13 @@ Split across: -* ``model`` — ``Phase`` / ``OperationPlan`` / ``ProgressModel``: what an +* ``model``: ``Phase`` / ``OperationPlan`` / ``ProgressModel``: what an operation looks like and how its phases become one fraction. Pure. -* ``calibration`` — what each phase actually cost, per machine. -* ``bar`` — the widget that paints the strip. -* ``facility`` — ``ProgressFacility`` / ``ProgressSession``, the API callers +* ``calibration``: what each phase actually cost, per machine. +* ``bar``: the widget that paints the strip. +* ``facility``: ``ProgressFacility`` / ``ProgressSession``, the API callers use, plus the visibility policy and the arbitration between classes of work. -* ``operations`` — the concrete plans and the trackers that drive them. This +* ``operations``: the concrete plans and the trackers that drive them. This is the only module that knows about any particular subsystem. """ diff --git a/fnd/tui/progress/bar.py b/fnd/tui/progress/bar.py index b953b44d..2a2212a5 100644 --- a/fnd/tui/progress/bar.py +++ b/fnd/tui/progress/bar.py @@ -1,12 +1,12 @@ -"""The progress line — one row spanning the full width of the panes. +"""The progress line: one row spanning the full width of the panes. Blank at rest. The row itself is always reserved (``visibility: hidden``, not ``display: none``) so showing it never reflows the panes above, but nothing is painted until an operation starts. Drawn as one box-drawing glyph per cell in the pane border's weight, -following :class:`fnd.tui.preview_scrollbar.ThinScrollBarRender` — the -filled run in the accent, the remainder as a dim rule — so the line +following :class:`fnd.tui.preview_scrollbar.ThinScrollBarRender` (the +filled run in the accent, the remainder as a dim rule) so the line reads as part of the frame rather than as a widget bolted underneath it. Integer cells only; no partial-block end caps, which flicker. """ @@ -27,7 +27,7 @@ # Both runs are the pane border's own rule, so the line sits in the frame at # exactly the weight of the borders it sits under; only colour separates the # filled part from the remainder. A heavier glyph for the fill was tried first -# and read as too loud — progress is ambient information, not an alert, and it +# and read as too loud: progress is ambient information, not an alert, and it # should not compete with the content for the eye. If it needs to recede # further, dim the accent rather than changing the glyph again. FILL_GLYPH = "─" @@ -49,8 +49,8 @@ def progress_line_segments( track_style: RichStyle | None = None, label_style: RichStyle | None = None, ) -> list[Segment]: - """Render one progress row. Pure — no widget, no console, no styles - resolution — so the layout rules below are unit-testable directly.""" + """Render one progress row. Pure: no widget, no console, no styles + resolution, so the layout rules below are unit-testable directly.""" if width <= 0: return [] fraction = 0.0 if fraction < 0.0 else (1.0 if fraction > 1.0 else fraction) @@ -114,7 +114,7 @@ class FNDProgressBar(Widget): /* visibility:hidden keeps the row, so toggling never reflows the panes above. */ FNDProgressBar.-idle { visibility: hidden; } FNDProgressBar > .progress--fill { color: $accent; } - /* Ambient work — a background reindex — in the same accent at half + /* Ambient work (a background reindex) in the same accent at half strength. Two operations can never share the line, so this is what separates "the thing I just asked for" from "something running on its own": a line that appears without the user touching anything is diff --git a/fnd/tui/progress/calibration.py b/fnd/tui/progress/calibration.py index 55aed679..0b5b9447 100644 --- a/fnd/tui/progress/calibration.py +++ b/fnd/tui/progress/calibration.py @@ -7,7 +7,7 @@ Two departures from the indexer's calibrator, both deliberate: -* **Median, not mean.** Navigation durations are heavy-tailed — one 8 s +* **Median, not mean.** Navigation durations are heavy-tailed: one 8 s outlier (a cold monster PDF, a machine under load) would drag a five-sample mean far off the typical case and leave the bar crawling for every normal navigation afterwards. @@ -37,7 +37,7 @@ from fnd.tui.progress.model import OperationPlan # How many recent runs of an operation to summarise. Wider than the -# indexer's 5 because these samples are individually noisy — a median over +# indexer's 5 because these samples are individually noisy: a median over # 9 rides out a couple of outliers without going stale. _SAMPLE_SIZE = 9 # Total records kept on disk across all operations. @@ -71,7 +71,7 @@ def _usable(ms: float) -> bool: ``json.loads`` accepts ``Infinity`` and ``NaN``, and an infinite value passes a bare ``>=`` comparison. It then reaches ``OperationPlan.weights``, - where ``inf / inf`` is ``nan`` and the fraction stops advancing at all — a + where ``inf / inf`` is ``nan`` and the fraction stops advancing at all: a corrupt line in this file would freeze the bar rather than be ignored, and corrupt lines are already an expected case here. """ @@ -96,8 +96,8 @@ def _load() -> None: continue # KeyError too: a line can be valid JSON and still lack "phases" # or "operation_id", and KeyError is a subclass of none of the - # others. It escaped _load, which runs from ProgressFacility.begin - # — and SearchController.run does not guard that call, so one + # others. It escaped _load, which runs from ProgressFacility.begin, + # and SearchController.run does not guard that call, so one # corrupt history line broke the next search. The module docstring # promises every parse path here is suppressed; it now is. with contextlib.suppress(json.JSONDecodeError, TypeError, ValueError, KeyError): @@ -133,7 +133,7 @@ def record(operation_id: str, observed_ms: Mapping[str, float]) -> None: def expected_ms(operation_id: str) -> dict[str, float]: """Median observed duration per phase over the recent runs of - ``operation_id``. Phases never observed are absent — the caller keeps + ``operation_id``. Phases never observed are absent: the caller keeps the plan's seed for those.""" _load() recent = [r for r in _store.records if r.operation_id == operation_id][-_SAMPLE_SIZE:] diff --git a/fnd/tui/progress/facility.py b/fnd/tui/progress/facility.py index 972401e7..b6f60aa5 100644 --- a/fnd/tui/progress/facility.py +++ b/fnd/tui/progress/facility.py @@ -9,7 +9,7 @@ feedback belongs to the keypress that caused it. * **Minimum visible duration.** Work that finishes inside :data:`_MIN_VISIBLE_S` still shows a complete fill instead of a flash. - Fast work is not hidden — a load the user can *see* complete is what + Fast work is not hidden: a load the user can *see* complete is what makes the app feel fast. * **Completion is always painted.** A session eases to 1.0 and holds before clearing, so the line never vanishes part-filled. @@ -19,7 +19,7 @@ Ownership: a session can only be closed by whoever holds it. The old facility closed whatever happened to be active, so any stale teardown -path could retire a newer operation's bar — the "disappears after half a +path could retire a newer operation's bar: the "disappears after half a second" failure. **Arbitration.** One line, two classes of work (see @@ -53,7 +53,7 @@ _MIN_VISIBLE_S = 0.40 # How long the full line is held before it clears. This is the "done" the # user reads, so it has to survive a loop that may only tick a few times a -# second — hence a hold rather than an animation. +# second, hence a hold rather than an animation. _COMPLETE_HOLD_S = 0.35 # A new session starting within this long of the last one ending inherits # its fill (see _HANDOFF_FLOOR_CAP). @@ -65,14 +65,14 @@ # How long the line may stay up without a SUBSTANTIVE update before it is # retired: a phase change, a change in reported units, or a change of label. # -# Deliberately not "since the last begin" — the paint check re-enters +# Deliberately not "since the last begin": the paint check re-enters # ``render_full_doc`` on a failed reveal and a burst of navigation supersedes # constantly, so a per-session budget handed a stuck line a fresh one every # time and it outlived any cap. # # And deliberately not "since the fill last moved". The eased fraction creeps # for as long as a phase runs, so a stuck operation drifts a cell every second -# or two indefinitely — movement that comes from the model guessing, not from +# or two indefinitely: movement that comes from the model guessing, not from # anything actually happening. Only real events count. # # Set well clear of the measured phase durations (the slowest observed is a @@ -83,18 +83,18 @@ # ends the session on ``task.done()``, a real asyncio result, where the # preview tracker can only infer completion from heuristics. So this is a # pure safety net rather than the normal path, and it is set beyond any -# plausible quiet stretch — a source walk over an evicted cloud vault can +# plausible quiet stretch: a source walk over an evicted cloud vault can # genuinely report nothing for minutes. _AMBIENT_STALL_CAP_S = 600.0 # An active session paints at most this much. A full line is reserved for the -# completion animation, so "full" always means finished — and a stall while +# completion animation, so "full" always means finished, and a stall while # working is visibly distinct from a stall in the clear. _ACTIVE_CEILING = 0.97 # Independent one-shot backstop, on its own timer, so a fault in the tick loop # cannot leave a line on screen. Measured from the same instant as the stall # cap, so it MUST sit clearly above it: at 8 s against a 10 s stall cap it beat # the tick loop to every retirement, which made the stall path dead code and -# routed every case through _force_clear — a superseded retirement, so the +# routed every case through _force_clear; a superseded retirement, so the # operation lost both its completion animation and its calibration sample. # The tick loop is the normal path; this only covers a tick loop that is gone. _WATCHDOG_MARGIN_S = 5.0 @@ -118,8 +118,8 @@ class ProgressTracker(Protocol): """A subsystem's adapter onto the line. This is the one seam every subsystem shares. A tracker knows how to - read its own pipeline and translate whatever it counts — rendered - lines, mounted chunks, indexed files — into the plan's phases and a + read its own pipeline and translate whatever it counts (rendered + lines, mounted chunks, indexed files) into the plan's phases and a ``report(done, total)``; the facility knows nothing about any of them. That translation at the boundary is what lets a single line serve operations with no unit in common. @@ -218,7 +218,7 @@ def report(self, done: float, total: float) -> None: self._facility.render(self) def set_label(self, label: str) -> None: - """Set the trailing text. Empty clears it — the line carries no + """Set the trailing text. Empty clears it: the line carries no label unless it says something the user does not already know.""" if self._closed or label == self._label: return @@ -226,7 +226,7 @@ def set_label(self, label: str) -> None: self._touch() def close(self) -> None: - """Finish this operation. A no-op if a newer session took over — + """Finish this operation. A no-op if a newer session took over: only the holder can retire its own bar.""" if self._closed: return @@ -238,7 +238,7 @@ def abandon(self) -> None: Closing paints the line to full and records the run for calibration, which is right for work that completed and a lie about work the user - stopped — a cancelled warm filled its bar to 100% next to a toast + stopped: a cancelled warm filled its bar to 100% next to a toast saying it had been stopped. """ if self._closed: @@ -323,7 +323,7 @@ def displayed(self) -> ProgressSession | None: Interactive first: an ambient run keeps ticking underneath but must never paint over the operation the user is waiting on. Nothing is - displayed during a completion hold — that full line has been earned + displayed during a completion hold: that full line has been earned by the session that just finished. """ if self._completing_at is not None: @@ -338,7 +338,7 @@ def _owns_line(self, session: ProgressSession) -> bool: """Whether ``session`` is the one the line is speaking for. Asked by slot rather than by :meth:`displayed`, because it is asked - during retirement — and ``close()`` sets the closed flag before the + during retirement, and ``close()`` sets the closed flag before the facility has cleared the slot, so ``displayed`` would already have moved on. That made every completing session look undisplayed, and the line stopped painting its own completion. @@ -421,7 +421,7 @@ def tick(self) -> None: Nothing in here may raise. Textual's ``Timer._tick`` hands a callback exception to ``App._handle_exception``, which takes the whole app - down — so an arithmetic slip in a progress bar would kill the session + down, so an arithmetic slip in a progress bar would kill the session the progress bar exists to serve. """ try: @@ -488,7 +488,7 @@ def _sample(self, session: ProgressSession) -> bool | None: def _on_close(self, session: ProgressSession) -> None: if session is not self._active and session is not self._ambient: # A stale holder retiring itself. Its bar already belongs to - # someone else — leave it alone. + # someone else: leave it alone. return self._retire(session, superseded=False) @@ -511,14 +511,14 @@ def _retire(self, session: ProgressSession, *, superseded: bool) -> None: now = self._clock() if superseded: # Abandoned work teaches nothing. Recording its finished phases - # looked harmless — they are real measurements — but it feeds a + # looked harmless (they are real measurements) but it feeds a # loop: an operation that gets stuck and re-dispatched writes long # durations, which raise the plan's expected total, which raises # the visibility cap, which lets the next stuck one stay up longer. # Only operations that actually completed get to set the pace. # # The successor takes the line over immediately, so there is no - # completion animation — and it inherits the fill so a held-down + # completion animation, and it inherits the fill so a held-down # cursor doesn't saw the bar back to zero every keypress. if session.kind is OperationKind.INTERACTIVE: self._last_end = now @@ -542,7 +542,7 @@ def _retire(self, session: ProgressSession, *, superseded: bool) -> None: self._completing_ambient = session.kind is OperationKind.AMBIENT self._completing_shown_at = session._shown_at # Paint the full line NOW, synchronously, rather than easing to it over - # the next few ticks. The ease depended on the event loop being free — + # the next few ticks. The ease depended on the event loop being free, # and this loop is saturated during exactly the operations the line # reports on. Measured: at a 300ms tick gap (routine during a cold # navigation, where the loop blocks for 400-1274ms at a stretch) the @@ -568,7 +568,7 @@ def render(self, session: ProgressSession | None = None) -> None: if session is None or session is not self.displayed(): return # An ACTIVE session never paints a full line. A full line means "this - # finished", and only the completion animation is entitled to say so — + # finished", and only the completion animation is entitled to say so, # otherwise a session whose phases have all eased out looks done while # it is still working, and a stall there is indistinguishable from a # stall in the clear. @@ -584,8 +584,8 @@ def render(self, session: ProgressSession | None = None) -> None: def _render_completing(self, now: float) -> None: """Hold the full line, then clear it. - The fill is already at 1.0 — painted synchronously when the session - retired — so this only decides WHEN to put it away. Holding until both + The fill is already at 1.0, painted synchronously when the session + retired, so this only decides WHEN to put it away. Holding until both budgets are spent is what turns a 40 ms cache hit from a flash into a legible "done", which is the point: fast work the user can see finish is what makes the app feel fast. @@ -631,7 +631,7 @@ def _clear(self) -> None: def note_progress(self, session: ProgressSession | None = None) -> float: """Something real happened: a phase advanced, units changed, or the - label changed. That is what earns the line more time — the eased fill + label changed. That is what earns the line more time: the eased fill moving on its own does not. Returns the timestamp to record.""" now = self._clock() kind = session.kind if session is not None else OperationKind.INTERACTIVE @@ -646,7 +646,7 @@ def _paint(self, fraction: float, label: str, *, ambient: bool, visible: bool) - # Only touch the widget when the CELLS would differ. ``fraction`` is a # Textual reactive, and the eased value changes by a fraction of a # percent on every tick, so assigning it unconditionally repainted the - # row 20 times a second to draw the identical thing — event-loop work + # row 20 times a second to draw the identical thing: event-loop work # during exactly the navigation this line exists to make feel smoother. # Quantise against the width actually on screen. width = max(1, widget.content_size.width) @@ -680,7 +680,7 @@ def _timer_alive(self) -> bool: """Whether the stored timer is still running. ``Timer.stop()`` cancels its task and drops the reference, so ``_task`` - is the liveness signal. (``_active`` is NOT — that is the pause flag, + is the liveness signal. (``_active`` is NOT: that is the pause flag, and ``stop`` actually *sets* it.) Absent the attribute, assume alive: the watchdog is the guarantee here, not this check. """ @@ -689,7 +689,7 @@ def _timer_alive(self) -> bool: return False if not hasattr(timer, "_task"): # A Textual upgrade renamed or removed the attribute. Degrade to - # "assume alive" — the watchdog is the guarantee — but say so once, + # "assume alive" (the watchdog is the guarantee) but say so once, # because the degraded path is silent otherwise. if not self._warned_timer_probe: self._warned_timer_probe = True @@ -710,7 +710,7 @@ def _arm_watchdog(self, delay: float) -> None: Everything else here runs on the repeating timer, so any fault in that timer strands a visible line. This is a separate one-shot timer whose only job is to put the line away. Re-armed when the line MOVES, not - when a session begins — see the note on _STALL_CAP_S. Its delay tracks + when a session begins: see the note on _STALL_CAP_S. Its delay tracks the stall cap of whichever class of work is on the line, so an ambient run's much looser budget is not cut short by a backstop meant for a navigation. @@ -732,7 +732,7 @@ def _force_clear(self) -> None: self._watchdog = None widget = self._widget() if widget is None: - # The line cannot be resolved right now — a modal screen on top of + # The line cannot be resolved right now: a modal screen on top of # the stack is the expected case, and an indexing run deliberately # sits behind one. Dropping the backstop here would retire it for # the rest of the session, because only real progress re-arms it @@ -747,11 +747,11 @@ def _force_clear(self) -> None: if session is not None: # Retire it as COMPLETED, not superseded: a superseded retirement # skips the completion entirely, so the line would vanish - # part-filled — the exact symptom this whole backstop exists to + # part-filled: the exact symptom this whole backstop exists to # prevent. We have given up, but the user reads a line that # resolves; the diagnostic above records the truth. session.close() - # close() paints the full line and leaves the hold to the tick loop — + # close() paints the full line and leaves the hold to the tick loop, # but this backstop exists precisely for the case where that loop is # gone, so schedule the clear independently rather than trust it. with contextlib.suppress(Exception): diff --git a/fnd/tui/progress/model.py b/fnd/tui/progress/model.py index 8025605d..21035acf 100644 --- a/fnd/tui/progress/model.py +++ b/fnd/tui/progress/model.py @@ -1,18 +1,18 @@ """Phase-weighted progress model. -Pure — no Textual, no I/O, injected clock. An operation declares an +Pure: no Textual, no I/O, injected clock. An operation declares an :class:`OperationPlan`: an ordered set of phases, each with an expected duration. The model turns "which phase are we in, and how far through it" into one monotonic 0..1 fraction. A phase's **weight is its share of the plan's total expected duration**, -so a calibrated expectation reshapes the bar automatically — there are +so a calibrated expectation reshapes the bar automatically: there are no hand-tuned weights to drift out of step with what the code actually does. Phases with countable units (chunks mounted, files indexed) report a -real fraction. Phases with nothing to count — a single -``await build_done``, a layout settle — ease on elapsed time against +real fraction. Phases with nothing to count (a single +``await build_done``, a layout settle) ease on elapsed time against their expectation, so the line keeps moving through the waits that dominate the budget without ever claiming a completion it has not reached. @@ -33,7 +33,7 @@ # # This single number balances the two halves of the original complaint. Too # low and a correctly-estimated operation finishes with the bar partway (at -# 0.5 — a plain hyperbolic — the flat path completed at a median fill of +# 0.5 (a plain hyperbolic) the flat path completed at a median fill of # 0.167). Too high and there is no headroom left for a phase that overruns, so # the fill stops moving: at 0.9 a phase running 5x its estimate painted the # same cell for 1.85s, which is the freeze this exists to prevent. @@ -51,7 +51,7 @@ class OperationKind(Enum): The distinction earns its place because the two classes have opposite needs and one line to share. INTERACTIVE work answers something the user just did, so it must take the line immediately. AMBIENT work runs - on its own schedule — a background reindex started by auto-resume — + on its own schedule (a background reindex started by auto-resume) and lasts orders of magnitude longer, so it must *yield* the line and get it back, rather than fight for it. """ @@ -66,7 +66,7 @@ class Phase: ``expected_ms`` is a seed; :mod:`fnd.tui.progress.calibration` replaces it with what this machine actually measured. ``countable`` marks a phase whose - caller can supply real units — it eases on time until the first + caller can supply real units: it eases on time until the first :meth:`ProgressModel.report`. """ @@ -171,7 +171,7 @@ def is_complete(self) -> bool: def enter(self, key: str) -> None: """Advance to ``key``; every earlier phase counts finished. - Re-entering the current phase, or naming an earlier one, is ignored — + Re-entering the current phase, or naming an earlier one, is ignored: an operation only ever moves forwards. """ if self._complete: @@ -184,7 +184,7 @@ def enter(self, key: str) -> None: if current is not None: current.ended = now self._index = target - # Phases stepped over were never observed, so they get no run record — + # Phases stepped over were never observed, so they get no run record: # calibration must not learn from a duration nobody measured. self._runs[key] = _PhaseRun(started=now) self.tick() @@ -247,7 +247,7 @@ def _phase_fraction(self) -> float: # # A plain hyperbolic (elapsed / (elapsed + expected)) reads HALF the # phase at exactly its expected duration, so even a perfectly - # calibrated operation finished with the bar at ~50% — measured on the + # calibrated operation finished with the bar at ~50%: measured on the # flat path, median fill at completion was 0.167. That is the # "it pauses partway" complaint, and no amount of fixing the # expectations cures it, because the curve itself is the cause. diff --git a/fnd/tui/progress/operations.py b/fnd/tui/progress/operations.py index 81727a98..9d2a6015 100644 --- a/fnd/tui/progress/operations.py +++ b/fnd/tui/progress/operations.py @@ -2,7 +2,7 @@ Each subsystem the line serves contributes two things and nothing else: -* an :class:`~fnd.tui.progress.model.OperationPlan` — what its phases are, +* an :class:`~fnd.tui.progress.model.OperationPlan`, what its phases are, what each is expected to cost, and whether the user is waiting on it (:class:`~fnd.tui.progress.model.OperationKind`); * a tracker satisfying :class:`~fnd.tui.progress.facility.ProgressTracker`, @@ -13,7 +13,7 @@ with no unit in common. Rendered lines, mounted chunks and indexed files never meet; each tracker turns its own units into ``report(done, total)`` and the phase weights turn the rest into one 0..1 fraction. Adding a -subsystem means adding a plan and a tracker — the facility, the widget +subsystem means adding a plan and a tracker: the facility, the widget and the calibration store need no knowledge of it. Seed durations come from the measured navigation budget; @@ -24,7 +24,7 @@ from inside the pipeline. Two reasons, both load-bearing: * The old design put a ``show``/``hide`` pair at every exit of the mount - path — sixteen call sites, each guarded by hand — and any stale one + path (sixteen call sites, each guarded by hand) and any stale one could retire a newer navigation's bar. Deriving "is anything in flight?" from the pipeline's own signals cannot go stale. * It keeps this work out of the mount/scroll code that is being changed @@ -46,7 +46,7 @@ # ── Preview navigation ─────────────────────────────────────────── # # Cold: a file the pane is not already showing. Seeds are the medians of -# the per-phase budget measured on a 1018-chunk PDF — decode off-thread, +# the per-phase budget measured on a 1018-chunk PDF: decode off-thread, # the ±window mount, the focus chunk's build (the single biggest item), # and the reconcile→scroll commit. PREVIEW_COLD = OperationPlan( @@ -61,7 +61,7 @@ # The FLAT path (PDF, TXT) installs a prebuilt document into one shared # LineBufferPreview: dispatch_flat_mount never assigns a mount task and leaves -# ``active`` as None, so ``mount`` and ``build`` are not slow on that path — +# ``active`` as None, so ``mount`` and ``build`` are not slow on that path; # they are unreachable. Giving a flat navigation the structural plan handed # 53% of the bar to phases it could never enter, so the fill was capped there # and the line stopped partway every time. A plan must only contain phases the @@ -78,7 +78,7 @@ # so no single expectation fits and the choice is which way to be wrong. The # curve is asymmetric: a phase that overruns creeps asymptotically from 0.78 # towards 0.97, which still reads as progress, while one that finishes early -# is capped proportionally — 55% of the expected duration means a bar that +# is capped proportionally, 55% of the expected duration means a bar that # stops at 0.43. For a high-variance phase, under-estimating is the graceful # failure. Two real-corpus runs measured medians of 1325 ms and 1767 ms. PREVIEW_COLD_FLAT = OperationPlan( @@ -92,12 +92,12 @@ ) # Warm: no decode to do, because the chunks are already cached. Same phases -# so the observer needs no special case, but its own operation id — mixing a +# so the observer needs no special case, but its own operation id: mixing a # genuinely cold monster into these medians would make both estimates useless. # # These are measured, not guessed, and the first guess was badly wrong: warm # was seeded at 10/40/40/90 on the assumption that a cache hit is nearly free. -# On a real corpus a warm navigation costs ~1030 ms — the cache saves the +# On a real corpus a warm navigation costs ~1030 ms: the cache saves the # decode, not the mount, the focus build or the scroll commit. Two effects, # and the second is the one that showed on screen: every phase ran 8-15x its # expectation and spent its whole life in the eased overrun region, and the @@ -116,7 +116,7 @@ ), ) -# A query is INTERACTIVE — the user is waiting on it — even though it is the +# A query is INTERACTIVE (the user is waiting on it) even though it is the # one operation whose work happens entirely off the loop. SEARCH = OperationPlan( operation_id="search", @@ -143,7 +143,7 @@ # Warming one file whole, on request. AMBIENT for the same reason indexing is: # it outlasts any navigation by orders of magnitude, so it has to give the line # up to whatever the user does next and take it back afterwards. Countable -# because the unit is a chunk and there are hundreds — a fraction alone would +# because the unit is a chunk and there are hundreds: a fraction alone would # read as a stalled bar. Measured on a real corpus: 69s for an 854-chunk book, # 167s for a 719-chunk PDF, so the seed is an order of magnitude, not a promise. WARM_WHOLE_FILE = OperationPlan( @@ -178,11 +178,11 @@ def plan_for(self, parent_id: str) -> OperationPlan: Two axes, and both matter: - * **Warm vs cold** — asks ``showing_parent()`` rather than reading + * **Warm vs cold**: asks ``showing_parent()`` rather than reading ``active`` directly, because the flat path leaves ``active`` as None and reading it classified every flat navigation as cold, including a jump inside an already-open PDF. - * **Flat vs structural** — a plan must only contain phases its path can + * **Flat vs structural**: a plan must only contain phases its path can reach (see PREVIEW_COLD_FLAT). Decided through ``uses_markdown_renderer``, the same predicate the dispatcher routes on, so the two cannot drift. @@ -192,12 +192,12 @@ def plan_for(self, parent_id: str) -> OperationPlan: # # * the pane is already showing this file, so there is nothing to open; # * coverage has captured every listed hit, so each mount is a blit - # rather than a markdown build (see fnd/tui/preview/coverage.py) — + # rather than a markdown build (see fnd/tui/preview/coverage.py): # this is now the strongest predictor, and the chunk cache alone # cannot see it; # * the chunks are decoded, so at least the decode is skipped. # - # File size, tried first, is a far weaker signal — a 54x size range + # File size, tried first, is a far weaker signal: a 54x size range # produced only a 3.6x duration range, because the preview mounts a # fixed window however large the file is. preview = self._app._preview @@ -215,7 +215,7 @@ def _is_structural(self, parent_id: str) -> bool: """Whether this file will take the structural renderer. Answered from the search hits, which satisfy the same protocol as the - chunks the dispatcher inspects — so this works at ``begin`` time, before + chunks the dispatcher inspects, so this works at ``begin`` time, before anything has been decoded. Unknown files are treated as structural: that plan is a superset, so a wrong guess costs pacing rather than making a phase unreachable. @@ -250,12 +250,12 @@ def _arrived(self, preview: Any, scroll: Any) -> bool: excludes a container behind ``-pre-reveal``, ``showing_parent`` confirms it is the right file, and the scroll must have committed. Without the last one, arrival short-circuited the settle and the line - cleared while the view was still moving — measured at 3 navigations in + cleared while the view was still moving: measured at 3 navigations in 30, which is the "it vanished before it finished" failure rather than the lingering one. A scroll that never settles cannot strand this: arrival simply stays - false and the fallback below bounds it — ``_landing`` gives up after + false and the fallback below bounds it, ``_landing`` gives up after the app's own reveal watchdog, and ``pipeline_busy`` goes quiet. """ if self._target is None: @@ -290,7 +290,7 @@ def sample(self, session: ProgressSession) -> bool: elif self._landing(preview, scroll): self._advance(session, "land") - # Done when the MATCH IS ON SCREEN — not when the pipeline runs dry. + # Done when the MATCH IS ON SCREEN, not when the pipeline runs dry. # # Those were the same thing until the capture cache landed. Now the # mount keeps filling below the fold long after the visible window has @@ -310,7 +310,7 @@ def sample(self, session: ProgressSession) -> bool: bool(preview.pipeline_busy()) or self._landing(preview, scroll) ) if not busy: - # The work is over — so every phase of it is over, including the + # The work is over, so every phase of it is over, including the # ones no sample ever landed inside. # # This is not bookkeeping. An observer polling at 20 Hz only sees @@ -320,7 +320,7 @@ def sample(self, session: ProgressSession) -> bool: # dropped and the pipeline can pass through build and land between # two samples. Measured on a real corpus: 7 of 31 navigations # finished while the tracker still believed they were mounting, - # and mount is followed by 53% of the bar — so the line sat at a + # and mount is followed by 53% of the bar, so the line sat at a # median of 0.50 and then jumped to full. That jump is the # "pauses halfway, then completes" report. self._advance(session, session.plan.phases[-1].key) @@ -329,8 +329,8 @@ def sample(self, session: ProgressSession) -> bool: def _landing(self, preview: Any, scroll: Any) -> bool: """A navigation whose scroll has not committed yet. - This is the last phase and it carries real time — the measured - reconcile-to-scroll-commit window is 440-740ms — so getting it wrong + This is the last phase and it carries real time (the measured + reconcile-to-scroll-commit window is 440-740ms) so getting it wrong is expensive in both directions. Gating it on ``inflight_target`` (an earlier attempt) made it @@ -348,7 +348,7 @@ def _landing(self, preview: Any, scroll: Any) -> bool: So: still landing while the scroll is outstanding AND there is something on screen to land on (every reset path clears that) AND the pipeline went idle only recently. The grace is the app's own bound on - how long a reveal may take — past it, the navigation is over whatever + how long a reveal may take: past it, the navigation is over whatever the scroll controller believes. """ if not scroll.is_settling: @@ -364,7 +364,7 @@ def _advance(session: ProgressSession, phase: str) -> bool: """Enter ``phase`` if this session's plan has one. The flat and structural plans deliberately carry different phases, and - the pipeline signals are not exclusive — a structural mount left over + the pipeline signals are not exclusive: a structural mount left over from the previous navigation can still be in flight while a flat session is active. Entering a phase the plan does not contain would raise, and the sampler's catch-all would then retire the line rather @@ -414,7 +414,7 @@ def _report_decode(session: ProgressSession, preview: Any) -> None: The flat path's duration spans more than an order of magnitude (p25 226ms, p75 3135ms on a real corpus) and nothing observable at - dispatch predicts it — file size scales it by roughly its fourth root, + dispatch predicts it: file size scales it by roughly its fourth root, because only a window is ever mounted. Estimating it was therefore never going to be accurate, so the renderer counts its own lines and this reads them. Falls back to the timed estimate when the count is @@ -428,7 +428,7 @@ def _report_decode(session: ProgressSession, preview: Any) -> None: session.report(min(done, total), total) def _report_mount(self, session: ProgressSession, preview: Any) -> None: - """Fraction of the *window* mounted — not of the file. + """Fraction of the *window* mounted, not of the file. The old bar divided by ``len(chunks)``, so a 1018-chunk PDF whose mount only ever lands a ±7 window topped out near 1%. The window is @@ -442,7 +442,7 @@ def _report_mount(self, session: ProgressSession, preview: Any) -> None: # The window the mount ACTUALLY chose, when it has chosen one. It is # selected by rows with the chunk count only as a cap, so on a # tall-chunk format it can hold two chunks where the tunables suggest - # fifteen — and pricing it at fifteen means this phase never reads as + # fifteen, and pricing it at fifteen means this phase never reads as # finished. Falls back to the tunables before the mount has picked. window = getattr(container, "mount_window", 0) or ( tuning.VISIBLE_FIRST_ABOVE + tuning.VISIBLE_FIRST_BELOW + 1 @@ -456,7 +456,7 @@ def _report_mount(self, session: ProgressSession, preview: Any) -> None: class IndexProgressTracker: """Samples a running index so a background reindex has a visible cost. - An ``open_modal=False`` run — which is what auto-resume starts on launch — + An ``open_modal=False`` run, which is what auto-resume starts on launch, used to surface nothing but a toast, so the machine could be indexing for minutes with no indication. The IndexerScreen keeps its own richer display and stays the drill-in; this only mirrors the headline onto the line. @@ -503,7 +503,7 @@ def sample(self, session: ProgressSession) -> bool: def _label(self, service: Any, state: Any, total: int) -> str: """Value only: what is being indexed and how far in. - A slow PDF gets the page counter too — during a multi-minute + A slow PDF gets the page counter too: during a multi-minute texturising the file counter alone does not move, which is the exact shape of "it looks frozen". """ @@ -513,7 +513,7 @@ def _label(self, service: Any, state: Any, total: int) -> str: if collection: chain_total = getattr(service, "chain_total", 1) or 1 if chain_total > 1: - # Shared with IndexerScreen's title so the two cannot drift — + # Shared with IndexerScreen's title so the two cannot drift, # and so this keeps the clamp, which it was missing: a state # where chain_remaining still holds every collection rendered # as "Work (0 of 4)". diff --git a/fnd/tui/results_labels.py b/fnd/tui/results_labels.py index 4345b25a..4111781a 100644 --- a/fnd/tui/results_labels.py +++ b/fnd/tui/results_labels.py @@ -36,7 +36,7 @@ _PASS_GLYPHS = {0: "●", 1: "~", 2: "⊕", 3: "❝"} # The engine matched this chunk, but nothing in the text the preview renders -# carries a paintable span — selecting the row lands somewhere with no visible +# carries a paintable span: selecting the row lands somewhere with no visible # highlight. The row is still listed (the engine's match is what makes a result # a result); the glyph says the match is there but couldn't be located, so a # highlighting regression shows up as marks on screen instead of results @@ -58,7 +58,7 @@ def _score_bar( # pyright: ignore[reportUnusedFunction] ) -> str: """Pure utility kept for the legacy test surface. - The TUI no longer draws score bars — the user's feedback on the + The TUI no longer draws score bars: the user's feedback on the eighth-block and full-block variants was that they read as visual noise. The current label formatters use :func:`_score_style` instead, colouring the numeric score in line with the theme. @@ -83,13 +83,13 @@ def _score_style(score: float, max_score: float) -> str: return "dim" ratio = max(0.0, min(1.0, score / max_score)) if ratio >= 0.85: - return "bold #9ece6a" # tokyo-night green — leader + return "bold #9ece6a" # tokyo-night green: leader if ratio >= 0.6: return "#7dcfff" # cyan if ratio >= 0.35: return "#7aa2f7" # accent blue (theme default) if ratio >= 0.15: - return "#bb9af7" # cool magenta — fades from accent + return "#bb9af7" # cool magenta: fades from accent return "dim #565f89" @@ -209,13 +209,13 @@ def _build_label(text: str, score: float, max_score: float) -> Any: with the file/section text (right, may truncate cleanly). Score-first layout means the colour-coded ranking signal is always - visible regardless of filename length — long titles truncate + visible regardless of filename length: long titles truncate against the right edge of the pane without ever eating the score. """ from rich.text import Text # Last-line-of-defence sanitise so a raw ``\t`` (or other control char) from - # any source — snippet, heading crumb, filename — can never over-run the row + # any source (snippet, heading crumb, filename) can never over-run the row # and corrupt the pane border. Snippets are already cleaned at their source # (fnd.query._make_snippet); this covers the locator/filename paths too. text = display_line(text) @@ -239,7 +239,7 @@ def _trim_redundant_heading(heading_path: str, title: str, path: str) -> str: A leading ``Templates`` segment is dropped when ``Templates`` also appears as a word in the file basename (``Design Notes - Templates - and Streams``) or in the title — covers both the + and Streams``) or in the title: covers both the pure ``# Templates`` H1 case and the deep multi-word filename case. """ if not heading_path: @@ -251,7 +251,7 @@ def _words(s: str) -> set[str]: parts = [p.strip() for p in heading_path.split(">") if p.strip()] haystack = _words(Path(path).stem) | _words(title or "") # If every segment is just a word from the filename / title, the - # whole crumb is redundant — keep the deepest one as the location + # whole crumb is redundant: keep the deepest one as the location # marker, or drop it entirely when there's only one segment so the # caller can fall back to a chunk locator. if parts and all(_words(p).issubset(haystack) for p in parts): @@ -273,8 +273,8 @@ def _elide_middle_keep_suffix(name: str, max_width: int) -> str: """Middle-truncate ``name`` to ``max_width`` chars, keeping the extension visible: ``really_long_report_final_v3.pdf`` -> ``really_…nal_v3.pdf``. - A terminal's default right-clip drops the extension — the one part that - says what kind of file it is — so we elide the stem's middle and keep both + A terminal's default right-clip drops the extension (the one part that + says what kind of file it is) so we elide the stem's middle and keep both ends plus the suffix. When even one stem char won't fit we still show ``…``; only when the suffix itself can't fit (``max_width`` shorter than ``…`` + suffix) do we fall back to a plain right-truncation that drops @@ -304,8 +304,8 @@ def _format_hit_label( """Result-tree row label: short locator left, snippet right. Locator is a few chars (page / slide / trimmed heading / chunk N) - so the body snippet — the actually useful context for "is this - the match I want" — claims most of the row width. + so the body snippet (the actually useful context for "is this + the match I want") claims most of the row width. ``match_visible=False`` prepends :data:`_UNLOCATABLE_GLYPH`: the row stays, but the user is told the preview won't be able to show them the match. @@ -334,7 +334,7 @@ def _format_hit_label( pass_marker = f" {glyph}" if h.pass_index > 0 else "" # Leading, not trailing: locator + 80-char snippet routinely overruns the # results pane, so the row is hard-truncated at the border and anything - # appended to the end is never drawn. Verified in a real terminal — the + # appended to the end is never drawn. Verified in a real terminal: the # marker was being emitted correctly and clipped off screen every time. prefix = "" if match_visible else f"{_UNLOCATABLE_GLYPH} " return _build_label(f"{prefix}{body}{pass_marker}", h.score, max_score) diff --git a/fnd/tui/results_view.py b/fnd/tui/results_view.py index 75417873..166ed10c 100644 --- a/fnd/tui/results_view.py +++ b/fnd/tui/results_view.py @@ -78,7 +78,7 @@ def __init__(self, app: FNDApp) -> None: self._stale: set[str] = set() def title(self) -> str: - """Border title for the results pane — counts live next to the data + """Border title for the results pane: counts live next to the data they describe, not in a global status bar. A collapsed pane says so: two `←` presses shrink it to one row and the @@ -150,11 +150,11 @@ def refresh(self) -> None: The top result is auto-expanded so its section rows (with their ``§ heading`` / ``p.N`` / ``chunk N`` locators) are immediately - visible — saves a keypress and makes the locator format + visible: saves a keypress and makes the locator format discoverable on first launch. """ # Cancel any debounced preview load from the previous result - # set — its parent_id may no longer be a hit, and the new + # set: its parent_id may no longer be a hit, and the new # cursor placement below will arm a fresh timer. self._app._preview.cancel_pending_load() tree = self._app.query_one("#results_pane", Tree) @@ -165,7 +165,7 @@ def refresh(self) -> None: # # Clearing the map instead was the first attempt and it did NOT # work. An unknown row falls through to the stock arrow, and - # Textual's ICON_NODE is byte-identical to the ready glyph — so + # Textual's ICON_NODE is byte-identical to the ready glyph, so # "no claim" rendered as "instant jump" for every row until the # next poll, at exactly the moment nothing was warm. Anything the # tree cannot answer has to fail towards COLD, never towards READY. @@ -183,7 +183,7 @@ def refresh(self) -> None: # the disk. The preview renders the INDEX's copy, so a row whose file # has gone or moved on needs to say so before the user trusts it. self._stale = {g.path for g in self._app._search.groups if is_stale(g)} - # Rows are never filtered on paintability — see fnd.tui.match_evidence. + # Rows are never filtered on paintability: see fnd.tui.match_evidence. # A row the preview can't highlight is marked, not withheld. strict = self._app._effective_evidence_spec painting = self._app._effective_match_spec @@ -237,7 +237,7 @@ def refresh(self) -> None: top_file = tree.root.children[0] if top_file.children: tree.cursor_line = 1 - # Dispatch explicitly — NodeHighlighted is suppressed when + # Dispatch explicitly: NodeHighlighted is suppressed when # cursor_line lands on the same index as before. top_group = self._app._search.groups[0] top_hit = top_group.hits[0] if top_group.hits else None @@ -389,8 +389,8 @@ def _apply_materialised( def refresh_warmth(self) -> bool: """Repaint any file row whose readiness changed. - Polled rather than pushed. Warmth changes from two directions — a - capture landing, and coverage moving to the next file — and the second + Polled rather than pushed. Warmth changes from two directions (a + capture landing, and coverage moving to the next file) and the second has no single write to hook, so a notification would have to be emitted from several places and kept in step with them. @@ -404,7 +404,7 @@ def refresh_warmth(self) -> bool: return False states = self._app._preview.warm_states() if states is None: - # Cannot be answered right now — keep what the rows already show. + # Cannot be answered right now: keep what the rows already show. # An EMPTY map is different: it means there are no files, and the # rows should be cleared rather than left claiming anything. return False diff --git a/fnd/tui/scope_panel.py b/fnd/tui/scope_panel.py index 5d1e3421..b4c2bb86 100644 --- a/fnd/tui/scope_panel.py +++ b/fnd/tui/scope_panel.py @@ -250,7 +250,7 @@ def __init__( # _commit_filter_change) so a burst of multi-select toggles coalesces. self._filter_search_timer: Timer | None = None self._batch_next = False - # Sidebar panel state — always loaded from disk so user-tuned + # Sidebar panel state: always loaded from disk so user-tuned # collapse / expand state survives the next launch, even when # ``--collection`` is passed. The CLI flag overrides search # *scope* (which collections / sources are active), NOT the @@ -283,10 +283,10 @@ def __init__( or b.startswith("tags:") or b.startswith("kinds:") # per-category File-type sub-branches } - # Scope — one provenance-carrying map (``selection``) is the single + # Scope: one provenance-carrying map (``selection``) is the single # source of truth; ``collections`` / ``active_sources`` are derived # views. A launch-time override (``--collection`` and/or the filter - # flags — a search copied out of the app) takes scope + filters from + # flags: a search copied out of the app) takes scope + filters from # the flags; otherwise reconstruct them from the persisted flat scope. # Panel *layout* always loads from disk, so a flagged launch never # discards the user's sidebar state. @@ -329,7 +329,7 @@ def _seed_from_defaults(self) -> dict[str, _FullScope | set[str]]: Reads ``defaults.collection``: ``all`` (the shipped default) ticks every configured collection, a name ticks just that one. Only ever - consulted on a first launch — once a scope is saved, the user's + consulted on a first launch: once a scope is saved, the user's sidebar selection is the source of truth and this never fires again. """ cfg = self._app._config @@ -351,14 +351,14 @@ def _valid_collection_names(self, raw: str) -> list[str]: ``all`` (any case) is the pseudo-name for every configured collection. Otherwise the shared vocabulary canonicalises the value (so ``work2`` reaches the index as ``Work2``) and unknown names are - dropped — the CLI has already offered the user a correction by the + dropped: the CLI has already offered the user a correction by the time a value gets here. With no config loaded, the raw value is trusted. Names absent from the config stay dropped even when the config defines none at all. Keeping one would put a key in ``selection`` that no panel row can toggle, which pins every search with no way to - clear it — the same phantom-scope failure ``_derive_selection`` + clear it: the same phantom-scope failure ``_derive_selection`` guards against. An ad-hoc ``fnd index --collection`` name is therefore searchable via ``fnd search -c`` but not scopeable in the TUI, whose scope model is config-driven by design. @@ -380,7 +380,7 @@ def _derive_selection( Full collections become ``FULL`` (config-relative). Each flat source id is attributed as a partial claim to every non-full - collection whose config contains it — the on-disk shape carries + collection whose config contains it: the on-disk shape carries no provenance, so a shared id is claimed by all owners. The live toggle path records exact provenance; only a save/reload of a shared-partial scope reconstructs approximately. @@ -405,7 +405,7 @@ def _derive_selection( return sel def _tag_source_ids(self) -> list[str]: - """Provider ids for the configured tag sources — the keys tag + """Provider ids for the configured tag sources: the keys tag selections are stored under, shared with the CLI.""" import sys @@ -436,14 +436,14 @@ def _seed_filters(self, launch: LaunchScope) -> None: @property def collections(self) -> list[str]: - """Collections fully in scope (●) — the search collection-filter + """Collections fully in scope (●): the search collection-filter channel. Derived from the selection map.""" return [name for name, sel in self.selection.items() if sel is FULL] @property def active_sources(self) -> list[str]: """Flat active source ids for the per-source search filter. - Only explicit (partial) selections contribute — FULL collections + Only explicit (partial) selections contribute: FULL collections scope via the collection channel. Deterministic config order.""" out: list[str] = [] seen: set[str] = set() @@ -511,7 +511,7 @@ def sync_legend(self, marks: CollectionMarks) -> None: def snapshot(self, query: str) -> SearchSnapshot: """Project the live scope into the read-only value object the command - serializer consumes — the one seam between scope state and + serializer consumes: the one seam between scope state and serialization, so neither reaches into the other.""" partial = tuple( name for name, sel in self.selection.items() if isinstance(sel, set) and sel @@ -634,7 +634,7 @@ def refresh_collections_panel(self) -> None: ) set_border_title(tree, self._panel_title(names)) self._app._results.relabel_rows(marks_only=True) - # The collections list changed length — reflow the sidebar heights. + # The collections list changed length: reflow the sidebar heights. self._app._reflow_sidebar() def _collection_row( @@ -699,7 +699,7 @@ def _present_kinds_for_scope(self) -> set[str] | None: Scoped to the COLLECTIONS, not the current search text (unlike the Tags filter): a file-type list shouldn't churn as the user types, and the set only changes when the collection selection changes. So the result is - CACHED per collection set — the aggregation runs once per scope change + CACHED per collection set: the aggregation runs once per scope change instead of on every search (which ran on every filter toggle), keeping the re-search cheap.""" from fnd.kind_catalogue import present_kinds @@ -708,8 +708,8 @@ def _present_kinds_for_scope(self) -> set[str] | None: index = getattr(searcher, "_index", None) if index is None: return None - # Key by the FULL active scope — full collections AND the active sources - # of partially-selected collections — so a source toggle recomputes and + # Key by the FULL active scope: full collections AND the active sources + # of partially-selected collections, so a source toggle recomputes and # the filter never reveals kinds from unselected sources of the same # collection. scope = self.source_scope @@ -1002,8 +1002,8 @@ def _action_colour(self) -> str: """Control rows take the *inactive pane border* colour so they read as interactive without competing with the focused-pane accent. - The border is ``round $primary 50%`` — primary at 50% opacity over the - app background — so a full-strength ``$primary`` label looks noticeably + The border is ``round $primary 50%``, primary at 50% opacity over the + app background, so a full-strength ``$primary`` label looks noticeably brighter. Reproduce the same blend here. Resolved live so it tracks the theme; empty string (plain text) if the app isn't mounted yet.""" try: @@ -1016,7 +1016,7 @@ def _action_colour(self) -> str: # Use the border's exact foreground: $primary 50% composited over # the pane surface (confirmed #6F6199 on the default theme). Text # glyphs and the border's box-drawing glyphs are both thin strokes, - # so the SAME foreground perceives identically — which is what makes + # so the SAME foreground perceives identically, which is what makes # the rows read as the inactive-border colour rather than # a fresh, brighter purple. base = variables.get("surface") or variables.get("background") or "#000000" @@ -1027,8 +1027,8 @@ def _action_colour(self) -> str: @staticmethod def _distinct_tag_values(by_source: dict[str, set[str]]) -> set[str]: """Tag values across all sources, deduped. The search groups tags by - value — a value selected in several sources is a single OR-ed term - (see fnd.tag_query._terms) — so every filter count follows that view + value: a value selected in several sources is a single OR-ed term + (see fnd.tag_query._terms), so every filter count follows that view rather than double-counting a value that fans across sources (e.g. a copied ``--tag``, which carries no source and seeds into all of them).""" return set[str]().union(*by_source.values()) @@ -1081,7 +1081,7 @@ def _update_clear_bar(self) -> None: def clear_filters(self) -> None: """Reset every filter to its default and re-run the active query. - Collections/sources are scope, not filters, so they are left alone — + Collections/sources are scope, not filters, so they are left alone: clearing them would silently change what corpus is searched. The tag match mode returns to its ``all`` default so the pane is fully reset. A no-op when nothing is active, so a stray keypress can't thrash search. @@ -1106,7 +1106,7 @@ def tag_catalogue_for_scope(self) -> dict[str, list[Any]]: """Tags present in the active collections, per source. Returns empty lists when the index isn't open yet or the aggregation - fails — the pane must still render. + fails: the pane must still render. """ from fnd.tag_catalogue import tag_catalogue @@ -1135,7 +1135,7 @@ def tag_catalogue_for_scope(self) -> dict[str, list[Any]]: def _facet_query(self, index: Any) -> Any: """Tantivy query narrowing the tag catalogue to the active search. - Deliberately built from the lexical text alone — NOT from the tag + Deliberately built from the lexical text alone, NOT from the tag selection. Facets computed over their own filter make every sibling tag vanish the moment one is selected, stranding the user with no way to switch without clearing first. @@ -1420,7 +1420,7 @@ def _restore_cursor(self, tree: Tree[dict[str, object]], keep: tuple[str, ...]) tree.cursor_line = line # Setting cursor_line alone doesn't re-scroll, so when the clear # bar appears above it (shrinking the tree by a row) the restored - # cursor can sit one row out of view. Scroll it back — DEFERRED + # cursor can sit one row out of view. Scroll it back: DEFERRED # to after the refresh, because the bar's show/hide resizes the # tree on the next layout pass, after this runs; scrolling now # would target the pre-resize height and still clip the row. @@ -1451,12 +1451,12 @@ def _on_filters_selected(self, ev: Tree.NodeSelected[dict[str, object]]) -> None Fires exactly once per click: the results tree no longer re-invokes the base ``Tree._on_click`` (which the MRO already dispatches on its own), - so a click posts a single ``NodeSelected`` — see + so a click posts a single ``NodeSelected``, see ``ResultsTree._on_click``. """ data = ev.node.data or {} kind = data.get("kind") - # A File-type category row toggles all its member kinds at once — + # A File-type category row toggles all its member kinds at once: # repaint the category + its leaves + the summary IN PLACE (never a # tree rebuild, so the cursor stays exactly where it was). if kind == "kind_category": @@ -1470,7 +1470,7 @@ def _on_filters_selected(self, ev: Tree.NodeSelected[dict[str, object]]) -> None self._commit_filter_change() return # A non-togglable section header (File type / Modified / Created / Tags) - # has nothing to toggle — a click or Enter expands/collapses it instead. + # has nothing to toggle: a click or Enter expands/collapses it instead. if kind == "filter_category": if ev.node.allow_expand: ev.node.toggle() @@ -1529,8 +1529,8 @@ def on_collections_selected(self, ev: Tree.NodeSelected[dict[str, object]]) -> N def _on_collections_selected(self, ev: Tree.NodeSelected[dict[str, object]]) -> None: """Enter on a collection node toggles the whole collection's scope (all sources at once); Enter on a single source row toggles that - source independently. Every change mutates the ``selection`` map — - the single source of truth — so the visible ●/◐/○ marker drives + source independently. Every change mutates the ``selection`` map ( + the single source of truth) so the visible ●/◐/○ marker drives the toggle direction and a shared source's per-collection provenance is preserved (toggling one owner off can't strip a source a sibling still claims).""" @@ -1617,7 +1617,7 @@ def _repaint_source_node(self, node: Any) -> None: parent_name = str(data.get("collection") or "") src_marker = "●" if self._source_active(parent_name, source_id) else "○" current_label = str(node.label) - # The source label is " . " — preserve the + # The source label is " . ": preserve the # ordinal and basename, just swap the marker glyph. if len(current_label) > 1 and current_label[0] in ("●", "○"): node.set_label(src_marker + current_label[1:]) @@ -1634,7 +1634,7 @@ def collapsed_marker(self, panel_id: str) -> str: def _panel_title(self, names: list[str]) -> str: """Border-title string from the selection map. Source counts use - ``_source_active`` — the same rule the row markers use — so the + ``_source_active`` (the same rule the row markers use) so the title and the row glyphs always agree (the toggle path and the full rebuild both call this).""" cfg = self._app._config diff --git a/fnd/tui/search_controller.py b/fnd/tui/search_controller.py index 33fa2ec0..29eb32ea 100644 --- a/fnd/tui/search_controller.py +++ b/fnd/tui/search_controller.py @@ -9,13 +9,13 @@ ``run()`` is split three ways so the blocking part can leave the event loop: -* **prepare** — on the loop. Parsing, validation and spec building are +* **prepare**: on the loop. Parsing, validation and spec building are cheap, and a malformed query has to raise its notice immediately. Produces a frozen :class:`_SearchRequest`. -* **execute** — on a worker thread. Only the actual search. It reads the +* **execute**: on a worker thread. Only the actual search. It reads the request and the searcher and writes nothing, which is what makes a superseded search harmless rather than a race. -* **commit** — back on the loop. Cache invalidation, DOM teardown and the +* **commit**: back on the loop. Cache invalidation, DOM teardown and the results rebuild, in the order they have always run, and skipped entirely if a newer query has been issued since. @@ -88,7 +88,7 @@ def __init__(self, app: FNDApp) -> None: self._app = app self.searcher: Searcher | None = None self.current_query: str = "" - # Cached match-spec for the active query — drives the markdown- + # Cached match-spec for the active query: drives the markdown- # widget highlight subclasses, the per-line plain renderer's # highlight pass, and the match-aware scrollbar marker map. The # spec captures the SAME literal / fuzzy / synonym semantics @@ -96,7 +96,7 @@ def __init__(self, app: FNDApp) -> None: # gets the user-visible highlight (not just exact-stem hits). # Recomputed on every ``_run_query``. self.match_spec: MatchSpec = MatchSpec() - # The painting spec minus AUTO-fuzzy — see _run_query. Answers "is the + # The painting spec minus AUTO-fuzzy: see _run_query. Answers "is the # term the user typed visible here?", which auto-fuzzy near-misses would # otherwise answer yes to. self.evidence_spec: MatchSpec = MatchSpec() @@ -115,7 +115,7 @@ def __init__(self, app: FNDApp) -> None: # Handle for the deferred prefetch timer so each new search cancels the # previous one instead of stacking. A burst of searches (e.g. toggling # several file-type filters) would otherwise queue a prefetch-of-10 per - # search — dozens of background preview mounts congesting the event loop + # search: dozens of background preview mounts congesting the event loop # and making every subsequent nav feel laggy. Only the latest search's # top results are worth prefetching, so keep just the last timer alive. self._prefetch_timer: Timer | None = None @@ -132,7 +132,7 @@ def __init__(self, app: FNDApp) -> None: self.ranking_profile: RankingProfile = RankingProfile() # Monotonic query counter. ``_prepare`` bumps it; ``_commit`` refuses # any result that does not carry the current value. This is the whole - # of the staleness defence — a superseded thread worker still runs to + # of the staleness defence: a superseded thread worker still runs to # completion and still arrives, it just finds its generation gone. self._generation: int = 0 self._committed_generation: int = 0 @@ -166,7 +166,7 @@ def _searches_running(self) -> bool: Asked of Textual rather than counted here, because a count can drift and this one cannot be allowed to. Counting was tried: `run` bumped a - counter and both commit paths decremented it — but `exclusive=True` + counter and both commit paths decremented it, but `exclusive=True` makes Textual cancel the previous worker SYNCHRONOUSLY inside `add_worker`, before the new one starts, so a worker cancelled before its coroutine ever ran reached neither commit path and the count @@ -176,7 +176,7 @@ def _searches_running(self) -> bool: an external reindex silently stops being picked up. The worker manager's own state cannot drift out of step with the - workers it owns — a cancelled worker leaves PENDING/RUNNING by itself. + workers it owns: a cancelled worker leaves PENDING/RUNNING by itself. """ from textual.worker import WorkerState @@ -192,7 +192,7 @@ def _searches_running(self) -> bool: def idle(self) -> bool: """True when every issued query has been committed or discarded. - The signal tests should wait on — never a fixed number of pauses. + The signal tests should wait on: never a fixed number of pauses. """ return self._committed_generation >= self._generation @@ -215,7 +215,7 @@ def _prepare(self, query: str) -> _SearchRequest | None: # Claim the generation FIRST, before anything that can fail. _fail() # marks the current generation committed, so allocating it after the # parse meant a malformed query marked an IN-FLIGHT search's - # generation as committed — that worker then passed the guard in + # generation as committed: that worker then passed the guard in # _commit, restored its stale results and wiped the error notice the # user had just been shown. self._generation += 1 @@ -223,7 +223,7 @@ def _prepare(self, query: str) -> _SearchRequest | None: # Re-point the searcher at the latest committed generation so a # reindex (in-app or external `fnd reindex`) that landed while the - # app is open shows up on this query — no restart. Near-free + # app is open shows up on this query: no restart. Near-free # (~0.1 ms) when nothing changed; ignore a vanished index dir. # # Only while nothing is executing. reload() reassigns the Searcher's @@ -267,7 +267,7 @@ def _prepare(self, query: str) -> _SearchRequest | None: # they actually searched for here?" (fnd.tui.match_evidence). # # Painting is deliberately generous: auto-fuzzy lights up near-misses so - # a typo still shows something. But near-misses are not evidence — a + # a typo still shows something. But near-misses are not evidence: a # query for "test" paints "best" and "rest" at distance 1, so judging # visibility on the painting spec would report every such chunk as fine # while the user stares at a paragraph with none of their term in it. @@ -305,7 +305,7 @@ def _prepare(self, query: str) -> _SearchRequest | None: match_all=scope.tag_match_all, ) # Collections are a HARD filter, passed as a list straight to the - # query layer — never a ``c:`` prefix string. The prefix path rides + # query layer: never a ``c:`` prefix string. The prefix path rides # the soft query parser (ranks instead of restricting) and splits # collection names on spaces, so a multi-collection scope leaked # other collections and dropped spaced names like ``DB Archive``. @@ -372,7 +372,7 @@ def _fail(self, err: Exception) -> None: # ── execute (worker thread) ────────────────────────────────── def _execute_and_commit(self, request: _SearchRequest, session: ProgressSession) -> None: - """Worker body. Marshals every outcome back to the loop — including + """Worker body. Marshals every outcome back to the loop, including failures, so a raising search can't leave the line up forever.""" from fnd.filter_dsl import FilterError from fnd.query_errors import QueryError @@ -390,8 +390,8 @@ def _execute_and_commit(self, request: _SearchRequest, session: ProgressSession) def _marshal(self, fn: Any, *args: Any) -> None: """Hop back to the event loop with the outcome. - A search can still be in flight when the app shuts down — the user - quits mid-query, or a test leaves its ``run_test`` block — and the + A search can still be in flight when the app shuts down: the user + quits mid-query, or a test leaves its ``run_test`` block, and the commit then has no DOM left to write to. That is not an error, and it must not surface as a failed worker. A failure while the app IS still running is a real bug, so it still propagates. @@ -407,7 +407,7 @@ def _execute(self, request: _SearchRequest) -> tuple[list[FileGroup], SearchTrac writes nothing.""" # Bound ONCE. Reading self.searcher again below is a time-of-check to # time-of-use gap: anything on the loop that reassigns it between the - # reads would pass a different snapshot — or None — into the search. + # reads would pass a different snapshot, or None, into the search. handle = self.searcher if handle is None or not (request.lexical.strip() or request.metadata_filter): return [], None @@ -462,13 +462,13 @@ def _commit( """Land the results. Everything below has always run AFTER the search returned; keeping it whole here preserves that order.""" # Superseded. Textual cancels the worker but cannot interrupt it, so a - # stale search always runs to completion and arrives here — this guard + # stale search always runs to completion and arrives here: this guard # is what keeps it from touching anything. if request.generation != self._generation: session.close() return self._committed_generation = request.generation - # Everything below runs on the loop and is real, measurable work — + # Everything below runs on the loop and is real, measurable work: # cache teardown, the results tree rebuild, the filters aggregation. # The plan names it, so enter it: leaving the session in "query" for # the whole operation capped the line at that phase's share and meant @@ -493,7 +493,7 @@ def _commit( self.current_query = request.query # the original (with [...]) for history # A new query must always re-render the first result, even when it - # lands on the same (parent, seq) as the last one — release the + # lands on the same (parent, seq) as the last one: release the # in-flight coalescing latch so this query's dispatch isn't # mistaken for a redundant same-tick duplicate of the previous. self._app._preview.inflight_target = None @@ -563,7 +563,7 @@ def _commit( def _show_query_notice(self, err: Exception) -> None: """Render a calm, practical line below the query bar for a malformed - query — message plus an actionable hint where we have one.""" + query: message plus an actionable hint where we have one.""" from fnd.filter_dsl import FilterError from fnd.query_errors import QuerySyntaxError @@ -594,7 +594,7 @@ def _clear_query_notice(self) -> None: notice.display = False def query_signature(self) -> str: - """Stable signature for the current query — match-bearing + """Stable signature for the current query: match-bearing widgets are baked with this query's highlights, so the cache must invalidate when it changes. Includes intent because intent biases snippet selection, and the highlight @@ -621,7 +621,7 @@ def toggle_fuzzy(self) -> None: """Flip ``defaults.fuzzy_enabled`` in the config TOML and re-run the current search so the new state is visible immediately. Per-term ``~N`` modifiers still trigger fuzzy expansion when - the toggle is off — only the auto-fuzzy pass is gated.""" + the toggle is off: only the auto-fuzzy pass is gated.""" from fnd.config import default_config_path, write_setting current = self._app._config.defaults.fuzzy_enabled if self._app._config else True @@ -668,7 +668,7 @@ def clear_results(self) -> None: """Drop the current result set and preview without re-running. Used when the user changes scope (toggles a collection) and the - existing results are about to go stale — but we don't want to + existing results are about to go stale, but we don't want to steal focus or thrash through a fresh search until the user explicitly asks for one. Mirrors the cache invalidation in ``_run_query`` minus the actual search call and the diff --git a/fnd/tui/settings_screen.py b/fnd/tui/settings_screen.py index 5031c6f5..6dda70eb 100644 --- a/fnd/tui/settings_screen.py +++ b/fnd/tui/settings_screen.py @@ -9,12 +9,12 @@ ``Tree.border_title``). * A plain one-line search ``Input`` at the top of the container, no decorative border. - * The row list itself — a ``Vertical`` of one ``MenuRow`` widget per + * The row list itself: a ``Vertical`` of one ``MenuRow`` widget per item so we can columnar-render key/label/value with Rich, skip headers from cursor nav, and pin trailing setting values to a fixed column. * A bottom edit-bar (mounted but hidden) that opens above the shared - hint bar when editing a scalar — same chrome wherever it appears. + hint bar when editing a scalar: same chrome wherever it appears. * A shared ``#footer_hints`` Static docked at the screen bottom, rendered by :func:`fnd.tui.app.render_hint_bar` so the visual is identical to the main app's footer. @@ -220,8 +220,8 @@ def build_confirm_body( """Three-row labelled body shared by every confirm screen. Default labels (Outcome / Cost / Safety) describe a pay-then-gain - action — installing, deleting, paying CPU to rebuild. Some actions - don't fit that framing — uninstalling FREES disk rather than + action: installing, deleting, paying CPU to rebuild. Some actions + don't fit that framing: uninstalling FREES disk rather than costing it. Callers override the labels (e.g. ``cost_label="Disk freed"``) so each screen reads naturally. @@ -315,7 +315,7 @@ def confirm_border_class(severity: str) -> str: # Per-kind glyph constants. All render in default macOS Terminal fonts -# (Menlo, SF Mono, Monaco) — verified safe. +# (Menlo, SF Mono, Monaco): verified safe. _GLYPH_TOGGLE_ON = "✓ on" # U+2713 + text _GLYPH_TOGGLE_OFF = "✗ off" # U+2717 + text _GLYPH_DRILL = "▸" # U+25B8 small triangle @@ -421,12 +421,12 @@ def _render_row( KIND_EXTERNAL drill: summary + ▸ ; external_app: path (dim), label gets leading ↗ KIND_PICKER value (bold) + ▾ (accent) KIND_SCALAR value (bold) - KIND_DISPLAY value (bold) — and label rendered dim instead of bright + KIND_DISPLAY value (bold), and label rendered dim instead of bright - ``app`` may be ``None`` for tests that don't construct a full app — + ``app`` may be ``None`` for tests that don't construct a full app: in that case the trailing slot is skipped. - ``breadcrumb`` is a tuple of section labels — when provided it is + ``breadcrumb`` is a tuple of section labels, when provided it is rendered instead of the normal trailing so the user knows which section each cross-section search result comes from. @@ -519,7 +519,7 @@ def _render_row( # The trailing affordance (rightmost segment) is the row's # primary signal of "what does Enter do." If the row's total # render would exceed width, the terminal silently truncates - # the right edge — losing the glyph the user needs. Truncate + # the right edge: losing the glyph the user needs. Truncate # the longest dim/summary segment instead, leaving room for at # least a 2-char dotted pad and the whole affordance. min_pad = 2 @@ -547,7 +547,7 @@ def _truncate_segments_to_fit( ) -> list[tuple[str, str]]: """Shrink the first dim/summary segment with ``…`` so the total fits within ``budget``. The trailing affordance segment (and any - other non-dim segments) is preserved verbatim — losing the glyph + other non-dim segments) is preserved verbatim: losing the glyph would defeat the per-kind visual language. Returns the original list when no truncation is needed.""" @@ -569,7 +569,7 @@ def _truncate_segments_to_fit( kept[longest] = (_shorten(text, room, elide), style) return kept if available_for_dim <= 1: - # Pathologically narrow row — drop dim segments altogether, + # Pathologically narrow row: drop dim segments altogether, # keep only the affordance. return [(t, s) for t, s in segments if "dim" not in s] out: list[tuple[str, str]] = [] @@ -593,11 +593,11 @@ def _trailing_segments(item: MenuItem, app: FNDApp | None) -> list[tuple[str, st """Per-kind trailing segments as (text, rich_style) pairs. Rich Text styles used: - ``bold green`` — toggle on, safe affirmation - ``bold red`` — toggle off, destructive - ``bold cyan`` — accent: action brackets, drill arrow, picker caret, ↗ - ``bold`` — bright value (scalar / picker value / display value) - ``dim`` — drill row summary text, parenthetical context + ``bold green`` (toggle on, safe affirmation + ``bold red``) toggle off, destructive + ``bold cyan`` (accent: action brackets, drill arrow, picker caret, ↗ + ``bold``) bright value (scalar / picker value / display value) + ``dim``, drill row summary text, parenthetical context """ if app is None: return [] @@ -611,7 +611,7 @@ def _trailing_segments(item: MenuItem, app: FNDApp | None) -> list[tuple[str, st if item.kind == KIND_ACTION: # Keybindings cheat-sheet rows carry a ``key`` glyph in their - # leading column — that IS the affordance. A trailing button + # leading column: that IS the affordance. A trailing button # would (a) repeat noise across ~30 rows of documentation and # (b) push the leading [key] into the right margin under # narrow widths. @@ -649,7 +649,7 @@ def _trailing_segments(item: MenuItem, app: FNDApp | None) -> list[tuple[str, st if item.external_app: # External app: dim path; no trailing arrow (leading ↗ on label). return [(summary, "dim")] if summary else [] - # Internal drill — same as KIND_SUBMENU. + # Internal drill: same as KIND_SUBMENU. if summary: return [(summary + " ", "dim"), (_GLYPH_DRILL, "bold cyan")] return [(_GLYPH_DRILL, "bold cyan")] @@ -991,7 +991,7 @@ class SettingsList(Widget, can_focus=True): SettingsList Static.row.-hint-section { background: $accent 8%; } /* Only paint the cursor row when the list itself owns focus. When the screen's filter Input is focused, the user is composing a - query — a list-cursor highlight at the same time would compete + query: a list-cursor highlight at the same time would compete for the eye. */ SettingsList:focus Static.row.-cursor { background: $accent 40%; text-style: bold; } """ @@ -1032,11 +1032,11 @@ def __init__(self) -> None: # Maps id(item) → breadcrumb tuple during cross-section search. # Empty when no filter is active. self._search_breadcrumbs: dict[int, tuple[str, ...]] = {} - # The active search query, lowercased — used to bold the matching + # The active search query, lowercased: used to bold the matching # substring inside each filtered row's label. self._search_query: str = "" # When set, the next ``_init_cursor`` lands on this item id instead - # of the first selectable row — lets a re-population (e.g. resume + # of the first selectable row: lets a re-population (e.g. resume # from a popped child screen) keep the cursor on the row the user # drilled from. Consumed (reset to None) once applied. self._pending_cursor_id: str | None = None @@ -1051,8 +1051,8 @@ def __init__(self) -> None: def compose(self) -> ComposeResult: # VerticalScroll (not plain Vertical) so long lists like the - # Keybindings cheat sheet — 30+ rows after the registry-derived - # rebuild — get a working scrollbar and ``_scroll_cursor_into_view`` + # Keybindings cheat sheet, 30+ rows after the registry-derived + # rebuild, get a working scrollbar and ``_scroll_cursor_into_view`` # has a scrollable parent to call ``scroll_to_widget`` on. yield VerticalScroll(id="settings_list_body") @@ -1069,7 +1069,7 @@ def set_items( # _init_cursor (deferred below) lands here if the id is present. self._pending_cursor_id = cursor_id body = self.query_one("#settings_list_body", VerticalScroll) - # Remove existing rows synchronously by walking the DOM directly — + # Remove existing rows synchronously by walking the DOM directly: # Textual's `remove_children` is async and would race against the # mount of fresh rows immediately below. for child in list(body.children): @@ -1194,12 +1194,12 @@ def watch_cursor_index(self, old: int, new: int) -> None: to the newly-cursored row without re-rendering every row. ``_render_all`` rebuilds every row's Rich ``Text`` and updates - every ``Static`` — on a long list (Keybindings has ~80 rows) it + every ``Static``: on a long list (Keybindings has ~80 rows) it dominates the cost of a single arrow keystroke. The cursor move only changes one CSS class on two rows; do exactly that. Also posts the Highlighted message so the parent screen can - update its hint bar / detail strip — any external setter of + update its hint bar / detail strip: any external setter of ``cursor_index`` (screen restoration, jump-to-row, tests) goes through this watcher so the cascade always fires. """ @@ -1273,7 +1273,7 @@ def _scroll_cursor_into_view(self) -> None: with contextlib.suppress(Exception): self.scroll_to_widget(rows[self.cursor_index], animate=False) - # Item kinds that "drill into a sub-screen" — these are what `right` + # Item kinds that "drill into a sub-screen": these are what `right` # activates. Right does nothing on scalars / toggles / actions / leaf # rows; the user must press Enter for those. _DRILL_KINDS: frozenset[str] = frozenset({KIND_SUBMENU, KIND_PICKER, KIND_EXTERNAL}) @@ -1287,7 +1287,7 @@ def action_activate(self) -> None: def action_drill(self) -> None: """Right-arrow navigation: drill into a sub-screen only. No-op for kinds that would otherwise *commit* state (scalar edit, - toggle, action dispatch) — those are Enter-only.""" + toggle, action dispatch): those are Enter-only.""" if 0 <= self.cursor_index < len(self._items): item = self._items[self.cursor_index] if item.kind in self._DRILL_KINDS: @@ -1429,7 +1429,7 @@ def on_mount(self) -> None: lst = self.query_one(SettingsList) lst.set_items(list(self._items)) # Root menu: focus the filter Input so typing immediately - # narrows. Sub-menus focus the list — the user just drilled + # narrows. Sub-menus focus the list: the user just drilled # in deliberately and wants to navigate, not re-filter from # scratch. The `/` shortcut still works to focus the filter # on demand from any screen. @@ -1449,7 +1449,7 @@ def on_screen_resume(self) -> None: def refresh_items(self) -> None: """Re-run the provider and repaint every row's trailing value. - A scalar/picker edit only changes one row's value — re-rendering + A scalar/picker edit only changes one row's value: re-rendering in place is enough since the row's ``value_getter`` lambda reads config lazily. But a structural edit (adding a source, renaming a collection) changes the *set* of rows, and the cached @@ -1459,7 +1459,7 @@ def refresh_items(self) -> None: trailing values needed refreshing. Also invalidates the lazy-trailing cache so async values (cache - size, pdf-structure disk, etc.) re-compute on resume — the user + size, pdf-structure disk, etc.) re-compute on resume: the user may have just run an action that changed the underlying numbers. """ import contextlib @@ -1491,7 +1491,7 @@ def refresh_items(self) -> None: # Preserve cursor position by item id: a structural edit (add / # remove row) shifts indices, so the closest semantic anchor is # the previously-cursored row's stable id. Threaded through - # set_items → _init_cursor so the restore is atomic — a separate + # set_items → _init_cursor so the restore is atomic: a separate # deferred restore loses the race against _init_cursor's own # deferred reset-to-first and the cursor jumps back to the top. prev_id: str | None = None @@ -1591,7 +1591,7 @@ def _row_hint_cluster(self) -> tuple[tuple[str, str], ...]: For the default branch, the `⏎` action label reflects what Enter does on the focused row (Toggle / Edit / Choose / Open / - Run / Open in editor) — or is omitted entirely for read-only + Run / Open in editor), or is omitted entirely for read-only rows. This way the footer never lies about the next action. """ # Edit-bar open: just the save/cancel pair. @@ -1689,7 +1689,7 @@ def _apply_filter(self, raw: str, *, cursor_id: str | None = None) -> None: self._filter_active = True filtered, breadcrumbs = self._filter_items(q) if not filtered: - # Empty-state hint — a non-selectable placeholder row so the + # Empty-state hint: a non-selectable placeholder row so the # cursor-skip rule keeps it inert. placeholder = MenuItem( id="search.empty", @@ -1704,7 +1704,7 @@ def _filter_items(self, q: str) -> tuple[list[MenuItem], dict[int, tuple[str, .. """Cross-section: walk every section's leaves, score by substring match against label + key + keywords + breadcrumb segments. - Spec deliberately excludes ``description`` prose — descriptions + Spec deliberately excludes ``description`` prose: descriptions surface in the detail strip on focus, and indexing them muddies the search results. """ @@ -1738,7 +1738,7 @@ def _filter_items(self, q: str) -> tuple[list[MenuItem], dict[int, tuple[str, .. def _on_search_submitted(self, _ev: Input.Submitted) -> None: # Search is navigation-only: Enter lands focus on the first match # (the cursor is already there from filtering) and stops. It does - # NOT fire the row's effect — no silent toggles, no accidental + # NOT fire the row's effect: no silent toggles, no accidental # side-effects (e.g. launching an editor). The user presses Enter # again on the now-focused list to actually act on the row. lst = self.query_one(SettingsList) @@ -1799,7 +1799,7 @@ def on_descendant_blur(self, _ev: events.DescendantBlur) -> None: refresh_search_placeholder(self, "#settings_search") def _row_metadata(self, item: MenuItem) -> str: - """Build the 2nd-line metadata for the detail strip — storage path, + """Build the 2nd-line metadata for the detail strip: storage path, constraint, applicability note, etc.""" parts: list[str] = [] if item.setting_path: @@ -1813,18 +1813,18 @@ def _row_metadata(self, item: MenuItem) -> str: def _activate_item(self, item: MenuItem) -> None: app: FNDApp = self.app # type: ignore[assignment] if item.kind == KIND_DISPLAY: - # Read-only row — Enter does nothing. Detail strip still + # Read-only row: Enter does nothing. Detail strip still # populates from cursor focus. return if item.kind == KIND_ACTION: # ACTION rows have three dispatch paths in priority order: # 1. ``external`` callable (custom side-effect, e.g. push # a confirm modal or an IndexerScreen). When present, - # run it and leave the settings stack alone — the + # run it and leave the settings stack alone: the # callable usually pushes its own screen. # 2. ``action_id`` (REGISTRY-bound app action). Close the # settings stack and dispatch via ``app.action_``. - # 3. Documentation-only rows (widget-level bindings — Move + # 3. Documentation-only rows (widget-level bindings: Move # cursor, Activate, etc. in the Keybindings sheet) # carry neither; Enter is a no-op so the user can keep # reading the cheat sheet. @@ -2086,7 +2086,7 @@ def _on_selected(self, ev: OptionList.OptionSelected) -> None: else: self._selected.add(target.value) # In-place update keeps the OptionList cursor on the toggled - # row — rebuilding via clear_options + add_option would reset + # row: rebuilding via clear_options + add_option would reset # it to index 0 every time and force the user to re-navigate. lst = self.query_one("#picker_list", OptionList) lst.replace_option_prompt_at_index(target_index, self._render_choice_prompt(target)) @@ -2709,7 +2709,7 @@ def _app_choices(self, _app: Any) -> list[ChoiceOption]: ) ] for app_id, app in registry.items(): - # Filter to apps that are actually installed on this host — + # Filter to apps that are actually installed on this host: # matches the global default-app picker and the Open-with # modal, both of which already filter by ``available()``. # Without this, the picker would let users pick e.g. Skim on @@ -2718,12 +2718,12 @@ def _app_choices(self, _app: Any) -> list[ChoiceOption]: if not app.available(): continue # `reveal` never opens the file, so it can't stand in as a - # source's app — see ``App.selectable_default``. + # source's app: see ``App.selectable_default``. if not app.selectable_default: continue handles = ",".join(app.handles) # ``app.notes`` carries the per-app advisory ("install plugin X - # for line-precise jumps", "no page-jump on macOS", etc.) — surface + # for line-precise jumps", "no page-jump on macOS", etc.): surface # it as the picker's description so users see the recommendation # at the point of choice. Fall back to ``handles:`` when an app # has no notes (most built-ins do). @@ -3265,7 +3265,7 @@ def _on_field_highlighted(self, ev: SettingsList.Highlighted) -> None: @on(TextArea.Changed, "#frontmatter_sample") def _on_sample_changed(self, _ev: TextArea.Changed) -> None: """Live match-status when the user pastes/edits a frontmatter - sample — mirrors SourceFormScreen so the wizard's tester is + sample: mirrors SourceFormScreen so the wizard's tester is actually functional, not just visually present.""" self._refresh_match_status() @@ -3561,7 +3561,7 @@ def __init__(self, *, collection_name: str) -> None: self._name = collection_name # Set once the worker is dispatched: freezes the bindings so the user # can't re-fire "Yes" (a second worker) or escape onto the now-stale - # parent screen mid-delete. Never cleared — the screen is single-use. + # parent screen mid-delete. Never cleared: the screen is single-use. self._deleting = False self._default_moved = False @@ -3635,7 +3635,7 @@ def _on_select(self, ev: OptionList.OptionSelected) -> None: # seconds on a fragmented one, so it runs on a worker with a spinner. # Non-atomic by design: the config is committed here, before the worker # drops the chunks. A _drop failure leaves orphan chunks with the - # collection already gone from config — surfaced to the user, cleared by + # collection already gone from config: surfaced to the user, cleared by # a later "Rebuild all"; no auto-rollback (matches the prior sync path). import contextlib @@ -3718,8 +3718,8 @@ def _finish_delete(self, app: FNDApp, error: str | None) -> None: ) with contextlib.suppress(Exception): app._scope.refresh_collections_panel() # type: ignore[attr-defined] - # Pop the Delete screen + the now-stale per-collection screen beneath it - # — but only while the Delete screen is still on top. A manual escape + # Pop the Delete screen + the now-stale per-collection screen beneath it, + # but only while the Delete screen is still on top. A manual escape # mid-delete means the user already navigated elsewhere, so popping by # count would drop unrelated screens. Never pop the root. if app.screen is self: @@ -3741,7 +3741,7 @@ def action_back(self) -> None: class CacheMaintenanceConfirm(Screen[None]): """Confirm screen for cache prune / clear. - Mirrors :class:`DeleteCollectionScreen` chrome — same bordered + Mirrors :class:`DeleteCollectionScreen` chrome, same bordered settings_box, same OptionList Yes/Cancel pattern, same key bindings. Arrows navigate between options; Enter selects. Destructive variants use ``$error`` border; reversible variants @@ -3843,11 +3843,11 @@ def _on_select(self, ev: OptionList.OptionSelected) -> None: class UpdateAllConfirm(Screen[None]): """Confirm + chain Update index across every collection. - Mirrors the CacheMaintenanceConfirm chrome — bordered box, + Mirrors the CacheMaintenanceConfirm chrome: bordered box, OptionList Yes/Cancel, hint bar. On Yes, kicks off the first collection's update via the existing per-collection modal path; when that completes, advances to the next. Phase F adds a - proper aggregate progress modal — for now we delegate to + proper aggregate progress modal: for now we delegate to sequential per-collection runs.""" ROLE: ClassVar[Role] = Role.CONFIRM @@ -4029,7 +4029,7 @@ def _pdf_cache_size_human() -> str: class StructuredPdfConfirmScreen(Screen[None]): """Disclosure + Yes/Cancel for the pdf-structure extra. - Mirrors :class:`CacheMaintenanceConfirm` chrome — bordered + Mirrors :class:`CacheMaintenanceConfirm` chrome, bordered settings_box, OptionList Yes/Cancel, hint bar. State at mount decides install vs uninstall copy. Confirming pushes the progress modal wired in step 6b. @@ -4072,7 +4072,7 @@ def __init__(self) -> None: self._extra = EXTRAS.get("pdf-structure") self._installed = self._extra is not None and self._is_installed() # Install is "safe" (additive, reversible). Uninstall is - # "recoverable" — packages go but indexed chunks stay, so the + # "recoverable": packages go but indexed chunks stay, so the # user can recover by reinstalling. self._severity = "recoverable" if self._installed else "safe" self.add_class(confirm_border_class(self._severity)) @@ -4789,7 +4789,7 @@ def _on_select(self, ev: OptionList.OptionSelected) -> None: def open_settings(app: FNDApp) -> None: - """Open the Settings root menu — a short list of categories the + """Open the Settings root menu: a short list of categories the user can drill into. No content stacked on a single screen.""" items = build_root_items(app) app.push_screen( @@ -4829,7 +4829,7 @@ def open_settings_section( Used by drill-in rows on the root menu AND by the global shortcuts (`?` → Keybindings, F3 → Collections). When called from `?`, an optional ``context_hint`` is threaded into the section's provider so - the Keybindings list reorders its sections — most-relevant first + the Keybindings list reorders its sections: most-relevant first after Global. Hint is ignored by sections that don't consume it. """ label = section_label(section_id) @@ -4851,10 +4851,10 @@ def open_settings_section( def _format_recorded_at(iso: str) -> str: """Compact local-time label for an ISO-8601 UTC timestamp. - today HH:MM — same calendar day - yesterday HH:MM — previous calendar day - Mon HH:MM — within the last 7 days - MMM DD HH:MM — older than a week""" + today HH:MM, same calendar day + yesterday HH:MM, previous calendar day + Mon HH:MM, within the last 7 days + MMM DD HH:MM, older than a week""" import datetime as _dt try: @@ -4878,7 +4878,7 @@ def _flat_pdfs_with_reasons( # pyright: ignore[reportUnusedFunction] # consume ) -> list[tuple[str, str, str, str | None]]: """Return a list of ``(collection, path, reason, recorded_at)`` for every PDF that is on disk but has no body_md-bearing chunk in the - tantivy index (i.e. not texturised — body_struct is present on every + tantivy index (i.e. not texturised: body_struct is present on every indexed PDF and can't distinguish flat from textured). ``recorded_at`` is the failure-log timestamp, or None when inferred. Reasons are sourced from the failure log when present; otherwise inferred from @@ -5084,15 +5084,15 @@ def _render_footer(self) -> None: def _refresh(self) -> None: """Paint the last scan instantly (or a 'Scanning…' placeholder on a cold cache) and recompute off the event loop. The flat-PDF scan - walks every source on disk and diffs the index — seconds on a real - corpus — so it must never run on the UI thread or the screen + walks every source on disk and diffs the index, seconds on a real + corpus, so it must never run on the UI thread or the screen freezes on open. ``_on_rows_ready`` repaints when the worker lands.""" from fnd.tui import flat_pdf_scan cached = flat_pdf_scan.cached_rows(self._collection_filter) if cached is not None: self._render_rows(cached) - # Only rescan when stale — a fresh cache would otherwise notify + # Only rescan when stale: a fresh cache would otherwise notify # _on_rows_ready inline and rebuild the whole row list a second # time (flicker + wasted work). if not flat_pdf_scan.is_fresh(self._collection_filter): diff --git a/fnd/tui/sidebar_layout.py b/fnd/tui/sidebar_layout.py index 0d646c7d..8c0bc4f1 100644 --- a/fnd/tui/sidebar_layout.py +++ b/fnd/tui/sidebar_layout.py @@ -1,6 +1,6 @@ """Dynamic height allocation for the stacked sidebar panels. -The left column holds three panels — Results, Collections, Filters — sharing a +The left column holds three panels (Results, Collections, Filters) sharing a fixed height. A static CSS split can't be both content-tight (no empty space when a panel is short) and priority-aware (Results dominates when long): CSS can express "size to content" or "take the leftover", never "content when @@ -24,7 +24,7 @@ pinned before the flex split, clamped so Results keeps about half the room and every other expanded panel its floor. -The function is pure — it takes measured demands and returns cell heights — so +The function is pure (it takes measured demands and returns cell heights) so the policy is tested without a running app. """ @@ -170,7 +170,7 @@ def _split_by_demand(secondaries: list[Panel], room: int, demand: dict[str, int] while active and remaining > 0: share = remaining // len(active) if share == 0: - # A handful of rows left over — hand them to the hungriest panels. + # A handful of rows left over: hand them to the hungriest panels. for s in sorted(active, key=lambda s: demand[s.key] - heights[s.key], reverse=True): if remaining <= 0: break @@ -192,7 +192,7 @@ def _proportional(secondaries: list[Panel], room: int, demand: dict[str, int]) - """Fallback when ``room`` can't cover even the per-panel floors: apportion what little there is by demand (largest-remainder), so the total is exactly ``room``. When there are more panels than rows the smallest-demand panels - get nothing — a 3-row sidebar can't show three panels — rather than the + get nothing (a 3-row sidebar can't show three panels) rather than the column being over-allocated (a floor of 1 each would sum past ``room``).""" if room <= 0: return {s.key: 0 for s in secondaries} diff --git a/fnd/tui/stall_watch.py b/fnd/tui/stall_watch.py index 2464f750..4874cfdf 100644 --- a/fnd/tui/stall_watch.py +++ b/fnd/tui/stall_watch.py @@ -1,7 +1,7 @@ """Report when the event loop stops answering, and say what was running. A freeze is an event loop that does not come back, and the hard part is never -noticing one — it is knowing which piece of work held it. Guessing has a poor +noticing one: it is knowing which piece of work held it. Guessing has a poor record here: an executor drain, a capture-store teardown and a chunk decode were each a confident explanation and each disproved by measurement, while the real one (coverage and lazy mount fighting over the same message pump) was found by a @@ -50,7 +50,7 @@ def stack_key(stack: traceback.StackSummary) -> str: Separators are normalised first. Windows reports backslashes, and without this the filter below matches nothing, every sample keys off the same empty - string, and unrelated stalls merge into one meaningless bucket — a + string, and unrelated stalls merge into one meaningless bucket: a diagnostic that says nothing, on the platform where attaching a debugger is hardest. """ @@ -88,7 +88,7 @@ def from_env(cls, app: FNDApp) -> StallWatch | None: threshold = _DEFAULT_THRESHOLD_MS # `=1` means "on", not "stall on everything over a millisecond". # `isfinite` because `inf`/`nan` pass the bound below and then silently - # disable every report — a diagnostic that says nothing looks like a + # disable every report: a diagnostic that says nothing looks like a # clean run. if not math.isfinite(threshold) or threshold <= 1: threshold = _DEFAULT_THRESHOLD_MS @@ -107,7 +107,7 @@ def _sample(self) -> None: A thread, not a task, and that is the whole point: a coroutine cannot observe the loop it is itself blocked on, so the flags in - :meth:`_snapshot` can only ever say which of OUR markers was set — never + :meth:`_snapshot` can only ever say which of OUR markers was set, never which code was actually running. Sampling from outside answers that directly, and it is what finally attributed these stalls (108 of 110 samples inside ``Stylesheet.apply``, over half of them the descendant @@ -168,7 +168,7 @@ async def _run(self) -> None: if late >= self._threshold_ms: # CPU consumed across the gap, reported rather than judged. # A heartbeat alone cannot tell a blocked loop from a process - # the OS stopped running — an unfocused terminal gets its timers + # the OS stopped running: an unfocused terminal gets its timers # coalesced, and a 7.7s gap nobody felt reads exactly like a # 7.7s freeze. CPU close to the gap means Python work held the # loop; CPU near zero means the process was either idle or @@ -208,7 +208,7 @@ def _snapshot(self) -> str: def _prefetch_state(self) -> str: """What prefetch is doing: the running job's file, and the queue depth. - Prefetch mounts widgets on the loop, so it can hold it — and it is the + Prefetch mounts widgets on the loop, so it can hold it, and it is the one background actor no other flag here covers, which is why stalls kept being reported with everything False. """ diff --git a/fnd/tui/strip_document.py b/fnd/tui/strip_document.py index 9aceb972..f59e8087 100644 --- a/fnd/tui/strip_document.py +++ b/fnd/tui/strip_document.py @@ -5,7 +5,7 @@ rather than document size. The width-rebuild is a subclass hook rather than inherited behaviour: strips are -cut at one width, and how they are re-derived depends on where they came from — +cut at one width, and how they are re-derived depends on where they came from; the flat (PDF/TXT) path re-renders ``FileView.lines`` at the new width. A base class that offered a default would let a subclass silently inherit the wrong one. @@ -36,15 +36,15 @@ class StripDocumentView(ScrollView, can_focus=True): Subclasses own their strips and their addressing, and supply: - * :meth:`_visual_row` — address -> visual row. An *address* is whatever + * :meth:`_visual_row`, address -> visual row. An *address* is whatever survives a reflow for that substrate: a wrap-stable logical line for the flat buffer, a document row for frozen captures. - * :meth:`_ready_for_scroll` — whether an address can be resolved yet. - * :meth:`_rebuild_for_width` — re-wrap, re-freeze, or nothing. + * :meth:`_ready_for_scroll`, whether an address can be resolved yet. + * :meth:`_rebuild_for_width`, re-wrap, re-freeze, or nothing. * :attr:`match_rows` (VISUAL rows), :meth:`address_of_chunk` and - :meth:`first_match_address_of_chunk` (substrate ADDRESSES — not the same + :meth:`first_match_address_of_chunk` (substrate ADDRESSES, not the same space; mixing them scrolls to the wrong place once wrapping is on). - * :meth:`_row_overlay_style` — optional per-row accent. + * :meth:`_row_overlay_style`, optional per-row accent. """ ALLOW_SELECT = True @@ -57,7 +57,7 @@ def __init__(self, *, id: str | None = None, show_match_markers: bool = True) -> # In-development scrollbar match highlighting. False suppresses the # marker feed (the bar still scrolls; it just paints no markers). self._show_match_markers: bool = show_match_markers - # Deferred scroll target — re-applied once there is a real viewport. + # Deferred scroll target: re-applied once there is a real viewport. self._pending_scroll_address: int | None = None self._pending_scroll_center: bool = False # Fraction of the viewport to drop a match below the top (context above @@ -80,11 +80,11 @@ def _rebuild_for_width(self, width: int) -> bool: @property def match_rows(self) -> list[int]: - """Sorted visual rows carrying a match — the scrollbar marker feed.""" + """Sorted visual rows carrying a match: the scrollbar marker feed.""" return [] def address_of_chunk(self, chunk_id: int) -> int | None: - """The chunk's first ADDRESS — the substrate's own space, not a visual + """The chunk's first ADDRESS: the substrate's own space, not a visual row. `scroll_to_address` is what converts it.""" return None @@ -98,7 +98,7 @@ def _row_overlay_style(self, visual_y: int) -> Any: return None def top_address(self) -> int | None: - """The address at the viewport top — a restorable reading position.""" + """The address at the viewport top: a restorable reading position.""" if not self._strips: return None return max(0, min(int(self.scroll_offset.y), len(self._strips) - 1)) @@ -147,7 +147,7 @@ def _apply_pending_scroll(self, retries: int = 8) -> None: self.call_after_refresh(self._apply_pending_scroll, retries - 1) return # Ensure the strips match the current viewport width before resolving - # the address — otherwise an install at one width followed by layout at + # the address, otherwise an install at one width followed by layout at # another races the resize and scrolls to a stale row. if self.size.width > 0 and self._rebuild_for_width(self.size.width): self._refresh_match_scrollbar() @@ -237,8 +237,8 @@ def render_line(self, y: int) -> Strip: composed_base = base if overlay is None else (base + overlay) strip = self._strips[row].crop_extend(scroll_x, scroll_x + self.size.width, composed_base) # Composite every cell on ``composed_base`` so plain-text segments - # (bgcolor=None straight out of Rich) paint at the widget's cascaded bg - # — without this, those cells fall through to whatever the terminal's + # (bgcolor=None straight out of Rich) paint at the widget's cascaded bg, + # without this, those cells fall through to whatever the terminal's # default is (Tokyo Night terminal bg = ``#1a1b26``), producing a darker # stripe under the text while the padding shows ``$surface``. # ``composed_base + seg.style`` combines them in the right direction: @@ -264,7 +264,7 @@ def get_selection(self, selection) -> tuple[str, str] | None: # type: ignore[ov """Mirror ``Log.get_selection`` so Textual's clipboard manager can extract multi-line selections. - ``selection`` carries ``(start, end)`` as ``(visual_row, col)`` pairs — + ``selection`` carries ``(start, end)`` as ``(visual_row, col)`` pairs: Textual measures selection coordinates in visual rows (which is what ``render_line`` paints), not in logical lines. Each row's plain text comes from the cached Strip, so every substrate behaves identically. diff --git a/fnd/tui/syntax_theme.py b/fnd/tui/syntax_theme.py index 13641a3f..be037869 100644 --- a/fnd/tui/syntax_theme.py +++ b/fnd/tui/syntax_theme.py @@ -2,17 +2,17 @@ Textual's stock fence highlighter (``textual.highlight.highlight``) lexes with Pygments then maps tokens through a sparse ``HighlightTheme`` that -collapses most token types onto a handful of theme variables — flat-looking +collapses most token types onto a handful of theme variables: flat-looking output. :func:`highlight_fenced` keeps the same single Pygments pass but: * maps through :class:`FNDSyntaxTheme`, a granular fixed-hex palette; -* differentiates bare ``Name`` tokens by position — Pygments has no semantic +* differentiates bare ``Name`` tokens by position, Pygments has no semantic analysis, so in C/C++/Java a namespace, type, and variable all arrive as a bare ``Name`` (a sea of blue). Cheap positional heuristics recover the distinction: ``name(`` is a call, ``name::`` a scope, ``::name`` a qualified type, ``PascalCase`` a type, ``ALL_CAPS`` a constant; * colours brackets ``()[]{}`` by nesting depth (rainbow brackets) so matching - pairs are obvious. Angle brackets are skipped — Pygments tags them as + pairs are obvious. Angle brackets are skipped: Pygments tags them as ambiguous operators. Colours are fixed truecolor, not ANSI: identical and legible for every user @@ -31,7 +31,7 @@ from textual.content import Content, Span from textual.highlight import HighlightTheme, guess_language -# Palette — shared by the theme map and the positional heuristics so a colour +# Palette: shared by the theme map and the positional heuristics so a colour # is defined once. Seeded from the user's terminal/VSCode colours. _COMMENT = "#4E6B6E" _STRING = "#CACACA" @@ -46,7 +46,7 @@ _NEUTRAL = "#A9B1D6" # separator punctuation (`;` `,` `.`) _ERROR = "#FF4971" -# Rainbow-bracket depth cycle (the user's bright terminal hues — vivid enough +# Rainbow-bracket depth cycle (the user's bright terminal hues: vivid enough # to track nesting, distinct from the token colours above). _BRACKET_CYCLE = ("#F2A272", "#B043D1", "#3FDCEE") _OPENERS = frozenset("([{") @@ -63,7 +63,7 @@ class FNDSyntaxTheme(HighlightTheme): Token.Keyword: _KEYWORD, # if/for/return/def/class Token.Keyword.Constant: _CONSTANT, # True/False/None/nullptr Token.Keyword.Namespace: _KEYWORD, # import/from - Token.Keyword.Type: _KEYWORD, # void/double/int — built-in type keywords read as keywords; teal is reserved for named types + Token.Keyword.Type: _KEYWORD, # void/double/int: built-in type keywords read as keywords; teal is reserved for named types Token.Operator: _OPERATOR, Token.Operator.Word: _OPERATOR, # and/or/not/in/is Token.Name: _VARIABLE, # bare identifiers; refined by _name_style @@ -103,13 +103,13 @@ class FNDSyntaxTheme(HighlightTheme): def _is_colon(tok: _Tok | None) -> bool: - """A lone ``:`` token — C/C++ lex ``::`` as two of these. Matched on text + """A lone ``:`` token, C/C++ lex ``::`` as two of these. Matched on text only (not token type), since lexers disagree (Operator vs Punctuation).""" return tok is not None and tok[1] == ":" def _is_scope(tok: _Tok | None) -> bool: - """A single ``::`` scope token — Rust lexes it as ``Punctuation '::'`` and + """A single ``::`` scope token, Rust lexes it as ``Punctuation '::'`` and Ruby as ``Operator '::'`` (C/C++ split it into two ``:`` instead).""" return tok is not None and tok[1] == "::" @@ -120,18 +120,18 @@ def _name_style(before: Sequence[_Tok], after: Sequence[_Tok], text: str) -> str nearest non-whitespace neighbours, nearest-first.""" nxt = after[0] if after else None prev = before[0] if before else None - # `name(` — a call. + # `name(`: a call. if nxt is not None and nxt[0] is Token.Punctuation and nxt[1] == "(": return _FUNCTION - # `name::` — a namespace / scope qualifier (one `::` token, or `:` then `:`). + # `name::`; a namespace / scope qualifier (one `::` token, or `:` then `:`). if _is_scope(nxt) or (_is_colon(nxt) and len(after) > 1 and _is_colon(after[1])): return _NAMESPACE - # `::name` — a qualified type or member. + # `::name`; a qualified type or member. if _is_scope(prev) or (_is_colon(prev) and len(before) > 1 and _is_colon(before[1])): return _TYPE - if len(text) >= 2 and text.isupper(): # ALL_CAPS — a constant / macro. + if len(text) >= 2 and text.isupper(): # ALL_CAPS: a constant / macro. return _CONSTANT - if text[:1].isupper(): # PascalCase — a type / class. + if text[:1].isupper(): # PascalCase: a type / class. return _TYPE return _VARIABLE @@ -143,7 +143,7 @@ def highlight_fenced(code: str, language: str | None) -> Content: ``textual.highlight.highlight``, but maps through :class:`FNDSyntaxTheme`, refines bare ``Name`` tokens positionally, and rainbow-colours brackets.""" # `guess_language` returns "text"/"default" (never falsy) and a bad name - # raises ClassNotFound below — so no crash today; the trailing "text" only + # raises ClassNotFound below, so no crash today; the trailing "text" only # guards against ever passing None if those contracts shift across versions. language = language or guess_language(code, None) or "text" code = "\n".join(code.splitlines()) @@ -162,7 +162,7 @@ def _build_spans(tokens: Sequence[_Tok], *, neutral_operators: bool = False) -> Shared by the Pygments (fence) and regex (inline) tokenizers. ``neutral_operators`` renders operator/separator chars in the neutral - foreground instead of red — for inline code, where ``/`` ``-`` ``:`` are + foreground instead of red: for inline code, where ``/`` ``-`` ``:`` are usually path/flag separators in prose, not arithmetic. The ``::`` scope heuristic is unaffected (it keys on token type, not colour).""" # Compact view (whitespace dropped) for neighbour lookups, mapping each @@ -186,7 +186,7 @@ def _build_spans(tokens: Sequence[_Tok], *, neutral_operators: bool = False) -> style = _BRACKET_CYCLE[depth % len(_BRACKET_CYCLE)] elif token_type is Token.Name and index in compact_pos: # Blank/whitespace Name tokens (some lexers, e.g. Perl, emit them) - # aren't in ``compact_pos`` — skip the positional heuristic and let + # aren't in ``compact_pos``: skip the positional heuristic and let # them fall through to the parent-walk rather than KeyError. p = compact_pos[index] before = [(compact[q][1], compact[q][2]) for q in range(p - 1, max(p - 3, -1), -1)] diff --git a/fnd/tui/upgrade_banner.py b/fnd/tui/upgrade_banner.py index 721756ee..36afc94b 100644 --- a/fnd/tui/upgrade_banner.py +++ b/fnd/tui/upgrade_banner.py @@ -1,7 +1,7 @@ """Count PDF Texture Cache entries left on a previous extractor version. When the texturising engine improves (new version, docling enabled, a -config change), already-textured PDFs keep their value — they just don't +config change), already-textured PDFs keep their value: they just don't yet benefit from the improvement. Rather than interrupt the user with a startup popup, fnd surfaces the count passively in Settings → Indexing & PDF Texture, where they can choose to re-texturise the outdated docs. diff --git a/fnd/tui/widgets/detail_strip.py b/fnd/tui/widgets/detail_strip.py index 8c363dcf..8fc95575 100644 --- a/fnd/tui/widgets/detail_strip.py +++ b/fnd/tui/widgets/detail_strip.py @@ -1,4 +1,4 @@ -"""DetailStrip — 2-line description + metadata area at the bottom of +"""DetailStrip: 2-line description + metadata area at the bottom of every settings screen. Empty by default. The parent screen calls ``set(description, metadata)`` @@ -31,7 +31,7 @@ class DetailStrip(Widget): Height is ``auto`` so the description wraps onto as many lines as it needs without truncating. ``max-height: 6`` bounds the panel - so a runaway-long description can't dominate the screen — keep + so a runaway-long description can't dominate the screen, keep descriptions concise enough to fit comfortably. */ DetailStrip { height: auto; @@ -72,7 +72,7 @@ def _refresh_strip(self) -> None: pass def _render_lines(self) -> tuple[Content, Content]: - """Pure render — tested directly without mounting the widget.""" + """Pure render: tested directly without mounting the widget.""" description = self._description if not isinstance(description, Content): description = Content(display_block(description)) diff --git a/fnd/tui/widgets/markdown.py b/fnd/tui/widgets/markdown.py index b3ed0b18..4f090b93 100644 --- a/fnd/tui/widgets/markdown.py +++ b/fnd/tui/widgets/markdown.py @@ -73,7 +73,7 @@ # of markdown-it tokens (headings, paragraphs, lists, blockquotes, # tables, fenced code, etc.). We subclass the block kinds whose inline # text should carry search-term highlights and overlay -# ``Content.add_spans`` after the base build runs — match-only spans +# ``Content.add_spans`` after the base build runs: match-only spans # layered on top of whatever style the base block produced. Code # fences (``MarkdownFence``) carry the overlay too (``FNDMarkdownFence``): # the match highlight reads over the lexer's syntax colours so a query @@ -87,7 +87,7 @@ def _build_match_spans(plain: str, spec: MatchSpec) -> list[Span]: """Return a list of highlight spans covering every word in ``plain`` - that matches ``spec`` under any of the cascade's pass semantics — + that matches ``spec`` under any of the cascade's pass semantics: exact-stem (literal / phrase / synonym) or fuzzy-AUTO. Char-level colour split: literal / synonym matches → one yellow @@ -108,7 +108,7 @@ def _build_match_spans(plain: str, spec: MatchSpec) -> list[Span]: def _build_match_spans_multi(plains: Sequence[str], spec: MatchSpec) -> list[list[Span]]: """Per-segment highlight spans with the proximity window computed across the - WHOLE sequence — see :func:`fnd.render.match_word_spans_multi`. Segments are + WHOLE sequence: see :func:`fnd.render.match_word_spans_multi`. Segments are the consecutive blocks of one chunk.""" if spec.is_empty: return [[] for _ in plains] @@ -120,7 +120,7 @@ def _build_match_spans_multi(plains: Sequence[str], spec: MatchSpec) -> list[lis spans.append(Span(a, b, style)) covered.update(range(a, b)) # Phrase highlighting (quoted phrase, or a stopword between content words) - # fills only the GAPS between term spans — never overlaps them. Textual's + # fills only the GAPS between term spans: never overlaps them. Textual's # Content drops overlapping differently-styled spans, so an overlapping # phrase span in multi-colour mode would blank the whole word. Phrases stay # block-local: a quoted phrase is contiguous by definition. @@ -131,7 +131,7 @@ def _build_match_spans_multi(plains: Sequence[str], spec: MatchSpec) -> list[lis def _spans_have_full_match(spans: list[Span]) -> bool: - """True if any span carries a non-dimmed match style — i.e. a real + """True if any span carries a non-dimmed match style: i.e. a real (in-window) proximity hit, phrase, or plain match, as opposed to a proximity-dimmed out-of-window stray.""" return any(str(s.style) not in DIM_STYLES for s in spans) @@ -170,7 +170,7 @@ def _record_first_match(block: MarkdownBlock, spans: list[Span]) -> None: def _apply_highlights_after_build(block: MarkdownBlock) -> None: """``build_from_token`` postlude for a block whose parent is NOT an - ``FNDMarkdown`` — the stock Markdown widget the help overlay uses. Blocks + ``FNDMarkdown``: the stock Markdown widget the help overlay uses. Blocks under an FNDMarkdown are highlighted chunk-wide instead, by :func:`apply_chunk_highlights`, so a proximity window can straddle them.""" md = block._markdown @@ -190,7 +190,7 @@ def _block_plain(block: MarkdownBlock) -> str: """A block's own rendered text. Fences carry theirs on ``_highlighted_code`` (the syntax-coloured code, or the mermaid art we deliberately match against instead of the diagram source); every other block carries it on ``_content``. - Container blocks — lists, table wrappers — own no text of their own.""" + Container blocks (lists, table wrappers) own no text of their own.""" code = getattr(block, "_highlighted_code", None) if code is not None: return str(code.plain) @@ -202,7 +202,7 @@ def _content_blocks(blocks: Iterable[MarkdownBlock]) -> Iterator[MarkdownBlock]: Walks ``_blocks`` rather than the DOM because a table's ``MarkdownTH`` / ``MarkdownTD`` cells are consumed into a DataTable by - ``FNDMarkdownTableDT.compose`` and are never mounted — a post-mount query + ``FNDMarkdownTableDT.compose`` and are never mounted: a post-mount query would miss exactly the cells where a cross-boundary window is most common. No block has both own text and children, so nothing is counted twice.""" for block in blocks: @@ -218,7 +218,7 @@ def _set_block_spans(block: MarkdownBlock, spans: list[Span]) -> None: The cache is what survives a fence's ``notify_style_update``: that rebuilds ``_highlighted_code`` from scratch and drops every span, so without it the - fence would silently fall back to recomputing block-locally — the very scope + fence would silently fall back to recomputing block-locally; the very scope this pass exists to widen.""" block._fnd_match_spans = spans # type: ignore[attr-defined] if not spans: @@ -235,8 +235,8 @@ def _set_block_spans(block: MarkdownBlock, spans: list[Span]) -> None: def apply_chunk_highlights(md: FNDMarkdown, blocks: list[MarkdownBlock]) -> None: """Bake match highlights across a whole chunk in ONE two-tier decision. - A proximity window is scoped to the chunk — the unit the index matched the - slop-phrase in — not to a single block. Highlighting block-by-block made the + A proximity window is scoped to the chunk (the unit the index matched the + slop-phrase in) not to a single block. Highlighting block-by-block made the token positions restart at every paragraph and list item, so a genuine ``{N}`` co-occurrence split across two blocks could never qualify and both terms rendered dimmed at any slop. @@ -263,7 +263,7 @@ def _apply_inline_code_highlights(block: MarkdownBlock) -> None: """Syntax-colour inline code (`` `x` ``) the same way fenced blocks are coloured. The base build marks inline code with the ``.code_inline`` style; we tokenise each such run and overlay per-token colours on top - (foreground only — the inline-code background, if any, shows through). + (foreground only: the inline-code background, if any, shows through). Runs before the match overlay so search terms still read over it.""" content = block._content syntax_spans: list[Span] = [] @@ -297,7 +297,7 @@ class _HeadingMarkerMixin: not a second style band. Must sit *inside* ``_HighlightingBlockMixin`` in the MRO so the - marker lands on the Content **before** highlight spans run — that + marker lands on the Content **before** highlight spans run: that way the highlight span offsets line up with the post-prefix plain. """ @@ -389,7 +389,7 @@ class FNDMarkdownFence(MarkdownFence): """Code fence that overlays search-term highlights on the syntax colouring. Stock ``MarkdownFence`` builds a syntax-highlighted ``Content`` in ``__init__`` and renders it via a single Label, with - no hook for match spans — so query terms inside a code block went + no hook for match spans, so query terms inside a code block went unhighlighted. We add the match overlay on top of the lexer colours (the highlight reads over them) right after the base build, and re-apply it when a theme change rebuilds the syntax colouring. @@ -405,7 +405,7 @@ class FNDMarkdownFence(MarkdownFence): def highlight(cls, code: str, language: str, ansi: bool = False, dark: bool = False) -> Content: # Stock highlighting maps Pygments tokens through a sparse theme; # swap in the granular FND palette. Native-ANSI terminals keep the - # base ANSI themes (safety net — the app runs truecolor by default). + # base ANSI themes (safety net: the app runs truecolor by default). if ansi: return super().highlight(code, language, ansi=ansi, dark=dark) return highlight_fenced(code, language or None) @@ -432,7 +432,7 @@ def _try_render_mermaid(self, token: Any, code: str) -> bool: # matches overlay the *art*, not the fence source. The art is derived # from the source, so a term in a node label survives into it and # highlights like any other text; a term living only in mermaid syntax - # (arrows, node ids) does not, and must not register as a match stop — + # (arrows, node ids) does not, and must not register as a match stop: # n/b and the ▲/▼ counts would then point at a diagram with nothing # visibly highlighted. self._set_diagram_content(art) @@ -447,7 +447,7 @@ def _set_diagram_content(self, art: Text) -> None: def notify_style_update(self) -> None: # The base rebuilds ``_highlighted_code`` from scratch on a theme - # change, dropping our content — re-render the diagram if this is a + # change, dropping our content: re-render the diagram if this is a # mermaid fence, else re-apply the match overlay. super().notify_style_update() code = getattr(self, "_mermaid_code", None) @@ -457,7 +457,7 @@ def notify_style_update(self) -> None: self._set_diagram_content(art) self._reapply_cached_highlights() return - # Re-render failed — this is no longer a diagram: drop the + # Re-render failed: this is no longer a diagram: drop the # diagram-only styling (hscroll/no-wrap) before falling back. The # cached spans were computed against the ART, so they address text # that no longer exists; drop them rather than replay them onto the @@ -473,7 +473,7 @@ def _reapply_cached_highlights(self) -> None: ``notify_style_update`` rebuilds ``_highlighted_code`` from scratch on every theme change (and fires several times during the initial mount), dropping all spans. Recomputing here would use the fence's own text - alone, undoing the chunk-wide proximity scope — so replay the cache + alone, undoing the chunk-wide proximity scope, so replay the cache instead. The length check is a backstop against a stale cache; the one case that actually invalidates it (a failed mermaid re-render) clears the cache at source.""" @@ -495,20 +495,20 @@ class FNDMarkdownTableDT(MarkdownTable): ``_FND_NO_W3=1`` (an opt-OUT, set nowhere by default). The DataTable path mounts ~4x fewer widgets and reflows ~2x faster on a width change (e.g. toggling Reading View) than the stock grid. (The old - ``_FND_W3_DATATABLE`` opt-in flag is gone — it is read nowhere.) + ``_FND_W3_DATATABLE`` opt-in flag is gone: it is read nowhere.) Styling matches Textual's MarkdownTable as closely as DataTable allows: rounded outer border + zebra rows + primary-coloured bold header. The inter-cell keylines that grid-layout MarkdownTableContent - gets aren't reachable on DataTable — it renders cells as a single + gets aren't reachable on DataTable: it renders cells as a single render output, not as keyline-eligible child widgets. """ DEFAULT_CSS = """ FNDMarkdownTableDT { & > DataTable { - /* Textual's DataTable defaults to ``max-height: 100%`` — one - CONTAINER height — so a table with more rows than fit in the + /* Textual's DataTable defaults to ``max-height: 100%``, one + CONTAINER height, so a table with more rows than fit in the chunk becomes a nested scroll region with its own scrollbar: scrolling the document into it scrolls the table instead, until it bottoms out. No content is unreachable, but the table is a @@ -564,7 +564,7 @@ def compose(self): # type: ignore[override] self._dt = dt # Compute per-column widths from the pane's content size so wide # cells wrap rather than overflow. Without this, DataTable's - # auto_width measures each column at its longest single line — + # auto_width measures each column at its longest single line: # paragraph cells produce ~700-cell columns that get truncated # to the pane's 91 cells, with no wrap. avail = self._available_table_width() @@ -581,7 +581,7 @@ def compose(self): # type: ignore[override] dt.add_row(*row, height=None) md = self._markdown # Tiers come from the cells' cached chunk-scoped spans, so a window - # spanning two cells of a row counts as full — see + # spanning two cells of a row counts as full: see # ``_match_coords_from_blocks``. Falls back to the spec-derived scan for # a non-FNDMarkdown parent, which never ran the chunk pass. if isinstance(md, FNDMarkdown): @@ -595,7 +595,7 @@ def compose(self): # type: ignore[override] if matches: match_coord, is_full = matches[0] dt._fnd_match_coord = Coordinate(*match_coord) # type: ignore[attr-defined] - # Register self as parent's match block — TH/TD widgets are + # Register self as parent's match block: TH/TD widgets are # bypassed so _record_first_match never fires. The table is one # stop in match_blocks; nav expands it to its matching cells via # _fnd_match_coords. @@ -623,7 +623,7 @@ def _available_table_width(self) -> int: return avail def on_resize(self, _event: events.Resize) -> None: - """Recompute column widths when the block's width changes — e.g. + """Recompute column widths when the block's width changes: e.g. toggling Reading View hides the sidebar and widens the pane. The DataTable's columns are sized once at ``compose``; without this they stay at the old width and the table reads compressed in the wider @@ -714,7 +714,7 @@ def _longest_line(s: str) -> int: return [max(m, mn) for m, mn in zip(maxs, mins, strict=True)] total_min = sum(mins) if total_min >= inner_avail: - # Mins alone exceed budget — scale every column down proportionally + # Mins alone exceed budget: scale every column down proportionally # (loses some content, but the only alternative is overflow). scale = inner_avail / total_min return [max(1, int(m * scale)) for m in mins] @@ -737,12 +737,12 @@ def _find_match_coords_in_table( ) -> list[tuple[tuple[int, int], bool]]: """Every matching cell as ``((row, col), is_full)``, full matches first. - A cell matches iff ``text_has_any_match`` does — a word match OR a - quoted-phrase span, the same gate the highlight overlay applies — so each + A cell matches iff ``text_has_any_match`` does: a word match OR a + quoted-phrase span, the same gate the highlight overlay applies, so each coordinate points at a cell that is actually highlighted (quoted phrases included). (Checking the Content's ``spans`` instead is wrong: that set also - carries the markdown styling spans — inline code, emphasis, links — + carries the markdown styling spans (inline code, emphasis, links) so a merely *styled* cell would slip in over a *matched* one.) ``text_has_any_match`` short-circuits on the first matching word and skips the per-char alignment / Span allocation that building the full @@ -783,7 +783,7 @@ def _merge_cell_tiers( Header hits and a first-data-row hit both map to ``(0, col)`` (the header has no cursor coordinate of its own), so a match in both would emit the - coordinate twice — inflating the count and making n/b land on it twice. + coordinate twice: inflating the count and making n/b land on it twice. Merge by coordinate, keeping the strongest tier (full beats dim-only) and first-seen order (headers before rows).""" tier_by_coord: dict[tuple[int, int], bool] = {} @@ -813,7 +813,7 @@ def _match_coords_from_blocks(table: Any) -> list[tuple[tuple[int, int], bool]]: The spec-derived :func:`_find_match_coords_in_table` re-tests each cell's text on its own, which reinstates the per-block window the chunk pass exists - to widen — a ``{N}`` co-occurrence split across two cells of one row would + to widen; a ``{N}`` co-occurrence split across two cells of one row would sort as dim-only. Reading the cached spans keeps the tier consistent with what was actually painted.""" from textual.widgets._markdown import MarkdownTD, MarkdownTH, MarkdownTR @@ -831,7 +831,7 @@ def _tier(block: MarkdownBlock) -> bool | None: header_tiers.append(_tier(block)) elif isinstance(block, MarkdownTR): row_tiers.append([]) - # A TD before any TR means a table with no header row — not reachable + # A TD before any TR means a table with no header row, not reachable # through GFM, which requires the delimiter row, but an unguarded # ``row_tiers[-1]`` here would take the whole preview down. elif isinstance(block, MarkdownTD) and row_tiers: @@ -879,7 +879,7 @@ class FNDMarkdown(Markdown): stashed on the instance so each block subclass can read them during ``build_from_token``. ``first_match_block`` resolves to the earliest block in document order whose Content gained at least - one highlight span — the preview pane scrolls to it so the user + one highlight span: the preview pane scrolls to it so the user sees the match without manual scrolling. """ @@ -900,14 +900,14 @@ class FNDMarkdown(Markdown): FNDMarkdown FNDMarkdownH4 { color: $accent; text-style: underline; } FNDMarkdown FNDMarkdownH5 { color: $accent; text-style: none; } FNDMarkdown FNDMarkdownH6 { color: $accent 70%; text-style: none; } - /* Inline emphasis colour-shifts too — text-style alone is too + /* Inline emphasis colour-shifts too, text-style alone is too subtle to read at terminal weight on most fonts. ``$primary`` contrasts with ``$accent`` (headings) so bold inside a heading is still visible. */ FNDMarkdown MarkdownBlock > .strong { color: $primary; text-style: bold; } FNDMarkdown MarkdownBlock > .em { color: $secondary; text-style: italic; } /* Callouts: typed left bar plus a 12% tint of the same colour. ``outer`` - is a half-block glyph — the only border style that tiles solid in every + is a half-block glyph: the only border style that tiles solid in every terminal font we tested, SF Mono included. */ FNDMarkdown .callout { border-left: outer $primary; background: $primary 12%; } FNDMarkdown .callout > .callout-title { color: $primary; text-style: bold; } @@ -1014,18 +1014,18 @@ def first_match_block(self) -> MarkdownBlock | None: @property def match_blocks(self) -> list[MarkdownBlock]: - """Match stops of this chunk for intra-file nav, in REGISTRATION order — + """Match stops of this chunk for intra-file nav, in REGISTRATION order, which is document order only until a table joins (it registers at mount). Order by painted row with :func:`~fnd.tui.preview.match_row.chunk_stop_rows`.""" return self._match_blocks or self._dim_match_blocks def update(self, markdown): # type: ignore[no-untyped-def, override] # Textual's dispatcher walks the MRO and invokes every class's - # _on_mount — overriding _on_mount and calling super() ran + # _on_mount: overriding _on_mount and calling super() ran # Markdown._on_mount twice; the second pass saw _initial_markdown # already consumed and called update("") which removed all # blocks. Hook into update() instead: AwaitComplete's future - # fires when parse+mount completes — set build_done from there. + # fires when parse+mount completes, set build_done from there. # # Reset document-scoped match state before the rebuild: a re-update # must not let a build_done waiter return on the prior render, and @@ -1051,7 +1051,7 @@ def _parse_markdown(self, tokens): # type: ignore[no-untyped-def, override] rewrite_callouts(tokens) # Materialise the block tree before yielding any of it: the two-tier # proximity decision needs the whole chunk's text at once, and right here - # every block — including the TH/TD cells the table later composes away — + # every block, including the TH/TD cells the table later composes away, # is still reachable and unmounted, so applying spans costs no layout. blocks = list(super()._parse_markdown(tokens)) apply_chunk_highlights(self, blocks) @@ -1063,7 +1063,7 @@ def _legacy_blocks_to_md(blocks: list[Any]) -> str: Block list. Used as a defensive fallback for chunks with empty ``body_md`` (e.g. an old index that slipped through the schema check). Never produces structurally-correct markdown for tables / - nested lists / fenced code — those round-tripping properly is + nested lists / fenced code: those round-tripping properly is exactly what the schema bump and extractor rewrite are for. Just enough to keep a stale-index preview from rendering empty. """ diff --git a/fnd/tui/widgets/md_inline.py b/fnd/tui/widgets/md_inline.py index e6a1680a..8cf0c4be 100644 --- a/fnd/tui/widgets/md_inline.py +++ b/fnd/tui/widgets/md_inline.py @@ -49,7 +49,7 @@ def has_match(text: str, spec: MatchSpec) -> bool: - """True when ``spec`` matches a word in ``text`` — the reveal test.""" + """True when ``spec`` matches a word in ``text``: the reveal test.""" if spec.is_empty or not text: return False return bool(match_word_spans(text, spec)) diff --git a/fnd/tui/widgets/preview_container.py b/fnd/tui/widgets/preview_container.py index 938daa51..2e433708 100644 --- a/fnd/tui/widgets/preview_container.py +++ b/fnd/tui/widgets/preview_container.py @@ -22,7 +22,7 @@ class PreviewContainer(Container): One container per cached file lives inside ``#preview_pane``; only one is ``-active`` (visible) at a time. Switching files toggles - classes — no remount cost. Each container tracks which chunk + classes: no remount cost. Each container tracks which chunk indices have been mounted so a partial-then-cancelled mount can be resumed on revisit. """ @@ -33,7 +33,7 @@ class PreviewContainer(Container): PreviewContainer { width: 100%; height: auto; } PreviewContainer.-hidden { display: none; } /* opacity:0 (not visibility:hidden) so the pane can be scrolled to the - match WHILE the container is invisible — scroll_to_region is a no-op + match WHILE the container is invisible, scroll_to_region is a no-op under visibility:hidden, which forced a reveal-at-top-then-scroll jump. The reveal lands only after the scroll commits (see _finalise_via_lock). */ PreviewContainer.-pre-reveal { opacity: 0%; } @@ -60,7 +60,7 @@ def __init__( # a frame or two after the reveal has been decided. self.has_painted: bool = False # How this container's chunks were obtained. The one number that - # says whether the capture cache is doing anything at all — its + # says whether the capture cache is doing anything at all: its # absence hid a cache that stored thousands of captures and served # none of them. self.served_chunks: int = 0 @@ -76,8 +76,8 @@ def is_complete(self) -> bool: def get_content_height(self, container, viewport, width): # type: ignore[no-untyped-def] # Textual's VerticalLayout has an "all children are dynamic - # height" shortcut that arranges them inside ``container.height`` - # — fine for flex-style nested containers, wrong here because + # height" shortcut that arranges them inside ``container.height``: + # fine for flex-style nested containers, wrong here because # this widget is the scrollable canvas of #preview_pane. The # shortcut caps total height at the pane's viewport height # (~19 cells), so chunks past that y are positioned in widget @@ -112,7 +112,7 @@ def __getattr__(self, name: str) -> Any: class PreviewCache: """LRU cache of :class:`PreviewContainer`, keyed by ``(parent_doc_id, query_signature)``. Files with fewer than - :data:`tuning.PREVIEW_CACHE_MIN_CHUNKS` chunks are NOT cached — they + :data:`tuning.PREVIEW_CACHE_MIN_CHUNKS` chunks are NOT cached; they mount fast enough that keeping the widget tree alive isn't worth the memory. """ @@ -143,12 +143,12 @@ def put( """Cache ``container`` and return any LRU-evicted containers for the caller to remove. ``protect`` is skipped during eviction. - ``protect`` is load-bearing, not just tidy: a STALE mount — one an - overshoot-and-return navigation cancelled — runs its ``finally`` LATE, + ``protect`` is load-bearing, not just tidy: a STALE mount, one an + overshoot-and-return navigation cancelled, runs its ``finally`` LATE, after a newer navigation has already re-activated a *different* container. With ``max_files == 1`` a put that protected only the entry it was inserting would evict that newly-active container out of the DOM - while ``self.active`` still pointed at it — a detached-active blank pane + while ``self.active`` still pointed at it: a detached-active blank pane that never self-heals. Callers therefore pass ``protect=`` so the active preview is never the one evicted (the stale/incoming container is evicted instead, keeping the cache at ``max_files``).""" diff --git a/fnd/tui/widgets/results_tree.py b/fnd/tui/widgets/results_tree.py index d5940da8..53b37918 100644 --- a/fnd/tui/widgets/results_tree.py +++ b/fnd/tui/widgets/results_tree.py @@ -29,8 +29,8 @@ class ResultsTree(HomeToFirstRow, CollectionMarkLabel, StateMarkerLabel, PlainTr Also owns "scan mode": Option/Alt + ↑/↓ move the cursor WITHOUT loading the preview (browse fast with no mount per row); a normal ↑/↓ ends scan mode so the preview loads where you land, and Enter loads the highlighted row - (wired in the app). Handling this in the tree's own actions — rather than a - bubbled ``app.on_key`` — is reliable: the focused tree always runs them, + (wired in the app). Handling this in the tree's own actions, rather than a + bubbled ``app.on_key``, is reliable: the focused tree always runs them, where a key the tree consumes may never reach the app. Earlier the rule was enforced after-the-fact by ``_on_tree_highlight`` @@ -38,7 +38,7 @@ class ResultsTree(HomeToFirstRow, CollectionMarkLabel, StateMarkerLabel, PlainTr for a frame and then bounce. With a slow preview load on top, the bounce became visible and felt like a glitchy jump. - Validating in :meth:`validate_cursor_line` is atomic — the cursor + Validating in :meth:`validate_cursor_line` is atomic; the cursor never lands on an expanded parent in the first place. Pressing ↓ from the row above an expanded parent moves directly to the parent's first child; pressing ↑ from a child moves directly to the row above @@ -53,7 +53,7 @@ class ResultsTree(HomeToFirstRow, CollectionMarkLabel, StateMarkerLabel, PlainTr class ReopenRequested(Message): """Posted when the pane is clicked while collapsed-to-header. The app - reopens the panel (and expands the clicked node) — a toggle the user + reopens the panel (and expands the clicked node): a toggle the user can't see is useless, so the click should surface content instead.""" def __init__(self, tree: ResultsTree, node: TreeNode[Any] | None) -> None: @@ -91,7 +91,7 @@ def _toggle_node(self, node: Any) -> None: # otherwise strand the cursor: Textual keeps the cursor *line index* # across the rebuild, so it lands on whatever row slides up into that # line (the section below the one just collapsed). Follow the cursor up - # onto the node being collapsed instead — that's the row the user is + # onto the node being collapsed instead: that's the row the user is # acting on. Captured before the collapse so the check is reliable. follow = ( node.is_expanded @@ -118,7 +118,7 @@ async def _on_click(self, event: events.Click) -> None: # a click to EVERY ``_on_click`` found while walking the MRO by naming # convention (message_pump._get_dispatch_methods), so the base # ``Tree._on_click`` already runs for us. Calling super as well runs it - # a second time — one physical click then toggles a node twice + # a second time: one physical click then toggles a node twice # (expand+collapse = nothing happens, e.g. clicking an expand arrow) or # posts ``NodeSelected`` twice (a leaf toggles on then off). Overriding # to ADD behaviour is fine; just never re-invoke the base handler. @@ -138,7 +138,7 @@ async def _on_click(self, event: events.Click) -> None: def _set_scan(self, scanning: bool) -> None: # Scan mode drives the PREVIEW, so only the results pane owns it. - # ResultsTree is also used for the Filters panel (app.py) — Option+arrow + # ResultsTree is also used for the Filters panel (app.py): Option+arrow # there must not flip the preview's scan flag and suppress a later load. if self.id != "results_pane": return @@ -147,15 +147,15 @@ def _set_scan(self, scanning: bool) -> None: preview._scan_move = scanning def on_key(self, event: events.Key) -> None: - # Any non-scan key ends scan mode — not just ↑/↓ but home/end/pageup/ - # pagedown/typing too — so a later move always loads instead of being + # Any non-scan key ends scan mode, not just ↑/↓ but home/end/pageup/ + # pagedown/typing too, so a later move always loads instead of being # silently suppressed. on_key runs before the key's binding, so the # cursor move that follows isn't treated as a scan. if event.key not in ("alt+up", "alt+down"): self._set_scan(False) def on_mouse_down(self, event: events.MouseDown) -> None: - # Clicking a result is a deliberate selection, never a silent scan — + # Clicking a result is a deliberate selection, never a silent scan, # otherwise mouse users could get stuck with the preview not updating. self._set_scan(False) @@ -191,13 +191,13 @@ def _is_selectable_when_expanded(self, node: Any) -> bool: Default False: results file rows are dead when open, so the cursor skips past them rather than parking where Enter does nothing. The - filters pane is the opposite — every row there *does* something on + filters pane is the opposite: every row there *does* something on Enter/click, so all its parent kinds stay selectable while expanded: ``filter_category`` section headers (Enter/click collapses them), ``kind_category`` file-type rows (toggle the whole category) and ``filter_value`` tag rows (toggle the tag). Keeping headers selectable also stops a click on an *expanded* header from drifting the cursor - down onto — and toggling — its first child. + down onto, and toggling, its first child. """ data = node.data if isinstance(node.data, dict) else None return bool(data) and data.get("kind") in ( @@ -234,7 +234,7 @@ def validate_cursor_line(self, value: int) -> int: return target next_target = target + direction if next_target < 0 or next_target > last: - # Boundary — don't shove the cursor off the edge; keep + # Boundary: don't shove the cursor off the edge; keep # it where it was so the press feels like a no-op # instead of a jump. return current @@ -247,21 +247,21 @@ def validate_cursor_line(self, value: int) -> int: # Navigation cost is bimodal: a jump into a file whose hits are captured # is a blit, one into a file that still has to be built can be seconds # (see fnd/tui/preview/coverage.py). The arrow says which you are about - # to get, on the two cells the tree already spends on it — so this costs + # to get, on the two cells the tree already spends on it, so this costs # no label width, which matters in a pane whose name budget is already # `width - 2 - 7`. # - # Shape carries the fact that changes a decision — hollow means a jump + # Shape carries the fact that changes a decision: hollow means a jump # here will build, filled means it will not. Colour carries the rest: # blue for cold (the score column's own accent blue, so it reads as part # of the same palette), the theme accent for both warm states. Cold and - # warm are therefore a change of HUE, not of brightness — a muted-vs- + # warm are therefore a change of HUE, not of brightness: a muted-vs- # accent version of the same glyph was tried on paper and rejected, # because at one cell the two are hard to tell apart. # # Not on match rows. Coverage warms a file's hits nearest-first, so they # are all ready within moments of landing, and those rows already carry a - # glyph for matches the preview cannot highlight — two unrelated marker + # glyph for matches the preview cannot highlight: two unrelated marker # systems on one row cost more than they tell you. ICON_WARM = "▶ " ICON_WARM_EXPANDED = "▼ " @@ -284,7 +284,7 @@ def validate_cursor_line(self, value: int) -> int: } DEFAULT_CSS = """ - /* Cold takes the accent blue the score column uses for its middle tier — + /* Cold takes the accent blue the score column uses for its middle tier: a file that has to build is not a problem, so it wears an ordinary colour. Warm takes the theme accent. The two therefore differ in HUE, not in brightness: at one cell a muted-versus-bright version of the @@ -318,11 +318,11 @@ def render_label(self, node: TreeNode[Any], base_style: Style, style: Style) -> icon = self.ICON_BUILDING if building else self.ICON_WARM # Inherit the stock icon's style rather than rebuilding it. It carries # the meta that makes clicking the arrow expand the node, and that meta - # lives in a private Textual module — importing it would let a minor + # lives in a private Textual module: importing it would let a minor # upgrade turn a decoration into an ImportError at app start. icon_style: Any = stock.spans[0].style if stock.spans else base_style # Colour EVERY state, not just cold. Inheriting the stock style for the - # warm ones left them in the plain foreground — the filled arrow came + # warm ones left them in the plain foreground: the filled arrow came # out white and nothing ever turned accent, which is the whole signal. # Taken from a component class rather than a constant so it follows the # theme, like the progress line's own fill does. @@ -358,7 +358,7 @@ def _warm_state_of(self, node: TreeNode[Any]) -> WarmState | None: def apply_warm_states(self, states: dict[str, WarmState]) -> bool: """Adopt ``states`` and repaint only the rows that changed. - Repainting the whole tree on every capture would strobe the list — + Repainting the whole tree on every capture would strobe the list: captures land at roughly ten a second. Diffing means a row is touched only when its state actually moves, which for a given file happens at most three times: into WARMING, into READY, then into FULL. diff --git a/fnd/tui/widgets/toggle_tree.py b/fnd/tui/widgets/toggle_tree.py index e7ae1448..204ab3b9 100644 --- a/fnd/tui/widgets/toggle_tree.py +++ b/fnd/tui/widgets/toggle_tree.py @@ -5,7 +5,7 @@ source-creation Includes picker share identical, bug-free behaviour instead of each re-deriving it (and re-deriving the same bugs). -Correct by construction — each property kills a class of bug the ad-hoc trees hit: +Correct by construction: each property kills a class of bug the ad-hoc trees hit: * ``auto_expand = False`` and Enter = *toggle only*; ←/→ = expand/collapse. Toggling a category can never also expand/collapse it. diff --git a/fnd/vocabulary.py b/fnd/vocabulary.py index 08f63d14..60bea8f6 100644 --- a/fnd/vocabulary.py +++ b/fnd/vocabulary.py @@ -1,7 +1,7 @@ """Closed value sets for the filters a user can name on the command line. -A :class:`Vocabulary` is the legal universe for one filter — collection -names, file kinds, date tokens — so a value outside it fails loudly with a +A :class:`Vocabulary` is the legal universe for one filter (collection +names, file kinds, date tokens) so a value outside it fails loudly with a suggestion rather than surviving as a clause that can never match. Both failure modes are silent today: an unknown ``-c`` name becomes a hard ``F_COLLECTION`` term that matches nothing, and an unknown ``--kind`` @@ -79,7 +79,7 @@ def match(self, raw: str) -> str | None: def suggest(self, raw: str) -> list[str]: """Names close enough to ``raw`` to have been the intent, best first. - A case-only variant always qualifies — it costs no edits, and on a + A case-only variant always qualifies: it costs no edits, and on a case-sensitive vocabulary it's the whole reason the value missed. Anything else has to fall inside the usual typo tolerance. @@ -118,7 +118,7 @@ def split_resolve(self, raw: str) -> tuple[list[str], list[str]]: A whole-string match is tried first so a legacy config name that itself contains a comma still resolves; only then is the value split. Names may legally contain spaces, so parts are stripped, not - tokenised. Each caller decides what to do with the unknowns — the CLI + tokenised. Each caller decides what to do with the unknowns: the CLI reports them, the sidebar drops them. """ whole = self.match(raw) @@ -137,7 +137,7 @@ def split_resolve(self, raw: str) -> tuple[list[str], list[str]]: return list(dict.fromkeys(known)), list(dict.fromkeys(unknown)) def unknown(self, raw: str, *, flag: str | None = None) -> UnknownFilterValueError: - """Build the error for ``raw`` without raising it — for callers that + """Build the error for ``raw`` without raising it: for callers that collect several problems before reporting any of them.""" return UnknownFilterValueError( label=self.label, diff --git a/fnd/walk.py b/fnd/walk.py index f7da6fcc..dbcd36b6 100644 --- a/fnd/walk.py +++ b/fnd/walk.py @@ -1,20 +1,20 @@ -"""Filesystem walker — yield supported files under a collection's roots. +"""Filesystem walker: yield supported files under a collection's roots. Includes/excludes precedence: 1. A path is in scope only if it lives under one of ``roots``. 2. If ``includes`` is set, the path must match at least one ``includes`` glob. -3. If the path matches **any** ``excludes`` glob, it is dropped — even if it +3. If the path matches **any** ``excludes`` glob, it is dropped, even if it matched an ``includes``. 4. Hidden files (``.foo``) are excluded by default. Only an include glob that names a dot-prefixed component admits one, and only the paths that glob itself matches; ``**/*.md`` alongside it does not widen the exception. 5. Symlinks are followed only if ``follow_symlinks = True``. This applies in two places: - - The collection root itself — if the user-supplied ``root`` is a symlink, + - The collection root itself, if the user-supplied ``root`` is a symlink, it is refused unless ``follow_symlinks=True``. This blocks a hostile config (or a typo) from pointing fnd at ``/etc`` via a symlinked root. - - Each file inside the tree — symlinked files are skipped when the flag is + - Each file inside the tree: symlinked files are skipped when the flag is off. Directory symlinks are not recursed into (we pass ``recurse_symlinks=False`` to ``Path.rglob`` rather than relying on the Python 3.13 default). @@ -88,7 +88,7 @@ def walk( ) -> Iterator[Path]: """Yield supported files under ``roots`` in deterministic order. - ``skip_dirs`` is a set of directory basenames pruned at descent — any + ``skip_dirs`` is a set of directory basenames pruned at descent: any directory whose ``name`` is in the set is not entered. Default is :data:`fnd.config.DEFAULT_JUNK_DIRS` so callers that don't pass this parameter get the expected developer-junk prune. Pass ``frozenset()`` @@ -187,12 +187,12 @@ def _scandir_walk( """DFS via ``os.scandir`` so excluded directories aren't descended. Children are sorted by name within each directory so traversal order - is deterministic and stable across platforms — the legacy ``rglob`` + is deterministic and stable across platforms: the legacy ``rglob`` relied on the filesystem's ordering, which is good enough for the indexer but causes flaky tests when the order leaks into assertions. """ # An index directory used directly as a scan root would otherwise have its - # internals (Tantivy meta.json, the schema sidecar, …) yielded — the + # internals (Tantivy meta.json, the schema sidecar, …) yielded: the # per-child guard below only catches index dirs *nested* under the root. # Ignore files apply from the source root downwards; see ancestor_stack. base = ancestor_stack(root, ignore_names) @@ -246,7 +246,7 @@ def _scandir_walk( if name in skip_dirs: continue # Never descend into an fnd index directory (identified by its - # schema-version sidecar) — otherwise the walker would index + # schema-version sidecar), otherwise the walker would index # fnd's own internals (e.g. the Tantivy meta.json) when an # index lives inside a scanned corpus. if _is_index_dir(entry.path): @@ -269,7 +269,7 @@ def _scandir_walk( if not follow_symlinks and is_symlink: continue # Suffix check uses the basename string so we skip Path() - # allocation for files that can't be indexed anyway — a + # allocation for files that can't be indexed anyway: a # measurable win on trees with many out-of-scope files # (icons, lockfiles, …) sitting next to in-scope ones. dot = name.rfind(".") @@ -281,8 +281,8 @@ def _scandir_walk( rel = entry_path.relative_to(root) except ValueError: continue - # ``as_posix`` (not ``str``) so include/exclude globs — which are - # always ``/``-delimited — match on Windows, where ``str(Path)`` + # ``as_posix`` (not ``str``) so include/exclude globs, which are + # always ``/``-delimited: match on Windows, where ``str(Path)`` # would yield backslash separators and never match. rel_str = rel.as_posix() diff --git a/pyproject.toml b/pyproject.toml index b67a7d1b..36f18161 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,7 @@ markers = [ # a worker thread. In production this blocks on the cross-thread future # and returns the callback's value cleanly; in `app.run_test()` the # event loop teardown races the worker so the internal `run_callback` -# coroutine is occasionally orphaned. Behaviour is correct — the +# coroutine is occasionally orphaned. Behaviour is correct: the # warning is purely a test-env artifact. filterwarnings = [ "ignore:coroutine 'App.call_from_thread..run_callback' was never awaited:RuntimeWarning", @@ -103,7 +103,7 @@ dev = [ # Full suite: 26:48 serial, 6:23 at -n 6 on a 10-core machine. "pytest-xdist>=3.6", ] -# Structured PDF rendering — opt-in. Lives in a PEP 735 dependency +# Structured PDF rendering: opt-in. Lives in a PEP 735 dependency # group rather than [project.optional-dependencies] so the user can # opt in via [tool.uv] default-groups below, and a subsequent # ``uv sync`` (no flags) won't wipe the install. ``fnd extras diff --git a/ruff.toml b/ruff.toml index ac5c3ac9..d2ff76fc 100644 --- a/ruff.toml +++ b/ruff.toml @@ -26,12 +26,12 @@ ignore = [ # pattern used across the codebase (`assert isinstance(x, T)` so # the type checker narrows). Convertible to `if not isinstance: # raise TypeError`, but the trade is more verbose code for no - # runtime safety gain — Python's `-O` flag strips asserts, but + # runtime safety gain, Python's `-O` flag strips asserts, but # we don't ship with `-O`. "S101", # S603: every subprocess call in this codebase uses list-form argv # (no shell=True, no string interpolation). S603 fires on every - # subprocess invocation regardless of input safety — the rule + # subprocess invocation regardless of input safety: the rule # can't tell argv-list calls apart from shell=True calls. Manually # audited; see SECURITY.md for the catalogue. "S603", @@ -50,7 +50,7 @@ extend-immutable-calls = ["typer.Option", "typer.Argument"] # best-effort pattern for UI operations whose failure should not abort # the surrounding flow (e.g. a stale-widget refresh, a scroll target # that's been removed). Each call site has a UX-flow rationale; the -# alternative — logging every swallowed exception — would flood the +# alternative, logging every swallowed exception, would flood the # log with noise from normal rapid UI churn. "fnd/tui/**" = ["S110", "S112"] # scripts/ are dev-only TUI snapshot harnesses (one-off `make diff --git a/tests/_failure_state.py b/tests/_failure_state.py index b16b768a..a217624e 100644 --- a/tests/_failure_state.py +++ b/tests/_failure_state.py @@ -1,7 +1,7 @@ """What the app looked like when a wait gave up. -A timing failure reaches CI as one line — ``wait_until timed out after 20.0s: -`` — which names the wait but not the clause still false, nor whether +A timing failure reaches CI as one line, ``wait_until timed out after 20.0s: +``, which names the wait but not the clause still false, nor whether the poller ran 3 times or 900. This module is the snapshot appended to it: the preview pipeline's state plus the ``_diag_log`` breadcrumbs the app already emits, teed here per-test by an autouse fixture in ``conftest``. diff --git a/tests/_pilot_wait.py b/tests/_pilot_wait.py index bd5e487f..d353055d 100644 --- a/tests/_pilot_wait.py +++ b/tests/_pilot_wait.py @@ -8,7 +8,7 @@ These helpers wrap ``pilot.pause()`` so timeouts are swallowed and state changes are awaited via predicate polling on a wall-clock -budget — robust to load spikes that exceed the 30 s internal bound. +budget: robust to load spikes that exceed the 30 s internal bound. """ from __future__ import annotations @@ -41,7 +41,7 @@ async def safe_press(pilot: Pilot[None], *keys: str) -> None: """``pilot.press(*keys)`` that swallows ``WaitForScreenTimeout``. ``pilot.press`` posts keys via ``_app._press_keys`` then calls - ``_wait_for_screen`` directly — that 30 s timeout is the same load + ``_wait_for_screen`` directly: that 30 s timeout is the same load flake source we work around in ``safe_pause``. """ try: @@ -55,7 +55,7 @@ async def safe_press(pilot: Pilot[None], *keys: str) -> None: async def settle(pilot: Pilot[None], ticks: int = 4) -> None: """Repeated ``safe_pause`` to drain the message loop. ``ticks`` matches the legacy ``for _ in range(n): await pilot.pause()`` - shape — each iteration tolerates a ``WaitForScreenTimeout``.""" + shape, each iteration tolerates a ``WaitForScreenTimeout``.""" for _ in range(max(1, ticks)): await safe_pause(pilot) @@ -80,8 +80,8 @@ async def wait_stable( The event-gated replacement for ``settle(pilot, ticks=N)``: a tick count assumes each ``safe_pause`` flushes a refresh, but a load spike degrades it to ``asyncio.sleep(0)``, so exactly when the wait is needed it does least. - Sampling the thing you actually care about — a scroll offset, a virtual - size — is load-proof, and the wall-clock budget bounds it. + Sampling the thing you actually care about (a scroll offset, a virtual + size) is load-proof, and the wall-clock budget bounds it. Prefer gating on a product signal (``is_restoring``, ``pipeline_busy()``) where one exists; use this when the only evidence is geometry holding @@ -99,7 +99,7 @@ async def wait_stable( current = sample() except Exception: # Mid-rebuild the widget may not be queryable. That is "still - # moving", not a test failure — keep waiting. + # moving", not a test failure: keep waiting. current = unreadable if current == last and current is not unreadable: stable += 1 @@ -132,7 +132,7 @@ async def wait_until( """Poll ``predicate`` until truthy or ``timeout`` (wall-clock) elapses. ``quiet`` skips the state snapshot for callers that CATCH the timeout and - carry on — there the snapshot is three region walks fired mid-navigation, + carry on: there the snapshot is three region walks fired mid-navigation, in the files most sensitive to exactly that. Each iteration: evaluate predicate, then yield the event loop via @@ -177,7 +177,7 @@ async def run_search(pilot: Pilot[None], app: Any, query: str, *, timeout: float ``SearchController.run`` returns as soon as the worker is dispatched, so the old ``run(q)`` + one ``pilot.pause()`` under-waits. Gate on the - controller's own ``idle`` signal — a product signal, not a tick count. + controller's own ``idle`` signal: a product signal, not a tick count. ``timeout`` is exposed because several call sites budget longer than the default for search readiness (15 s, 30 s); without it they would gate on a diff --git a/tests/_preview_corpus.py b/tests/_preview_corpus.py index 1527efa5..0dd2fa98 100644 --- a/tests/_preview_corpus.py +++ b/tests/_preview_corpus.py @@ -18,7 +18,7 @@ def wide_doc(tmp_path: Path, tmp_index_dir: Path) -> Path: Both properties are load-bearing. Above ``FULLMOUNT_CHUNK_BUDGET`` (250) chunks the file is never full-mounted, so an out-of-window jump takes the - fresh-container rebuild path — the one that leaked. A smaller file is + fresh-container rebuild path: the one that leaked. A smaller file is full-mounted and every jump scrolls in place, which is why the first version of the reclaim test passed with the fix reverted. """ diff --git a/tests/_preview_fakes.py b/tests/_preview_fakes.py index 62c2bc83..bfcfbaaf 100644 --- a/tests/_preview_fakes.py +++ b/tests/_preview_fakes.py @@ -8,7 +8,7 @@ The stub mirrors the parts of Textual's API that ``preview/visibility.py`` uses, including the ``update`` keyword and the ``app.stylesheet`` restyle that follows it. Restyling means nothing to a stub that only holds a set of names, so both are -RECORDED — ``FakeContainer.class_calls`` and ``FakeStylesheet.updated`` — and +RECORDED, ``FakeContainer.class_calls`` and ``FakeStylesheet.updated``, and ``tests/test_preview_visibility.py`` asserts against them. Recording without an assertion would be decoration. diff --git a/tests/_progress_stubs.py b/tests/_progress_stubs.py index d92e7405..1cdb4b4d 100644 --- a/tests/_progress_stubs.py +++ b/tests/_progress_stubs.py @@ -3,7 +3,7 @@ One copy, because these encode a contract with ``ProgressFacility``: what it reads off the widget and what it asks of the app. Four test modules had grown their own near-identical versions, and the first time the facility started -reading ``content_size`` three of them broke at once — the duplication was +reading ``content_size`` three of them broke at once: the duplication was doing no work except deferring that breakage. """ @@ -54,7 +54,7 @@ class StubTimer: """Models Textual's ``Timer`` where it matters: ``stop()`` cancels the task and drops the reference, and ``_task`` is therefore the liveness signal. - ``_active`` is deliberately not modelled — it is the PAUSE flag, and + ``_active`` is deliberately not modelled: it is the PAUSE flag, and ``stop()`` actually *sets* it, so reading it as liveness yields a check that is always True. That mistake cost a debugging round; encoding the real shape here keeps it from being repeated. @@ -77,7 +77,7 @@ class StubSearch: """The slice of SearchController the progress trackers read. Empty groups mean "unknown file", which PreviewProgressTracker treats as - structural — the superset plan, so a wrong guess costs pacing rather than + structural: the superset plan, so a wrong guess costs pacing rather than making a phase unreachable. """ @@ -94,7 +94,7 @@ def __init__(self, bar: StubBar | None = None) -> None: self.timers: list[StubTimer] = [] self.watchdogs: list[tuple[StubTimer, Any]] = [] # Kept alongside, because the watchdog's delay is no longer a - # constant — it tracks the stall cap of whichever class of work holds + # constant: it tracks the stall cap of whichever class of work holds # the line, and a watchdog under that cap makes the stall path dead. self.timer_delays: list[float] = [] diff --git a/tests/build_fixtures.py b/tests/build_fixtures.py index 28e73422..ca5dc220 100644 --- a/tests/build_fixtures.py +++ b/tests/build_fixtures.py @@ -1,7 +1,7 @@ """Build the test-fixture corpus. Run with ``uv run python -m tests.build_fixtures``. Outputs are committed so the -contract is reproducible without running this script — but running it must produce +contract is reproducible without running this script, but running it must produce byte-identical content (no timestamps in PDF metadata, no random ordering). Anchor phrases are deliberately weird so they cannot appear naturally elsewhere. @@ -23,7 +23,7 @@ # picked up now that .py is an indexed type); outputs go to tests/fixtures/. FIXTURES = Path(__file__).parent / "fixtures" -# (path, page-or-section, content) — anchor phrases must be unique across the corpus. +# (path, page-or-section, content): anchor phrases must be unique across the corpus. # The generator inserts them so the test layer knows where to find them. PDF_PAGES: dict[int, str] = { 1: "Introduction. This is the first page of the test PDF document.", @@ -102,7 +102,7 @@ def _write_pdf(path: Path, pages: dict[int, str]) -> None: # ── Invisible-text fixture (scanned-OCR mimic) ─────────────────────────────── # Each page has a visible heading plus a body drawn in render-mode 3 -# (invisible) — exactly how scanned books store their OCR layer behind a +# (invisible): exactly how scanned books store their OCR layer behind a # page image. pymupdf4llm's layout parser drops the invisible body # (coverage collapses), so this fixture exercises the InvisibleTextTier # recovery path deterministically without the real scanned corpus. @@ -190,7 +190,7 @@ def _write_headings_pdf(path: Path) -> None: ): page.insert_text((72, y), line, fontsize=11, fontname="helv", render_mode=3) y += 16 - # A visible running footer — real scanned pages carry a small + # A visible running footer: real scanned pages carry a small # visible page label, so the layout path emits something (the gate # requires production Markdown to be present before it fires). page.insert_text((72, 760), "page seven", fontsize=9, fontname="helv") @@ -282,7 +282,7 @@ def build() -> None: _write_docx(FIXTURES / "docs" / "methods.docx", DOCX_PARAGRAPHS) -# ── Anchor table — the test contract ───────────────────────────────────────── +# ── Anchor table: the test contract ───────────────────────────────────────── # Each entry is (path-relative-to-FIXTURES, kind, locator, phrase). # `locator` is a page number for PDF and a heading_path for MD. ANCHORS: list[tuple[str, str, object, str]] = [ diff --git a/tests/conftest.py b/tests/conftest.py index b615b8ea..02a0e506 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -76,7 +76,7 @@ async def _safe_press(self: Pilot, *keys: str) -> None: # type: ignore[type-arg # When the pdf-structure extra is installed in the dev venv, PDF chunks # carry body_md and the preview dispatcher routes them through the # structural Markdown widget. Most existing tests pre-date that and -# assert the flat-buffer routing PDFs have always taken — they pass +# assert the flat-buffer routing PDFs have always taken: they pass # in CI (no extra installed) but fail locally when a dev has installed # it. Default the whole test suite to flat-PDF routing so the # invariant tests stay green; the two structural-PDF tests opt out @@ -223,7 +223,7 @@ def saved_empty_scope(isolated_ui_state: Path) -> Path: """Persist a deliberately-empty scope before the app starts. A profile that has never saved a scope is seeded from - ``defaults.collection`` — all collections by default — so a test that + ``defaults.collection``, all collections by default, so a test that wants to start from "nothing selected" has to ask for it. Saving an empty state says "the user unticked everything", which is a different situation from "brand-new profile" and the one these tests mean. @@ -288,7 +288,7 @@ def isolated_pdf_structure_cache( # pyright: ignore[reportUnusedFunction] pre-structured-extra run) make later tests see wrong data. The PDF extractor caches its ExtractionCache instance in - ``_cache_singleton`` — reset it so the patched default_cache_dir + ``_cache_singleton``: reset it so the patched default_cache_dir actually takes effect.""" root = tmp_path / "pdf-structure-cache" monkeypatch.setattr("fnd.cache.default_cache_dir", lambda: root) @@ -308,7 +308,7 @@ def isolated_indexer_resume_state( # pyright: ignore[reportUnusedFunction] ``state_file_for("default")`` from the real user-data dir. A resumable state left there (an interrupted real index, or another test that drove the indexer against the real default dir) makes the - app auto-resume a "default" indexer the moment it launches — so any + app auto-resume a "default" indexer the moment it launches, so any test that drives the indexer sees a spurious extra "default" run before its own collections. Redirect the lookup so tests never read (or write) real resume state. @@ -330,7 +330,7 @@ def isolated_progress_calibration( # pyright: ignore[reportUnusedFunction] The calibrator learns from every completed operation, so without this any test that drives a preview load would write the developer's real - calibration — and read it back, making the bar's pacing (and therefore + calibration, and read it back, making the bar's pacing (and therefore these tests) depend on whatever the last interactive session measured. Reset the in-memory store on both sides: it is module-global, so a stale one leaks between tests just as readily as the file does.""" diff --git a/tests/fuzz/test_extractor_fuzz.py b/tests/fuzz/test_extractor_fuzz.py index 7b06310c..0bdf8037 100644 --- a/tests/fuzz/test_extractor_fuzz.py +++ b/tests/fuzz/test_extractor_fuzz.py @@ -7,7 +7,7 @@ If an extractor leaks a non-ExtractError exception out, the indexer ``except ExtractError`` in fnd/index.py:185/235 misses it and the -whole index build aborts on one bad file — exactly the regression +whole index build aborts on one bad file, exactly the regression M6 closed. Hypothesis generates byte blobs (random, prefix-of-magic, near-valid-OOXML) and feeds them to each extractor; the moment any input produces an uncaught exception the failure shrinks down to a @@ -162,7 +162,7 @@ def _corpus_files() -> list[Path]: @pytest.mark.parametrize("path", _corpus_files(), ids=lambda p: p.name) def test_corpus_files_never_leak_raw_exception(path: Path) -> None: """Seed corpus regression: every file under tests/fixtures/malformed/ - must either extract cleanly or raise ExtractError — never anything + must either extract cleanly or raise ExtractError: never anything else. When Hypothesis or oss-fuzz finds a new crashing input, minimise and drop it here.""" try: diff --git a/tests/test_action_label_style.py b/tests/test_action_label_style.py index 6399e91e..69abdfe2 100644 --- a/tests/test_action_label_style.py +++ b/tests/test_action_label_style.py @@ -26,7 +26,7 @@ def test_action_label_applies_the_colour() -> None: def test_action_label_is_not_dim() -> None: - """Distinct from the dim category headers — actions should read brighter.""" + """Distinct from the dim category headers: actions should read brighter.""" label = _styled_action_label("x", "#BB9AF7") assert "dim" not in str(label.style).lower() @@ -87,7 +87,7 @@ async def test_match_row_carries_the_action_colour(cfg: Config, idx: Path) -> No @pytest.mark.asyncio async def test_action_colour_equals_the_border_foreground(cfg: Config, idx: Path) -> None: - """The rows use the border's exact foreground — $primary 50% over surface. + """The rows use the border's exact foreground: $primary 50% over surface. Same foreground + same thin-stroke glyphs => same perceived colour.""" from textual.color import Color @@ -122,5 +122,5 @@ async def test_a_tag_row_is_not_action_coloured(cfg: Config, idx: Path) -> None: await pilot.pause() primary = app.get_css_variables().get("primary", "").lower() recipe = _find(tree, "recipe").label - # A plain string label (no primary styling) — tags aren't actions. + # A plain string label (no primary styling): tags aren't actions. assert primary not in str(getattr(recipe, "style", "")).lower() diff --git a/tests/test_actions_keymap.py b/tests/test_actions_keymap.py index 0d5074d0..f2e83dc6 100644 --- a/tests/test_actions_keymap.py +++ b/tests/test_actions_keymap.py @@ -52,7 +52,7 @@ def test_default_keymap_includes_every_action_with_default_key() -> None: for a in REGISTRY: if a.default_key is not None: # ``default_key`` may list several keys (comma-separated); each must - # land in the keymap (see load_keymap — one action, multiple keys). + # land in the keymap (see load_keymap: one action, multiple keys). for key in (k.strip() for k in a.default_key.split(",")): assert km.bindings.get(key) == a.id, f"action {a.id} default key {key!r} missing" @@ -127,12 +127,12 @@ async def test_open_command_palette_pushes_settings_menu(built_index: Path) -> N app.action_open_command_palette() await settings_ready(pilot, app) assert isinstance(app.screen, SettingsScreen) - # Reachable root — the menu's top level shows every section. + # Reachable root: the menu's top level shows every section. assert app.screen._breadcrumb == () # Second press closes the stack. app.action_open_command_palette() # This press CLOSES the stack, so wait for it to go rather than for a - # list to populate — the same action toggles both ways. + # list to populate: the same action toggles both ways. await wait_until( pilot, lambda: not isinstance(app.screen, SettingsScreen), @@ -181,7 +181,7 @@ async def test_root_menu_search_is_cross_section(built_index: Path) -> None: @pytest.mark.asyncio async def test_show_help_pushes_keybindings_subscreen(built_index: Path) -> None: - """`?` pushes the Keybindings sub-screen directly — one Esc returns + """`?` pushes the Keybindings sub-screen directly: one Esc returns to the main app.""" from fnd.tui.settings_screen import SettingsList, SettingsScreen diff --git a/tests/test_app_defaults_picker.py b/tests/test_app_defaults_picker.py index 01c5978b..cb089242 100644 --- a/tests/test_app_defaults_picker.py +++ b/tests/test_app_defaults_picker.py @@ -20,7 +20,7 @@ def test_picker_lists_apps_that_handle_the_kind( ) -> None: """For PDF, the picker should list every available app whose ``handles`` covers 'pdf' (skim, preview, pdf_expert, system) plus - the '(auto-resolve)' sentinel — and NOT obsidian/vscode (md only).""" + the '(auto-resolve)' sentinel, and NOT obsidian/vscode (md only).""" from fnd.tui.menu import _choices_apps_for_kind monkeypatch.setattr(apps, "_skim_app_exists", lambda: True) @@ -40,7 +40,7 @@ class FakeApp: # md-only apps must NOT appear in the PDF picker. assert "obsidian" not in ids # vscode has wildcard '*' handle so it DOES appear (it can open - # PDF as text — user choice). + # PDF as text: user choice). assert "vscode" in ids diff --git a/tests/test_app_flat_buffer_path.py b/tests/test_app_flat_buffer_path.py index b5adae56..5ce9eaec 100644 --- a/tests/test_app_flat_buffer_path.py +++ b/tests/test_app_flat_buffer_path.py @@ -4,10 +4,10 @@ * The active widget for a PDF is a LineBufferPreview, not a tree of per-line Static widgets. Steady-state DOM under the preview pane is - O(1) widgets per file regardless of file size — the whole reason for + O(1) widgets per file regardless of file size: the whole reason for the redesign. * The buffer's own scrollbar (MatchAwareScrollBar) is fed line-precise - match positions automatically on ``set_file_view`` — the host doesn't + match positions automatically on ``set_file_view``: the host doesn't call ``set_match_map`` for flat files. * Cache hits flip ``-hidden`` instead of remounting (no rebuild cost). * Switching to a markdown file falls back to the structural pipeline @@ -55,7 +55,7 @@ def _force_flat_for_pdf_tests( # pyright: ignore[reportUnusedFunction] renderer. Tests asserting the flat-buffer invariant set _FND_FORCE_FLAT=1 via this fixture. Tests asserting MD/structural paths are unaffected (the env var only forces *PDFs/TXT* to flat; - actually it forces *all* files to flat — so we scope this fixture + actually it forces *all* files to flat, so we scope this fixture to tests whose name has "pdf" in it). """ if ( @@ -74,7 +74,7 @@ def pdf_index(fixtures_dir: Path, tmp_index_dir: Path) -> Path: @pytest.fixture def multi_match_pdf_index(tmp_path: Path, tmp_index_dir: Path) -> Path: - """A multi-page PDF where ``zebra`` appears on every page — gives + """A multi-page PDF where ``zebra`` appears on every page: gives the tree multiple section hits under one file so cursor-between- sections tests have something to move between.""" import pymupdf # type: ignore[import-not-found] @@ -187,7 +187,7 @@ async def test_cursor_between_sections_calls_scroll_to_chunk_each_time( """User reported: 'Selecting different matches within a file no longer displays those matches in the preview, only the first match'. Cursor moves between section hits under the same file - must call ``scroll_to_chunk`` with each new chunk_seq — testing + must call ``scroll_to_chunk`` with each new chunk_seq: testing that directly via a spy (independent of fixture viewport size / whether the buffer is tall enough to actually scroll).""" app = FNDApp(index_dir=multi_match_pdf_index, initial_query="zebra") @@ -203,7 +203,7 @@ async def test_cursor_between_sections_calls_scroll_to_chunk_each_time( await safe_pause(pilot) buf = app._flat.active_buffer assert buf is not None - # The file should have multiple section children — bail clearly + # The file should have multiple section children: bail clearly # if the fixture didn't produce them. section_count = sum( 1 @@ -245,7 +245,7 @@ async def test_only_one_flat_buffer_is_visible_at_a_time( Root cause: LineBufferPreview had no ``.-hidden`` CSS rule, so the ``-hidden`` class ``_activate_flat_buffer`` adds didn't actually remove non-active - buffers from the layout — they stacked in the pane, each with + buffers from the layout; they stacked in the pane, each with their own scrollbar. Pin the invariant: at most ONE buffer in the pane is visible (display != none) at any time.""" app = FNDApp(index_dir=multi_match_pdf_index, initial_query="zebra") @@ -339,7 +339,7 @@ async def test_switching_md_to_pdf_hides_structural_container( @pytest.mark.asyncio async def test_flat_buffer_cache_hit_reuses_widget(pdf_index: Path) -> None: - """Re-cursoring back onto the same PDF doesn't remount — the + """Re-cursoring back onto the same PDF doesn't remount: the cached LineBufferPreview is flipped visible again.""" app = FNDApp(index_dir=pdf_index, initial_query="blue penguin sandwich") async with app.run_test() as pilot: @@ -368,7 +368,7 @@ async def test_flat_buffer_cache_hit_reuses_widget(pdf_index: Path) -> None: @pytest.mark.asyncio async def test_md_preview_keeps_structural_path(cfg: Config, md_index: Path) -> None: - """Markdown files still mount the PreviewContainer — the structural + """Markdown files still mount the PreviewContainer: the structural path is unchanged by the flat-buffer wire-in.""" app = FNDApp(index_dir=md_index, config=cfg, initial_query="susy") async with app.run_test() as pilot: diff --git a/tests/test_app_shutdown.py b/tests/test_app_shutdown.py index 2bc1e3dd..8b42e673 100644 --- a/tests/test_app_shutdown.py +++ b/tests/test_app_shutdown.py @@ -2,7 +2,7 @@ `_on_exit_app` stops the preview pipeline, cancels the prefetch drainer and stops the stall watch before handing back to Textual. Every step after a raising one is -skipped, so this is a path where an exception does not merely log — it silently +skipped, so this is a path where an exception does not merely log: it silently drops the rest of the teardown. """ @@ -21,7 +21,7 @@ async def test_exiting_while_the_drainer_runs_completes_the_teardown( tmp_index_dir: Path, ) -> None: """`await`ing a cancelled task raises CancelledError, and that is a - BaseException — so `suppress(Exception)` lets it through and it takes the + BaseException, so `suppress(Exception)` lets it through and it takes the rest of `_on_exit_app` with it, including `super()._on_exit_app()`. The drainer is idle-blocked on its queue here, which is its normal state: @@ -45,7 +45,7 @@ def stop(self) -> None: await app._on_exit_app() assert stall_stopped == [True], ( - "teardown stopped before the stall watch — the drainer's " + "teardown stopped before the stall watch: the drainer's " "CancelledError escaped and skipped the rest of the shutdown" ) assert app._prefetch.sink_drainer is None diff --git a/tests/test_apps_cross_platform.py b/tests/test_apps_cross_platform.py index 4d154605..40ca519c 100644 --- a/tests/test_apps_cross_platform.py +++ b/tests/test_apps_cross_platform.py @@ -32,7 +32,7 @@ def _capture_argv(monkeypatch: pytest.MonkeyPatch) -> list[list[str]]: # The handlers stringify the path via str(req.path), which yields backslashes -# on the Windows CI runner — assert against str(p), not a POSIX literal. +# on the Windows CI runner: assert against str(p), not a POSIX literal. _PDF = Path("/d/a.pdf") diff --git a/tests/test_apps_preview.py b/tests/test_apps_preview.py index d08d406f..f43bf2ff 100644 --- a/tests/test_apps_preview.py +++ b/tests/test_apps_preview.py @@ -1,7 +1,7 @@ """Open-PDF-in-Preview targets the correct document + page. Guards the regression where the page-jump matched the front document by -substring and sent "go to page" keystrokes to whatever was frontmost — so +substring and sent "go to page" keystrokes to whatever was frontmost, so opening a PDF behind an already-open one paged the wrong document. """ @@ -24,7 +24,7 @@ def test_preview_script_uses_exact_path_match() -> None: def test_preview_script_gates_keystrokes_on_front_document() -> None: """`matched` must be set from confirming the front document IS our target (defends against window-name collisions + open/window timing), and the - keystrokes must be gated by it — never page a doc we can't confirm front.""" + keystrokes must be gated by it: never page a doc we can't confirm front.""" s = apps._PREVIEW_PAGE_JUMP_SCRIPT assert "path of front document" in s # The front-document confirmation drives `matched`, which gates keystrokes. diff --git a/tests/test_apps_registry.py b/tests/test_apps_registry.py index 5ff5e582..8d2a51ba 100644 --- a/tests/test_apps_registry.py +++ b/tests/test_apps_registry.py @@ -8,7 +8,7 @@ the requested kind. * User-defined ``[apps.]`` tables build runnable handlers via pure template substitution; mutually-exclusive ``argv`` / ``url`` is enforced. -* No template handler ever spawns a shell — every dispatch goes through +* No template handler ever spawns a shell: every dispatch goes through ``subprocess.run`` with an argv list. """ @@ -26,8 +26,8 @@ def _capture_url_dispatch(monkeypatch: pytest.MonkeyPatch) -> list[list[str]]: """Capture ``["open", url]`` for each URL a handler hands to the launcher's open-url seam. The ``"open"`` element is a stable marker for "dispatched as - a URL" — independent of the per-OS opener command (``open`` / ``xdg-open`` / - ``os.startfile``) — so URL-construction assertions read the same on every + a URL": independent of the per-OS opener command (``open`` / ``xdg-open`` / + ``os.startfile``), so URL-construction assertions read the same on every platform in the CI matrix.""" captured: list[list[str]] = [] monkeypatch.setattr(apps.launcher, "open_url", lambda url: captured.append(["open", url]) or 0) @@ -164,7 +164,7 @@ def test_render_url_percent_encodes_path_and_query() -> None: def test_render_url_drops_empty_placeholders() -> None: - """Placeholders for empty fields render as the empty string — callers + """Placeholders for empty fields render as the empty string: callers that care about a clean URL should design templates accordingly.""" req = OpenRequest(path=Path("/tmp/x.md"), kind="md", heading_path="") url = apps._render_url("obsidian://open?vault={vault_pct}&file={path_pct}", req) @@ -197,7 +197,7 @@ def test_load_user_apps_rejects_argv_and_url_both(tmp_path: Path) -> None: cfg = { "broken": _user_app_cfg( argv=["x"], - url="custom://{path_pct}", # both set — invalid + url="custom://{path_pct}", # both set: invalid ) } with pytest.raises(ValueError, match=r"argv|url"): @@ -339,7 +339,7 @@ def test_obsidian_handler_without_vault_uses_path_form_not_system_fallback( the "Open with…" menu but no vault is configured, the handler MUST still fire Obsidian (via ``?path=``) rather than silently delegating to ``open `` (which would open the macOS default - for that file kind — often VS Code for .md — and look like a + for that file kind, often VS Code for .md, and look like a silent app-swap to the user).""" captured = _capture_url_dispatch(monkeypatch) req = OpenRequest( @@ -365,7 +365,7 @@ def test_obsidian_handler_converts_breadcrumb_to_chained_anchor( ) -> None: """``heading_path`` is a breadcrumb ("A > B > C") in the chunk; Obsidian's anchor syntax wants nested headings separated by ``#`` - ("A#B#C"). Without this rewrite, the anchor reads ``#A > B > C`` — + ("A#B#C"). Without this rewrite, the anchor reads ``#A > B > C``: Obsidian opens the file but cannot find that heading and lands at the top. Regression test for the user-reported "Obsidian opened the file but not at the location".""" @@ -381,7 +381,7 @@ def test_obsidian_handler_converts_breadcrumb_to_chained_anchor( argv = captured[0] # Chained-heading anchor (URL-encoded #): Q3%20Plans%23Mobile%20App%23Launch. assert "%23Mobile%20App%23Launch" in argv[1], argv[1] - # The raw " > " breadcrumb separator MUST NOT survive into the URL — + # The raw " > " breadcrumb separator MUST NOT survive into the URL: # spaces around ` > ` would percent-encode to %20%3E%20 and Obsidian # would fail to navigate. assert "%20%3E%20" not in argv[1], argv[1] @@ -389,14 +389,14 @@ def test_obsidian_handler_converts_breadcrumb_to_chained_anchor( def test_advanced_uri_detector_finds_plugin(tmp_path: Path) -> None: """``_advanced_uri_available`` returns True when the plugin folder - sits under any ancestor's ``.obsidian/plugins/`` — the standard + sits under any ancestor's ``.obsidian/plugins/``: the standard Obsidian vault layout.""" vault = tmp_path / "vault" (vault / ".obsidian" / "plugins" / "obsidian-advanced-uri").mkdir(parents=True) sub = vault / "Notes" / "deep" / "nested" sub.mkdir(parents=True) assert apps._advanced_uri_available(sub) is True - # Detector takes a file too — walks up to the containing dir first. + # Detector takes a file too: walks up to the containing dir first. f = sub / "note.md" f.write_text("# x") assert apps._advanced_uri_available(f) is True @@ -455,7 +455,7 @@ def test_obsidian_handler_uses_advanced_uri_when_plugin_and_line_present( assert "line=42" in argv[1] # New tab: opening a match must not hijack whatever's in the active tab. assert "openmode=tab" in argv[1], argv[1] - # Heading anchor MUST NOT be appended in advanced-uri form — line is + # Heading anchor MUST NOT be appended in advanced-uri form: line is # the only locator we need. assert "%23Findings" not in argv[1] @@ -500,7 +500,7 @@ def test_resolve_match_line_finds_first_term_occurrence(tmp_path: Path) -> None: def test_resolve_match_line_returns_from_line_on_miss(tmp_path: Path) -> None: """When no line carries a query term we fall back to the chunk - start — better than randomly skipping ahead.""" + start: better than randomly skipping ahead.""" f = tmp_path / "n.md" f.write_text("# Heading\nline two\nline three\n") assert apps._resolve_match_line(f, "absentterm", 1) == 1 @@ -525,7 +525,7 @@ def test_resolve_match_line_is_case_insensitive(tmp_path: Path) -> None: def test_resolve_match_line_is_whole_word_not_substring(tmp_path: Path) -> None: """Whole-word matching avoids matching 'template' inside - 'templating' — stays conservative on fuzzy/stem hits and lets the + 'templating': stays conservative on fuzzy/stem hits and lets the handler fall back to chunk.line on miss.""" f = tmp_path / "n.md" f.write_text("# Header\n\ntemplating engines are useful\nthe template here\n") @@ -565,7 +565,7 @@ def test_obsidian_advanced_uri_uses_resolved_match_line_not_chunk_line( ) apps.BUILTIN_APPS["obsidian"].handler(req) argv = captured[0] - # Match is on line 5 — Advanced URI URL must carry line=5, not line=1. + # Match is on line 5: Advanced URI URL must carry line=5, not line=1. assert "line=5" in argv[1], argv[1] diff --git a/tests/test_ax_permission_modal.py b/tests/test_ax_permission_modal.py index 9d9df6f1..a27162d0 100644 --- a/tests/test_ax_permission_modal.py +++ b/tests/test_ax_permission_modal.py @@ -143,7 +143,7 @@ def test_preview_handler_emits_ax_notice_when_blocked( def test_preview_handler_silent_when_no_page_locator( monkeypatch: pytest.MonkeyPatch, ) -> None: - """``page == 0`` means "open this PDF" with no position request — no + """``page == 0`` means "open this PDF" with no position request: no page-jump attempt, so no Accessibility-permission notice.""" apps._reset_ax_cache() monkeypatch.setattr(apps, "_probe_ax_trusted", lambda: False) diff --git a/tests/test_cascade_synonyms.py b/tests/test_cascade_synonyms.py index 2ec2b720..20206fd7 100644 --- a/tests/test_cascade_synonyms.py +++ b/tests/test_cascade_synonyms.py @@ -5,7 +5,7 @@ * :func:`fnd.synonyms.expand` rewrites a query string by wrapping any single-term that matches a synonym group into ``(term OR sym1 OR sym2)``. Synonyms live in a user-owned TOML file and apply at *query time* - only — the index never sees the expansion, so synonym edits don't require + only: the index never sees the expansion, so synonym edits don't require a rebuild. * :func:`fnd.cascade.cascade_search` orchestrates a sequence of widening query passes. Pass 1 is the literal query; if the result count is below @@ -58,7 +58,7 @@ def test_synonyms_unknown_term_passes_through() -> None: def test_synonyms_does_not_expand_inside_phrase() -> None: - """A term inside a quoted phrase must not be expanded — the user asked + """A term inside a quoted phrase must not be expanded: the user asked for an exact phrase, so we respect that.""" table = SynonymTable.from_groups([["MSSM", "minimal supersymmetric standard model"]]) out = expand('"the MSSM model"', table) @@ -112,7 +112,7 @@ def small_md_corpus(tmp_path: Path, tmp_index_dir: Path) -> Path: def test_cascade_returns_exact_pass_when_threshold_met(small_md_corpus: Path) -> None: """When the literal pass already returns enough hits, widening passes - must not run — every hit must be tagged ``pass_index=0``.""" + must not run: every hit must be tagged ``pass_index=0``.""" s = Searcher(index_dir=small_md_corpus) hits = cascade_search(s, query="penguin sandwich", threshold=2) assert hits, "expected literal-pass hits" @@ -139,7 +139,7 @@ def test_cascade_fuzzy_matches_when_indexed_token_is_stemmed( token for "Templates" is ``templat``. A typo query like "Templatas" lowercases to ``templatas`` (8 chars). Without stemming the query, Tantivy's ``fuzzy_term_query`` sees distance 2 from ``templat`` and - rejects the match — even though both forms come from the same root. + rejects the match, even though both forms come from the same root. Stem the query before issuing the fuzzy search so the on-disk Levenshtein distance is computed between the same token shapes the analyzer wrote. diff --git a/tests/test_chunk_pieces_match_target.py b/tests/test_chunk_pieces_match_target.py index 6d964e2b..71895edd 100644 --- a/tests/test_chunk_pieces_match_target.py +++ b/tests/test_chunk_pieces_match_target.py @@ -123,7 +123,7 @@ async def test_match_target_is_a_line_widget_not_the_header( """Contract: when a focused PDF chunk contains a match, ``scroll_to_chunk`` lands on the matched line, NOT the chunk's first line. The user-visible bug was scrolling to "page top" - instead of the actual match position — the flat buffer's + instead of the actual match position: the flat buffer's ``first_hit_line_in_chunk`` map keeps the precise target.""" app = FNDApp(index_dir=long_page_pdf_index, initial_query="zebra") async with app.run_test() as pilot: @@ -147,7 +147,7 @@ async def test_match_target_is_a_line_widget_not_the_header( ) focused_seq, hit_line = next(iter(fv.first_hit_line_in_chunk.items())) chunk_start, _ = fv.chunk_to_range[focused_seq] - # The matched line is past the chunk's first line — that's the + # The matched line is past the chunk's first line: that's the # whole point of scroll_to_chunk(prefer_first_match=True). assert hit_line >= chunk_start # And the line really does contain "zebra". @@ -158,7 +158,7 @@ async def test_match_target_is_a_line_widget_not_the_header( async def test_match_target_falls_back_to_header_when_no_match_in_chunk( long_page_pdf_index: Path, ) -> None: - """Chunks without query-term matches still need a scroll target — + """Chunks without query-term matches still need a scroll target: ``scroll_to_chunk`` falls back to the chunk's first line when ``first_hit_line_in_chunk`` has no entry for that chunk.""" app = FNDApp(index_dir=long_page_pdf_index, initial_query="zebra") @@ -178,7 +178,7 @@ async def test_match_target_falls_back_to_header_when_no_match_in_chunk( assert buf is not None fv = buf.file_view assert fv is not None - # Every chunk in chunk_to_range must have SOME scroll target — + # Every chunk in chunk_to_range must have SOME scroll target, # either a first-match line OR the chunk's first line. for seq, rng in fv.chunk_to_range.items(): assert seq in fv.first_hit_line_in_chunk or rng[0] >= 0 diff --git a/tests/test_clear_bar.py b/tests/test_clear_bar.py index 31e05c83..88a11582 100644 --- a/tests/test_clear_bar.py +++ b/tests/test_clear_bar.py @@ -1,7 +1,7 @@ """The pinned Clear bar docked at the top of the filters pane container. It floats above the scrolling tree so it stays visible whatever the tag list's -scroll — the reason a scrolling in-tree row was rejected. +scroll: the reason a scrolling in-tree row was rejected. """ from __future__ import annotations @@ -35,7 +35,7 @@ def cfg(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Config: def idx(tmp_path: Path, tmp_index_dir: Path) -> Path: root = tmp_path / "papers" root.mkdir(parents=True, exist_ok=True) - # Many tagged files so the tree scrolls — the case the pinned bar exists for. + # Many tagged files so the tree scrolls: the case the pinned bar exists for. for i in range(40): (root / f"n{i}.md").write_text( f"---\ntags: [t{i}]\n---\n\n# N{i}\n\nsaffron\n", encoding="utf-8" @@ -72,7 +72,7 @@ async def test_bar_appears_when_active(cfg: Config, idx: Path) -> None: @pytest.mark.asyncio async def test_bar_docks_at_top_inside_the_pane(cfg: Config, idx: Path) -> None: - """Inside the container's border, above the scrolling tree — so it floats + """Inside the container's border, above the scrolling tree, so it floats in view regardless of scroll.""" app = FNDApp(index_dir=idx, config=cfg) async with app.run_test(size=(100, 40)) as pilot: @@ -150,8 +150,8 @@ async def test_filters_tree_has_bounded_height_so_it_scrolls(cfg: Config, idx: P """Regression: the tree inside the container must be a bounded (1fr) height so it SCROLLS. `height: auto` grew it to full content height and it was merely clipped by the container, stranding the cursor off-screen. (Actual - scrolling can't be asserted headlessly — run_test doesn't resolve layout - heights — so this guards the exact style that broke; scroll behaviour itself + scrolling can't be asserted headlessly, run_test doesn't resolve layout + heights, so this guards the exact style that broke; scroll behaviour itself is verified in the tmux harness.)""" app = FNDApp(index_dir=idx, config=cfg) async with app.run_test() as pilot: @@ -159,13 +159,13 @@ async def test_filters_tree_has_bounded_height_so_it_scrolls(cfg: Config, idx: P tree = app.query_one("#filters_panel_tree", Tree) height = tree.styles.height assert height is not None - assert not height.is_auto, "tree height is auto — it will clip instead of scroll" + assert not height.is_auto, "tree height is auto: it will clip instead of scroll" assert height.unit.name == "FRACTION", f"expected 1fr, got {height}" @pytest.mark.asyncio async def test_bar_shows_the_active_filter_count(cfg: Config, idx: Path) -> None: - """Not a '(X)' key-hint that reads as a placeholder — the count of active + """Not a '(X)' key-hint that reads as a placeholder: the count of active filters.""" app = FNDApp(index_dir=idx, config=cfg) async with app.run_test() as pilot: diff --git a/tests/test_clear_filters.py b/tests/test_clear_filters.py index e81b54e5..326a72d3 100644 --- a/tests/test_clear_filters.py +++ b/tests/test_clear_filters.py @@ -79,7 +79,7 @@ async def test_clear_filters_resets_every_filter(cfg: Config, idx: Path) -> None @pytest.mark.asyncio async def test_clear_filters_leaves_scope_untouched(cfg: Config, idx: Path) -> None: - """Collections/sources are scope, not filters — clear must not touch them.""" + """Collections/sources are scope, not filters: clear must not touch them.""" app = FNDApp(index_dir=idx, config=cfg) async with app.run_test() as pilot: await pilot.pause() diff --git a/tests/test_cli_filter_flags.py b/tests/test_cli_filter_flags.py index f1c095f1..bdfc69ce 100644 --- a/tests/test_cli_filter_flags.py +++ b/tests/test_cli_filter_flags.py @@ -108,7 +108,7 @@ def test_kind_category_flag_expands_to_members(captured: dict[str, Any]) -> None def test_parse_filter_flags_expands_categories_for_the_tui_seed() -> None: """``parse_filter_flags`` expands category ids to fine-grained kinds in the LaunchScope, so the TUI (which seeds ``filter_kinds`` from it) emits - index-compatible ``kind:`` clauses — a raw ``kind:code`` matches nothing.""" + index-compatible ``kind:`` clauses; a raw ``kind:code`` matches nothing.""" from fnd.cli import parse_filter_flags from fnd.kinds import KINDS_IN_CATEGORY diff --git a/tests/test_cli_reindex_scoping.py b/tests/test_cli_reindex_scoping.py index 77b12980..5eba8860 100644 --- a/tests/test_cli_reindex_scoping.py +++ b/tests/test_cli_reindex_scoping.py @@ -3,7 +3,7 @@ ``-c all`` means every collection here exactly as it does for a search, so one token says "everything" across the whole CLI. Naming nothing stays an error: re-indexing every collection is a minutes-long rebuild, so it is asked for -rather than defaulted into — but the error proposes ``-c all`` through the same +rather than defaulted into, but the error proposes ``-c all`` through the same report-and-offer path a typo goes through, instead of Typer's bare "Missing argument 'NAME'". """ @@ -30,7 +30,7 @@ @pytest.fixture def cli(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> tuple[CliRunner, list[str]]: """A runner over a two-collection config, plus the list of collections each - reindex actually touched (the index build itself is stubbed — this is about + reindex actually touched (the index build itself is stubbed: this is about scope resolution, not indexing).""" for name in ("a", "b"): (tmp_path / name).mkdir() diff --git a/tests/test_cli_routing.py b/tests/test_cli_routing.py index bf3b0d4e..d8956841 100644 --- a/tests/test_cli_routing.py +++ b/tests/test_cli_routing.py @@ -97,7 +97,7 @@ def test_multiword_unquoted_query_joins_with_space( self, monkeypatch: pytest.MonkeyPatch ) -> None: # `fnd buffer overflow` (no quotes) collects both positionals and - # joins them — matches the natural English shape without forcing + # joins them: matches the natural English shape without forcing # the user to quote. mock_app = MagicMock() monkeypatch.setattr("fnd.tui.FNDApp", mock_app) diff --git a/tests/test_cli_unknown_filter_values.py b/tests/test_cli_unknown_filter_values.py index a4eb2a8f..d9d778ac 100644 --- a/tests/test_cli_unknown_filter_values.py +++ b/tests/test_cli_unknown_filter_values.py @@ -23,7 +23,7 @@ runner = CliRunner() -# Kwargs each stubbed FNDApp construction was called with — empty means the +# Kwargs each stubbed FNDApp construction was called with: empty means the # TUI never got as far as being built. _built: list[dict[str, object]] = [] @@ -31,7 +31,7 @@ @pytest.fixture def corpus(tmp_path: Path, tmp_index_dir: Path, monkeypatch: pytest.MonkeyPatch) -> Path: """Two real collections: ``Work2`` (mixed case, the reported bug) and - ``papers`` — indexed, with a config TOML the CLI actually loads.""" + ``papers``: indexed, with a config TOML the CLI actually loads.""" for name in ("Work2", "papers"): root = tmp_path / name root.mkdir() @@ -63,7 +63,7 @@ def corpus(tmp_path: Path, tmp_index_dir: Path, monkeypatch: pytest.MonkeyPatch) @pytest.fixture def tty(monkeypatch: pytest.MonkeyPatch) -> None: - """Force the interactive branch — same escape hatch fnd.migrate uses.""" + """Force the interactive branch: same escape hatch fnd.migrate uses.""" monkeypatch.setenv("_FND_FORCE_TTY", "1") @@ -95,7 +95,7 @@ def test_wrong_case_collection_is_offered_and_accepted(corpus: Path, tty: None) assert code == 0, out assert "no collection named 'work2'" in out assert "Did you mean 'Work2'?" in out - # Accepting scopes the search — the papers copy must not appear. + # Accepting scopes the search: the papers copy must not appear. assert "Work2" in out assert "papers" not in out @@ -108,7 +108,7 @@ def test_declining_the_suggestion_exits_without_searching(corpus: Path, tty: Non def test_non_tty_reports_instead_of_prompting(corpus: Path) -> None: - """Piped or scripted, the prompt would hang — print the fix instead.""" + """Piped or scripted, the prompt would hang: print the fix instead.""" code, out = _search("lightning", "-c", "work2") assert code == 2, out assert "no collection named 'work2'" in out @@ -133,14 +133,14 @@ def test_unknown_kind_is_rejected_with_a_suggestion(corpus: Path) -> None: def test_kind_case_is_canonicalised_silently(corpus: Path) -> None: - """``--kind PDF`` is unambiguous — resolve it, don't interrogate the user.""" + """``--kind PDF`` is unambiguous: resolve it, don't interrogate the user.""" code, out = _search("lightning", "--kind", "PDF") assert code == 0, out assert "did you mean" not in out.lower() def test_in_query_collection_token_reports_but_never_prompts(corpus: Path, tty: None) -> None: - """A ``c:`` token inside the query is reported, not rewritten — silently + """A ``c:`` token inside the query is reported, not rewritten, silently editing text the user typed is worse than telling them what to change.""" code, out = _search("c:work2 lightning", input="y\n") assert code == 2, out @@ -197,7 +197,7 @@ def test_bad_date_and_bad_tag_match_report_together(corpus: Path) -> None: @pytest.mark.parametrize("blank", ["", " ", ","]) def test_blank_collection_value_is_an_error_not_everything(corpus: Path, blank: str) -> None: """``-c "$COLL"`` with ``COLL`` unset is the scripted version of the bug - this PR fixes — it must not resolve to "search everything".""" + this PR fixes: it must not resolve to "search everything".""" code, out = _search("lightning", "-c", blank) assert code == 2, out assert "no collection named" in out diff --git a/tests/test_clone_source.py b/tests/test_clone_source.py index 514eb939..65eec852 100644 --- a/tests/test_clone_source.py +++ b/tests/test_clone_source.py @@ -22,7 +22,7 @@ def _setup_two_collections(tmp_path: Path) -> Path: - """Two collections — `src_coll` has one fully-populated source, + """Two collections: `src_coll` has one fully-populated source, `dst_coll` is empty.""" cfg_path = tmp_path / "config.toml" src = SourceConfig( @@ -63,7 +63,7 @@ def test_clone_appends_to_target_collection(tmp_path: Path) -> None: source_index=0, target_collection="dst_coll", ) - # Returned index is 0-based — the new entry's position in the + # Returned index is 0-based: the new entry's position in the # target collection's sources list. assert new_idx == 0 cfg = load(cfg_path) diff --git a/tests/test_cloud_files.py b/tests/test_cloud_files.py index 9326b51a..1f7ab775 100644 --- a/tests/test_cloud_files.py +++ b/tests/test_cloud_files.py @@ -55,7 +55,7 @@ def test_detection_is_independent_of_location( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """An evicted file under ~/Documents is as dataless as one in the - iCloud Drive folder — Desktop & Documents sync keeps ordinary paths.""" + iCloud Drive folder: Desktop & Documents sync keeps ordinary paths.""" monkeypatch.setattr("fnd.cloud_files.sys.platform", "darwin") monkeypatch.setattr("fnd.cloud_files._stat", _fake_stat(st_flags=0x40000000)) for where in ( @@ -84,7 +84,7 @@ def test_windows_placeholder_attributes( def test_linux_reports_unknown(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - """No common marker on Linux — say so rather than guessing, and treat + """No common marker on Linux: say so rather than guessing, and treat it as local so nothing is skipped for a condition we can't detect.""" monkeypatch.setattr("fnd.cloud_files.sys.platform", "linux") monkeypatch.setattr("fnd.cloud_files._stat", _fake_stat()) diff --git a/tests/test_collapsed_pane_nav.py b/tests/test_collapsed_pane_nav.py index a08889f7..d63f8e8f 100644 --- a/tests/test_collapsed_pane_nav.py +++ b/tests/test_collapsed_pane_nav.py @@ -1,7 +1,7 @@ """While a sidebar panel is collapsed-to-header, Up/Down navigate *between* panels (up = previous, down = next, wrapping) rather than moving a cursor no one can see. The filters tree lives inside the ``#filters_pane`` wrapper, so it -is not a direct child ``Tree`` of the column — the navigation must still reach +is not a direct child ``Tree`` of the column: the navigation must still reach it, both to depart from and to arrive at. Regression: the wrapper hid the filters tree from the pane list, so Up/Down stopped working there. """ @@ -32,8 +32,8 @@ async def _collapse_all(app: FNDApp) -> None: async def test_pane_collapse_reflows_synchronously(built_index: Path) -> None: """Collapsing a panel reallocates the sidebar heights in the SAME frame, not after the next refresh. A deferred reflow left the panel drawn at its - old (taller) inline height for one frame — a container with side walls but - no bottom edge — before snapping to the header box (the flicker). + old (taller) inline height for one frame (a container with side walls but + no bottom edge) before snapping to the header box (the flicker). """ app = FNDApp(index_dir=built_index, initial_query="blue penguin sandwich") async with app.run_test() as pilot: diff --git a/tests/test_collection_name_spaces.py b/tests/test_collection_name_spaces.py index a39863e4..5ceeaa34 100644 --- a/tests/test_collection_name_spaces.py +++ b/tests/test_collection_name_spaces.py @@ -33,7 +33,7 @@ def test_spaced_name_round_trips_through_toml( write_collection_source(config_path=cfg, collection_name=name, source=src) written = cfg.read_text(encoding="utf-8") - # tomlkit auto-quotes the key because of the spaces — the unquoted + # tomlkit auto-quotes the key because of the spaces: the unquoted # form would be a TOML parse error. assert f'[[collections."{name}".sources]]' in written diff --git a/tests/test_collection_name_validation.py b/tests/test_collection_name_validation.py index 1b92541a..2efb2148 100644 --- a/tests/test_collection_name_validation.py +++ b/tests/test_collection_name_validation.py @@ -5,10 +5,10 @@ collection "Book Club". The forbidden set is narrowed to characters that would actually break a downstream consumer: -- ``/`` and ``\\`` — collide with the per-collection state file path -- quote characters and backticks — TOML / shell / DSL quoting conflict -- ``,`` — the ``c:,`` DSL list separator -- control characters and DEL — never useful +- ``/`` and ``\\``: collide with the per-collection state file path +- quote characters and backticks (TOML / shell / DSL quoting conflict +- ``,``) the ``c:,`` DSL list separator +- control characters and DEL, never useful The first character must still be an ASCII alnum or underscore so the name can't start with a hyphen (CLI flag collision), a dot (hidden file @@ -34,7 +34,7 @@ def test_windows_reserved_device_names_rejected_on_win32( name: str, monkeypatch: pytest.MonkeyPatch ) -> None: """A collection named after a Windows device (CON, PRN, …) can't own a - ``.state.toml`` there — reject it on Windows (case-insensitive, and + ``.state.toml`` there: reject it on Windows (case-insensitive, and for the stem before the first dot).""" monkeypatch.setattr("sys.platform", "win32") with pytest.raises(InvalidCollectionNameError, match="reserved device name"): diff --git a/tests/test_collection_rebuild_action.py b/tests/test_collection_rebuild_action.py index 84b8c85e..c1cad16b 100644 --- a/tests/test_collection_rebuild_action.py +++ b/tests/test_collection_rebuild_action.py @@ -1,10 +1,10 @@ """The per-collection 'Rebuild index' action must reach the indexer as a -true rebuild — drop chunks (rebuild) + bypass durable cache (force_fresh) +true rebuild: drop chunks (rebuild) + bypass durable cache (force_fresh) + revisit every file (skip_unchanged off) + texturise on. Regression for a Rebuild that silently ran a plain incremental update. Drives the real menu action callback against the real app, capturing the -exact kwargs start_indexer receives — the layer where the bug would hide. +exact kwargs start_indexer receives: the layer where the bug would hide. The action asks first, because it empties the collection before refilling it, so the test confirms as a user would. That also proves the diff --git a/tests/test_collection_tristate_toggle.py b/tests/test_collection_tristate_toggle.py index 06a19241..563c0f67 100644 --- a/tests/test_collection_tristate_toggle.py +++ b/tests/test_collection_tristate_toggle.py @@ -63,7 +63,7 @@ async def test_toggling_collection_on_marks_all_sources( await pilot.pause() await pilot.press("enter") await pilot.pause() - # After toggle the whole collection is FULL — it scopes via the + # After toggle the whole collection is FULL: it scopes via the # collection filter, so it appears in ``collections`` (not by # enumerating its source ids into ``active_sources``). assert "TWO" in app._scope.collections, f"expected TWO full, got {app._scope.collections}" @@ -165,7 +165,7 @@ async def test_cli_collection_shows_full_marker( built_index: Path, multi_source_config: Config, isolated_ui_state: Path ) -> None: """Launching with ``--collection TWO`` should display TWO as - fully active (●) — every source toggled on — not as empty (○).""" + fully active (●), every source toggled on, not as empty (○).""" app = FNDApp(index_dir=built_index, collection="TWO", config=multi_source_config) async with app.run_test() as pilot: await pilot.pause() @@ -190,8 +190,8 @@ async def test_cli_collection_shows_full_marker( async def test_legacy_scope_only_collections_renders_as_full( built_index: Path, multi_source_config: Config, isolated_ui_state: Path ) -> None: - """A saved state from before the tri-state rework — where - ``_collections=["TWO"]`` but ``_active_sources=[]`` — should + """A saved state from before the tri-state rework, where + ``_collections=["TWO"]`` but ``_active_sources=[]``, should *render* as fully active (●) on every row, even though ``_active_sources`` stays empty in memory (we don't auto-expand it because the config / index might disagree on source paths).""" @@ -251,7 +251,7 @@ async def test_collection_off_keeps_shared_source_of_active_sibling( kept its ● marker, so Home searches silently lost every md file. Both collections FULL scope via the collection filter, so survival - is observable as BBB staying ● (FULL) after AAA toggles off — its + is observable as BBB staying ● (FULL) after AAA toggles off: its shared vault is still in scope through BBB's collection channel.""" app = FNDApp(index_dir=built_index, config=shared_source_config) async with app.run_test() as pilot: @@ -330,7 +330,7 @@ async def test_collection_off_keeps_shared_source_of_partial_sibling( saved_empty_scope: Path, ) -> None: """#63: AAA full + BBB *partial* (only the shared vault on). Toggling - AAA off must not strip the vault — BBB still claims it, so BBB stays + AAA off must not strip the vault: BBB still claims it, so BBB stays ◐ and the vault row under BBB stays ●. The flat ``active_sources`` list had no record of BBB's partial claim, so it was pruned.""" app = FNDApp(index_dir=built_index, config=shared_source_config) @@ -361,7 +361,7 @@ async def test_collection_off_keeps_shared_source_of_partial_sibling( await pilot.press("enter") await pilot.pause() assert "AAA" in app._scope.collections - # Toggle AAA off — the shared vault must survive (BBB still claims it). + # Toggle AAA off: the shared vault must survive (BBB still claims it). ctree.cursor_line = 0 await pilot.pause() await pilot.press("enter") @@ -384,7 +384,7 @@ async def test_panel_title_source_count_agrees_with_markers( """#58: the toggle-time title refresh must count sources by the same rule the row markers use (``collection_full or id in active_sources``). A CLI ``--collection`` full collection paints every row ● but, with - the old rule, the toggle-path title reported 0 sources — disagreeing + the old rule, the toggle-path title reported 0 sources: disagreeing with the full-rebuild title.""" app = FNDApp(index_dir=built_index, collection="TWO", config=multi_source_config) async with app.run_test() as pilot: @@ -408,7 +408,7 @@ async def test_panel_title_source_count_agrees_with_markers( async def test_toggle_collection_off_from_cli_state( built_index: Path, multi_source_config: Config, isolated_ui_state: Path ) -> None: - """Bug B v2 — pressing Enter on a CLI-flag collection (rendered ● + """Bug B v2: pressing Enter on a CLI-flag collection (rendered ● with empty ``_active_sources``) must turn it off, not flip into the "fully on with sources" state silently.""" app = FNDApp(index_dir=built_index, collection="TWO", config=multi_source_config) diff --git a/tests/test_collections.py b/tests/test_collections.py index 50702ed1..280f5533 100644 --- a/tests/test_collections.py +++ b/tests/test_collections.py @@ -19,7 +19,7 @@ def _make_md(p: Path, content: str) -> None: def _posix(path: str) -> str: """Stored path in forward-slash form so ``/``-based suffix/substring - assertions are separator-agnostic — the index stores native paths, which + assertions are separator-agnostic: the index stores native paths, which use backslashes on Windows.""" return Path(path).as_posix() @@ -32,13 +32,13 @@ def shaped_corpus(tmp_path: Path) -> Path: root/ keep/ - wanted.md — body: "phrase keepable" + wanted.md, body: "phrase keepable" drafts/ - skip-me.md — body: "phrase keepable" (must be excluded) + skip-me.md, body: "phrase keepable" (must be excluded) archive/ - old.md — body: "phrase keepable" (must be excluded) + old.md, body: "phrase keepable" (must be excluded) .hidden/ - hidden.md — body: "phrase keepable" (must be excluded) + hidden.md, body: "phrase keepable" (must be excluded) """ root = tmp_path / "root" _make_md(root / "keep" / "wanted.md", "# Wanted\nphrase keepable here.\n") diff --git a/tests/test_copy_query_command_tui.py b/tests/test_copy_query_command_tui.py index 0d1b8cba..7a56901d 100644 --- a/tests/test_copy_query_command_tui.py +++ b/tests/test_copy_query_command_tui.py @@ -2,7 +2,7 @@ Covers both halves: the ``tui`` launch flags hydrate the scope (relaunch), and the ``copy_query_command`` action serialises live scope back out (copy). -The two are inverses — ``test_seed_then_snapshot_round_trips`` asserts it. +The two are inverses: ``test_seed_then_snapshot_round_trips`` asserts it. """ from __future__ import annotations @@ -160,7 +160,7 @@ async def test_nothing_to_copy_skips_clipboard( @pytest.mark.asyncio async def test_tag_fanned_across_sources_counts_once(cfg: Config, home_index: Path) -> None: """A source-agnostic `--tag` seeds into every source, but the search treats - it as one OR-ed term — the active-filter count must not double it. Reopening + it as one OR-ed term: the active-filter count must not double it. Reopening `fnd Tree -c Work --tag strategy-pattern` used to report 2 tags active.""" app = FNDApp(index_dir=home_index, config=cfg, collection="home") async with app.run_test() as pilot: diff --git a/tests/test_cursor_on_expand.py b/tests/test_cursor_on_expand.py index bfd1fe28..13d516f2 100644 --- a/tests/test_cursor_on_expand.py +++ b/tests/test_cursor_on_expand.py @@ -4,13 +4,13 @@ presses Right on a collapsed file row), the cursor must drop onto the first hit *child* of that file. Leaving it on the parent file row forces a redundant Down keypress before navigation actually advances -to a fresh match — the preview is already showing the file's first +to a fresh match: the preview is already showing the file's first hit, so re-selecting it via the parent contributes nothing. ``node.expand()`` only invalidates the tree's line cache; the new children keep a stale ``_line`` of -1 until a rebuild. Moving via ``move_cursor(child)`` then reads -1 and the skip-expanded-parents -validator parks the cursor on the first file — visible when the rebuild +validator parks the cursor on the first file: visible when the rebuild lags (preview pane mid full-mount). The move goes through ``move_cursor_to_line(node.line + 1)``, which forces the rebuild. """ @@ -38,7 +38,7 @@ def built_index(fixtures_dir: Path, tmp_index_dir: Path) -> Path: class _ResultsTreeApp(App[None]): """Bare host for a ``ResultsTree`` so the stale-line race can be - reproduced deterministically with several file rows — the real + reproduced deterministically with several file rows: the real corpus yields too few groups to tell a wrong "first file" jump apart from a correct "first hit" landing.""" @@ -65,7 +65,7 @@ async def test_naive_move_cursor_with_stale_line_jumps_to_first_file() -> None: """Characterizes the root cause: ``move_cursor(child)`` while the child's line index is stale (-1) sets ``cursor_line`` to -1, which the skip-expanded-parents validator clamps to 0 and parks on the first - file — the bug the production helper must avoid.""" + file: the bug the production helper must avoid.""" app = _ResultsTreeApp() async with app.run_test() as pilot: tree = app.query_one("#t", ResultsTree) diff --git a/tests/test_default_collection_all.py b/tests/test_default_collection_all.py index 63a7927b..3ec034fb 100644 --- a/tests/test_default_collection_all.py +++ b/tests/test_default_collection_all.py @@ -2,8 +2,8 @@ A fresh profile should search everything rather than a collection named ``default`` that most users never create. The setting seeds scope only when -nothing has been remembered — once the sidebar has saved a selection, that -wins — so ``-c all`` exists as the quick per-launch override. +nothing has been remembered (once the sidebar has saved a selection, that +wins) so ``-c all`` exists as the quick per-launch override. """ from __future__ import annotations @@ -39,7 +39,7 @@ def test_other_values_are_not_the_pseudo_name(value: str | None) -> None: def test_a_real_collection_named_all_wins() -> None: - """Configs written before the name was reserved keep working — their + """Configs written before the name was reserved keep working: their own collection resolves, rather than silently widening to everything.""" assert is_all_collections("all", known={"all", "papers"}) is False diff --git a/tests/test_delete_source.py b/tests/test_delete_source.py index ac8417ba..a2588ec2 100644 --- a/tests/test_delete_source.py +++ b/tests/test_delete_source.py @@ -2,7 +2,7 @@ The actual TUI flow (Ctrl+D → DeleteSourceScreen modal → confirm) needs Pilot for the keystroke path; this file pins the persistence -side — invoking the same write_collection round-trip the modal does +side: invoking the same write_collection round-trip the modal does and asserting the source is gone. """ diff --git a/tests/test_delete_source_e2e.py b/tests/test_delete_source_e2e.py index f6a3896f..3a6f9914 100644 --- a/tests/test_delete_source_e2e.py +++ b/tests/test_delete_source_e2e.py @@ -76,7 +76,7 @@ async def test_delete_modal_confirm_removes_source_and_lands_above( monkeypatch: pytest.MonkeyPatch, ) -> None: """After confirming, both the DeleteSourceScreen and the now-stale - SourceFormScreen pop — the user lands on the screen they were on + SourceFormScreen pop: the user lands on the screen they were on before opening the form (not the main app).""" cfg_path = tmp_path / "config.toml" monkeypatch.setattr("fnd.config.default_config_path", lambda: cfg_path) @@ -106,7 +106,7 @@ async def test_delete_modal_confirm_removes_source_and_lands_above( async with app.run_test() as pilot: await pilot.pause() app._config = load() - sentinel = app.screen # main screen — pop target after delete + sentinel = app.screen # main screen: pop target after delete app.push_screen(SourceFormScreen(collection_name="probe", source_index=1)) await pilot.pause() await pilot.press("ctrl+d") diff --git a/tests/test_detail_strip_advisory.py b/tests/test_detail_strip_advisory.py index f6515eeb..f716d538 100644 --- a/tests/test_detail_strip_advisory.py +++ b/tests/test_detail_strip_advisory.py @@ -2,7 +2,7 @@ visible border so it reads as a distinct guidance panel. The strip is the only place we proactively surface "install plugin X" -/ "no page-jump on macOS" / etc. — without the advisory routed +/ "no page-jump on macOS" / etc. without the advisory routed through, users have to discover capability gaps by trial and error. """ @@ -53,7 +53,7 @@ def test_obsidian_app_notes_mention_advanced_uri_plugin() -> None: def test_app_defaults_picker_obsidian_choice_carries_plugin_advisory() -> None: """``_choices_apps_for_kind`` already routes ``app.notes`` into the picker. Pin that the obsidian choice's description names the - plugin — without this nudge users don't know why o-on-md sometimes + plugin, without this nudge users don't know why o-on-md sometimes lands at the section heading instead of the matched line.""" from fnd.tui.menu import _choices_apps_for_kind @@ -99,7 +99,7 @@ async def test_vault_field_hint_mentions_advanced_uri_plugin( tmp_path: Path, ) -> None: """SourceFormScreen's Obsidian vault row's hint is the user's - second chance to learn about the plugin — first sighting is at + second chance to learn about the plugin: first sighting is at App picker, second at the vault field itself.""" from fnd.config import CollectionConfig, Config, SourceConfig from fnd.tui.settings_screen import SourceFormScreen diff --git a/tests/test_escape_cascade.py b/tests/test_escape_cascade.py index 77186974..581127ab 100644 --- a/tests/test_escape_cascade.py +++ b/tests/test_escape_cascade.py @@ -1,4 +1,4 @@ -"""Esc cascade — from any non-results pane Esc focuses the results tree. +"""Esc cascade: from any non-results pane Esc focuses the results tree. The Settings menu lives on the screen stack with its own Esc handler; this test exercises only the main-app cascade. Esc inside the menu is diff --git a/tests/test_explain_trace.py b/tests/test_explain_trace.py index d1d24e13..a7970193 100644 --- a/tests/test_explain_trace.py +++ b/tests/test_explain_trace.py @@ -32,7 +32,7 @@ def cfg(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Config: @pytest.fixture def medium_index(tmp_path: Path, tmp_index_dir: Path) -> Path: - """8 docs that all mention "quick brown fox" — fusion's the right + """8 docs that all mention "quick brown fox": fusion's the right regime here (lots of candidates, no clear winner).""" a = tmp_path / "notes" for i in range(8): @@ -113,7 +113,7 @@ def test_cascade_trace_records_pass_widening( with hits. Uses ``glimmer`` / ``glimer`` (both pass through the index analyser unchanged - so the on-disk Levenshtein distance is exactly 1) — same canonical + so the on-disk Levenshtein distance is exactly 1): same canonical fixture as ``test_ux_j_cascade_fallback.py``.""" from fnd.cascade import cascade_search from fnd.query import Searcher diff --git a/tests/test_extract_pool_fd_recovery.py b/tests/test_extract_pool_fd_recovery.py index 7e9d7c94..eaeedd93 100644 --- a/tests/test_extract_pool_fd_recovery.py +++ b/tests/test_extract_pool_fd_recovery.py @@ -7,7 +7,7 @@ ``manager.Queue()`` call allocated a Unix socket FD on the parent that lingered until GC. After hundreds of PDFs the FD set passed to ``_posixsubprocess.fork_exec`` exceeded a threshold and the next -worker spawn was rejected — leaving every subsequent PDF in the +worker spawn was rejected, leaving every subsequent PDF in the indexing run failing with ExtractError("ValueError: bad value(s) in fds_to_keep"). In the chained-extraction bug this produced the exact symptom "chain completes, 29 misses, 0 docs landed". diff --git a/tests/test_extract_safety.py b/tests/test_extract_safety.py index f4e51c3f..312f02f3 100644 --- a/tests/test_extract_safety.py +++ b/tests/test_extract_safety.py @@ -42,7 +42,7 @@ def test_zip_bomb_total_size_rejected(tmp_path: Path) -> None: def test_zip_bomb_ratio_rejected(tmp_path: Path) -> None: bomb = tmp_path / "ratio.docx" with zipfile.ZipFile(bomb, "w", zipfile.ZIP_DEFLATED) as zf: - # A 16 KB run of repeating bytes compresses to a few bytes — + # A 16 KB run of repeating bytes compresses to a few bytes: # natural ratio well over 200×. zf.writestr("bomb.xml", b"A" * 16384) with pytest.raises(ExtractError, match="ratio"): @@ -58,8 +58,8 @@ def test_not_a_zip_is_rejected(tmp_path: Path) -> None: def test_docx_parser_crash_becomes_extract_error(tmp_path: Path) -> None: """A ZIP that passes the bomb precheck but isn't actually a valid - OOXML package raises ``PackageNotFoundError`` inside ``python-docx`` - — we convert it to ExtractError.""" + OOXML package raises ``PackageNotFoundError`` inside ``python-docx``: + we convert it to ExtractError.""" f = tmp_path / "fake.docx" with zipfile.ZipFile(f, "w") as zf: zf.writestr("not-the-right-parts.xml", b"") @@ -199,7 +199,7 @@ def flaky_extract(path: Path) -> Iterator[Chunk]: build_index(roots=[tmp_path], index_dir=index_dir, collection="default") searcher = Searcher(index_dir=index_dir) - # The poison file's body never wins a hit — its (partial) chunks + # The poison file's body never wins a hit: its (partial) chunks # should have been cleaned up in the except branch. assert searcher.search("partial-chunk-1") == [] assert searcher.search("partial-chunk-2") == [] diff --git a/tests/test_extraction_cache.py b/tests/test_extraction_cache.py index 18d7969f..6a3ea382 100644 --- a/tests/test_extraction_cache.py +++ b/tests/test_extraction_cache.py @@ -114,7 +114,7 @@ def test_put_atomic_no_partial_file_on_failure( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """F13: a failed write must not leave the entry path partially populated. - Either the previous entry survives, or nothing exists — never a corrupt half-write.""" + Either the previous entry survives, or nothing exists: never a corrupt half-write.""" cache = ExtractionCache(root=tmp_path) key = "atomic--v1" @@ -181,7 +181,7 @@ def test_entry_path_sanitisation_is_collision_free(monkeypatch: pytest.MonkeyPat def test_pipe_bearing_key_round_trips(tmp_path: Path) -> None: - """put()/get() of a legacy ``|``-bearing key works on the host OS — + """put()/get() of a legacy ``|``-bearing key works on the host OS: on Windows only because entry_path sanitises the filename.""" cache = ExtractionCache(root=tmp_path) key = "deadbeef--pymupdf4llm-1.0|cfg-abc" @@ -248,7 +248,7 @@ def test_sha256_file_is_stable(tmp_path: Path) -> None: def test_cache_overlay_per_file_identity_on_restore(tmp_path: Path) -> None: """Regression: two files with identical bytes at DIFFERENT paths share a cache entry (keyed by content hash), but the restored - chunks must reflect the current file's parent_id / path / mtime — + chunks must reflect the current file's parent_id / path / mtime, otherwise the indexer routes chunks to the wrong Tantivy parent_id and the search shows only one of the two files. @@ -273,7 +273,7 @@ def test_cache_overlay_per_file_identity_on_restore(tmp_path: Path) -> None: # First extraction populates the cache. chunks_a = list(extract(a)) - # Second extraction is a cache hit — must overlay parent_id. + # Second extraction is a cache hit: must overlay parent_id. chunks_b = list(extract(b)) assert chunks_a, "extract() must yield chunks" diff --git a/tests/test_extractor_docx_emits_markdown.py b/tests/test_extractor_docx_emits_markdown.py index 663761b0..9930865b 100644 --- a/tests/test_extractor_docx_emits_markdown.py +++ b/tests/test_extractor_docx_emits_markdown.py @@ -20,7 +20,7 @@ @pytest.fixture def docx_path(tmp_path: Path) -> Path: """Build a docx fixture covering bold/italic runs, bulleted list, - numbered list, and a 2x2 table — all under one Heading 1.""" + numbered list, and a 2x2 table: all under one Heading 1.""" doc = Document() doc.add_heading("Top Section", level=1) p = doc.add_paragraph() @@ -73,7 +73,7 @@ def test_body_md_starts_with_heading(docx_path: Path) -> None: def test_body_struct_remains_plain_text(docx_path: Path) -> None: - """Snippet pipeline reads body_struct — markdown markers in body_md + """Snippet pipeline reads body_struct: markdown markers in body_md must not leak into block.text.""" chunk = next(extract(docx_path)) for block in chunk.body_struct: @@ -82,7 +82,7 @@ def test_body_struct_remains_plain_text(docx_path: Path) -> None: def test_table_cells_are_indexed(docx_path: Path) -> None: - """Cell text should be searchable — landed in chunk.body so F_BODY + """Cell text should be searchable: landed in chunk.body so F_BODY sees it.""" chunk = next(extract(docx_path)) assert "beanstalk" in chunk.body, chunk.body diff --git a/tests/test_extractor_md_preserves_source.py b/tests/test_extractor_md_preserves_source.py index a47d70e4..91dd53c8 100644 --- a/tests/test_extractor_md_preserves_source.py +++ b/tests/test_extractor_md_preserves_source.py @@ -2,7 +2,7 @@ Pre-fix the extractor only kept ``h1..h6`` and ``p`` blocks; tables, nested lists, blockquotes, and fenced code blocks were either flattened -to ``p`` or — in the case of code-only sections — silently dropped from +to ``p`` or, in the case of code-only sections, silently dropped from the index. ``body_md`` carries the verbatim section source so the preview renderer can pass it to the Textual Markdown widget without any lossy reconstruction. @@ -23,8 +23,8 @@ def _write(tmp_path: Path, body: str) -> Path: def test_body_md_round_trips_table_source(tmp_path: Path) -> None: - """A markdown table survives extraction verbatim — same pipes, - same alignment row — so the preview renderer can hand it straight + """A markdown table survives extraction verbatim (same pipes, + same alignment row) so the preview renderer can hand it straight to Textual's Markdown widget.""" body = textwrap.dedent( """\ @@ -125,8 +125,8 @@ def test_blockquote_round_trips(tmp_path: Path) -> None: def test_heading_path_unchanged(tmp_path: Path) -> None: - """``heading_path`` is built from the heading stack inline text — - not from the new line-map slice — so adding ``body_md`` mustn't + """``heading_path`` is built from the heading stack inline text, + not from the new line-map slice, so adding ``body_md`` mustn't change sidebar locator labels.""" body = textwrap.dedent( """\ @@ -149,7 +149,7 @@ def test_heading_path_unchanged(tmp_path: Path) -> None: def test_body_struct_remains_plain_text_for_snippets(tmp_path: Path) -> None: - """``body_struct`` Block list stays as plain-text blocks — snippets + """``body_struct`` Block list stays as plain-text blocks: snippets must not show literal markdown markers like ``**bold**``.""" body = textwrap.dedent( """\ @@ -162,7 +162,7 @@ def test_body_struct_remains_plain_text_for_snippets(tmp_path: Path) -> None: assert len(chunks) == 1 block_texts = [b.text for b in chunks[0].body_struct] # Inline tokens preserve markdown markers in their content (markdown-it - # behaviour) — that's fine because snippets show what the user typed. + # behaviour): that's fine because snippets show what the user typed. # The key invariant is that body_struct is *not* the preview renderer's # source; body_md is. Either presentation reads as expected English. assert any("bold" in t for t in block_texts) diff --git a/tests/test_extractor_pdf_heading_survives_cache.py b/tests/test_extractor_pdf_heading_survives_cache.py index a96e875d..f84f9965 100644 --- a/tests/test_extractor_pdf_heading_survives_cache.py +++ b/tests/test_extractor_pdf_heading_survives_cache.py @@ -2,7 +2,7 @@ The PDF texture cache stores whole chunks (including ``body``) keyed by content hash, and durably reuses prior entries across signature bumps. -So the heading-into-body fix must apply when serving cached chunks too — +So the heading-into-body fix must apply when serving cached chunks too, otherwise a plain ``reindex --rebuild`` would keep serving the old un-folded body and the fix would require a full re-texture to land. """ diff --git a/tests/test_extractor_pdf_indexes_heading.py b/tests/test_extractor_pdf_indexes_heading.py index e4228144..29806377 100644 --- a/tests/test_extractor_pdf_indexes_heading.py +++ b/tests/test_extractor_pdf_indexes_heading.py @@ -4,7 +4,7 @@ from the embedded TOC, or from font-size clustering) went to ``heading_path`` but never into ``body``. Since search gates visibility on ``body``, a TOC section heading that isn't rendered verbatim in the -page text was unsearchable — unlike markdown/docx/pptx, which all fold a +page text was unsearchable: unlike markdown/docx/pptx, which all fold a chunk's own heading into ``body``. """ diff --git a/tests/test_extractor_pptx_emits_markdown.py b/tests/test_extractor_pptx_emits_markdown.py index 145c5038..08981a01 100644 --- a/tests/test_extractor_pptx_emits_markdown.py +++ b/tests/test_extractor_pptx_emits_markdown.py @@ -47,7 +47,7 @@ def _make_deck_with_bullets_and_table(out: Path) -> Path: table.cell(1, 0).text = "alpha" table.cell(1, 1).text = "scaffolding" - # Slide 3: speaker notes only — confirms blockquote path. + # Slide 3: speaker notes only, confirms blockquote path. s3 = prs.slides.add_slide(blank) s3.shapes.title.text = "Notes Slide" # type: ignore[union-attr] s3.shapes.add_textbox(Inches(1), Inches(2), Inches(6), Inches(2)).text_frame.text = "body text" @@ -103,7 +103,7 @@ def test_per_slide_chunk_seq_unchanged(deck: Path) -> None: def test_body_struct_remains_plain_text(deck: Path) -> None: - """Snippet pipeline reads body_struct — markdown markers in body_md + """Snippet pipeline reads body_struct: markdown markers in body_md must not leak into block.text.""" for chunk in extract(deck): for block in chunk.body_struct: @@ -112,7 +112,7 @@ def test_body_struct_remains_plain_text(deck: Path) -> None: def test_no_change_to_existing_simple_deck_extraction(tmp_path: Path) -> None: """A deck that's just title + flat-level body (the legacy fixture - shape) keeps producing the same searchable body — no regression on + shape) keeps producing the same searchable body: no regression on existing test_pptx_anchor_attributes_to_correct_slide.""" prs = Presentation() blank = prs.slide_layouts[5] diff --git a/tests/test_extras_detection.py b/tests/test_extras_detection.py index 920ab617..ab4ceb5e 100644 --- a/tests/test_extras_detection.py +++ b/tests/test_extras_detection.py @@ -64,7 +64,7 @@ def test_module_with_only_garbage_files_is_not_installed( ) -> None: """A namespace-shaped directory whose ``__init__.py`` has been removed (only "__init__ 2.py" or similar detritus remains) is NOT - installed — find_spec would otherwise treat it as a namespace + installed: find_spec would otherwise treat it as a namespace package and the naive check would lie.""" site = tmp_path / "site-packages" pkg = site / "fnd_test_husk" @@ -99,7 +99,7 @@ def test_uv_tool_package_uses_uv_dir_not_path( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """For ``install_via='uv-tool'``, detection must look at uv's tool root - (``paths.uv_tool_root()/``) — NOT at PATH. A system-wide binary + (``paths.uv_tool_root()/``), NOT at PATH. A system-wide binary with the same name (e.g. user already has a ``docling`` from a separate Python install) must not register as fnd's install. @@ -122,7 +122,7 @@ def test_uv_tool_ignores_system_binary_on_path( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """Even when ``shutil.which`` would resolve a system-wide binary, - uv-tool detection must NOT use that — it'd misreport fnd's install + uv-tool detection must NOT use that: it'd misreport fnd's install state when the user has unrelated copies of the same CLI on PATH.""" tool_root = tmp_path / "uv-tools" tool_root.mkdir() diff --git a/tests/test_fence_blank_name_crash.py b/tests/test_fence_blank_name_crash.py index f7465120..5ee4925f 100644 --- a/tests/test_fence_blank_name_crash.py +++ b/tests/test_fence_blank_name_crash.py @@ -4,8 +4,8 @@ Some Pygments lexers (e.g. Perl, which ``guess_language`` picks for a bare fence whose content contains ``□``/``<<``/``|``) emit empty ``Token.Name`` tokens. ``_build_spans`` skips blank tokens when building its neighbour-lookup -map, but the ``Token.Name`` branch then looked the token up unconditionally — -``KeyError`` — which propagated out of ``FNDMarkdownFence.__init__`` and crashed +map, but the ``Token.Name`` branch then looked the token up unconditionally, +``KeyError``, which propagated out of ``FNDMarkdownFence.__init__`` and crashed the whole ``FNDMarkdown`` mount (Textual ``_parse_markdown``). """ diff --git a/tests/test_fence_highlight_live.py b/tests/test_fence_highlight_live.py index 87a5375d..4fef8b7f 100644 --- a/tests/test_fence_highlight_live.py +++ b/tests/test_fence_highlight_live.py @@ -5,7 +5,7 @@ terms inside a ``` fence ``` went unhighlighted. ``FNDMarkdownFence`` overlays the match spans on top of the lexer colours. -One document with two fences sharing the document's single match spec — +One document with two fences sharing the document's single match spec: the same shape as the real app, where every chunk of a result gets the one ``_current_match_spec``. The matching fence is highlighted; the non-matching fence is left clean (no over-highlight bleed). @@ -73,7 +73,7 @@ async def test_fence_highlights_subword_of_underscore_identifier() -> None: token of ``recursive_directory_iterator`` and a search for it finds the chunk. The highlighter previously tokenised doc text with ``\\w+`` (keeps underscore), saw one token that failed to stem-match, and left that one - occurrence unhighlighted while sibling plain matches lit up — the exact + occurrence unhighlighted while sibling plain matches lit up: the exact "one code-block match isn't highlighted" field report. Both occurrences must now highlight identically.""" source = ( diff --git a/tests/test_filename_elision.py b/tests/test_filename_elision.py index 3ab1fbcd..5ae8d8f3 100644 --- a/tests/test_filename_elision.py +++ b/tests/test_filename_elision.py @@ -48,14 +48,14 @@ def test_no_extension_still_middle_elides() -> None: def test_extension_longer_than_budget_degrades_gracefully() -> None: - # Suffix can't fit alongside an ellipsis — must not crash or over-run. + # Suffix can't fit alongside an ellipsis: must not crash or over-run. out = _elide_middle_keep_suffix("x.markdown", 4) assert len(out) <= 4, out def test_ellipsis_plus_suffix_fits_at_exact_boundary() -> None: # max_width == len(suffix) + 1: exactly room for "…" + the full - # extension and no stem — must show "…", not drop the extension. + # extension and no stem: must show "…", not drop the extension. assert _elide_middle_keep_suffix("longfile.txt", 5) == "….txt" assert _elide_middle_keep_suffix("report.docx", 6) == "….docx" diff --git a/tests/test_filter_dsl.py b/tests/test_filter_dsl.py index a3e3245a..3703ea14 100644 --- a/tests/test_filter_dsl.py +++ b/tests/test_filter_dsl.py @@ -168,7 +168,7 @@ def test_eval_inequality() -> None: def test_eval_missing_field_strict_null() -> None: - """Per spec: missing field treats the predicate as False — even for != + """Per spec: missing field treats the predicate as False, even for != and even for `not in`. The user opted into strict null.""" pred_eq = compile_filter("Project == 'Website'") pred_neq = compile_filter("Project != 'Website'") diff --git a/tests/test_filter_kinds_nested.py b/tests/test_filter_kinds_nested.py index b18b3808..ec6dc5cb 100644 --- a/tests/test_filter_kinds_nested.py +++ b/tests/test_filter_kinds_nested.py @@ -44,7 +44,7 @@ def test_toggle_category_selects_all_then_clears() -> None: def test_individual_type_survives_category_partial() -> None: """Include a category, drop one type: the parent goes ◐ and the dropped - type is gone — the 'include .py but not .cpp' requirement.""" + type is gone: the 'include .py but not .cpp' requirement.""" sc = _controller() sc._toggle_kind_category("code") sc.filter_kinds.remove("cpp") diff --git a/tests/test_filters_pane_reopen.py b/tests/test_filters_pane_reopen.py index a5d87fc4..6401cc37 100644 --- a/tests/test_filters_pane_reopen.py +++ b/tests/test_filters_pane_reopen.py @@ -3,7 +3,7 @@ The filters tree is wrapped in ``#filters_pane`` which carries the collapse class and, at header height, the only clickable rows (the tree has zero height when collapsed). So the click has to be caught on the container and the reopen -has to act on that frame — not the tree, which is what results/collections use. +has to act on that frame, not the tree, which is what results/collections use. """ from __future__ import annotations @@ -74,7 +74,7 @@ async def test_clicking_collapsed_filters_pane_reopens_it(cfg: Config, idx: Path @pytest.mark.asyncio async def test_clicking_collapsed_results_pane_still_reopens(cfg: Config, idx: Path) -> None: - """Regression guard for the unwrapped panels — the click path they use must + """Regression guard for the unwrapped panels: the click path they use must keep working.""" app = FNDApp(index_dir=idx, config=cfg, initial_query="saffron") async with app.run_test(size=(120, 45)) as pilot: @@ -96,7 +96,7 @@ async def test_clicking_collapsed_results_pane_still_reopens(cfg: Config, idx: P @pytest.mark.asyncio async def test_clicking_expanded_filters_pane_is_not_swallowed(cfg: Config, idx: Path) -> None: - """The container click handler must only act while collapsed — an expanded + """The container click handler must only act while collapsed: an expanded filters pane's clicks still reach the tree (it takes focus), so filter rows stay togglable by mouse.""" app = FNDApp(index_dir=idx, config=cfg, initial_query="saffron") diff --git a/tests/test_first_reindex_warning.py b/tests/test_first_reindex_warning.py index 8c14eff4..59d39f78 100644 --- a/tests/test_first_reindex_warning.py +++ b/tests/test_first_reindex_warning.py @@ -2,7 +2,7 @@ Tests cover the marker-file lifecycle, ETA estimator math, and the helpers that the FNDApp uses to decide whether to show the warning. -The Textual modal class itself is exercised indirectly — full pilot +The Textual modal class itself is exercised indirectly: full pilot testing of modals is flaky under load and out of scope here. """ diff --git a/tests/test_flat_fallback_tier.py b/tests/test_flat_fallback_tier.py index 2733f15a..2efdbe7f 100644 --- a/tests/test_flat_fallback_tier.py +++ b/tests/test_flat_fallback_tier.py @@ -3,7 +3,7 @@ indexed (flat-layer) token is present in the preview texture. The tier keeps the structured Markdown and appends ONLY the flat blocks it -missed (in reading order) — it is not a wholesale flat replacement, so a +missed (in reading order): it is not a wholesale flat replacement, so a well-extracted page is left untouched. """ @@ -43,7 +43,7 @@ def fake_blocks(_page: object) -> list[str]: def test_passthrough_on_complete_coverage() -> None: """A fully-captured page is never touched, and the flat blocks are not - even queried — the one exact short-circuit: if every flat token is in the + even queried: the one exact short-circuit: if every flat token is in the Markdown, no block can be missing.""" flat = _distinct_words(0, 40) calls: list[int] = [] @@ -58,7 +58,7 @@ def test_passthrough_on_complete_coverage() -> None: def test_backfills_a_dropped_block_on_a_well_covered_page() -> None: """The regression this tier's old 0.90 page-level floor allowed through. - A page can be 92% covered and still have lost a whole block — searchable + A page can be 92% covered and still have lost a whole block: searchable prose the preview could never show. Block-level evidence decides now, not a page-level ratio. """ @@ -133,7 +133,7 @@ def test_no_op_when_nothing_missing() -> None: def test_flat_fallback_is_not_a_pipeline_tier() -> None: """Flat-fallback runs in _finalise_body_md AFTER the born-digital - docling fallback, not inside the recovery pipeline — so the prose it + docling fallback, not inside the recovery pipeline, so the prose it backfills can't be discarded by a later docling swap, and a flat-appended 'Table N' can't retro-trigger that docling.""" from fnd.extract import pdf @@ -145,7 +145,7 @@ def test_flat_fallback_is_not_a_pipeline_tier() -> None: def test_finalise_runs_docling_check_before_flat_backfill(monkeypatch: pytest.MonkeyPatch) -> None: """The born-digital docling decision must see the structured markdown - BEFORE flat-fallback appends — otherwise a backfilled 'Table 1' block + BEFORE flat-fallback appends, otherwise a backfilled 'Table 1' block could retro-trigger docling. Assert the markdown handed to _needs_docling_fallback carries no flat-appended prose, and that flat-fallback still backfills afterwards.""" diff --git a/tests/test_flat_pdf_scan.py b/tests/test_flat_pdf_scan.py index 7828ac3a..4a7d34fd 100644 --- a/tests/test_flat_pdf_scan.py +++ b/tests/test_flat_pdf_scan.py @@ -155,7 +155,7 @@ def _fake_scan(*, collection: str | None = None) -> list[flat_pdf_scan.Row]: def test_cold_scan_failure_is_not_cached_as_empty(monkeypatch: pytest.MonkeyPatch) -> None: """A scan that raises before any success must not mint a fresh empty - result — that would show 'nothing to fix' and block retries for the TTL.""" + result: that would show 'nothing to fix' and block retries for the TTL.""" flat_pdf_scan.invalidate_all() calls = {"n": 0} @@ -246,7 +246,7 @@ def test_texturise_flat_pushes_confirm_even_when_scan_fails( monkeypatch: pytest.MonkeyPatch, ) -> None: """The Texturise-flat worker recomputes on a cold cache; if that scan - raises (transient index lock) it must not die silently — the update-all + raises (transient index lock) it must not die silently: the update-all confirm still has to be pushed.""" import fnd.tui.menu as menu @@ -279,7 +279,7 @@ async def test_indexer_modal_never_scans_on_ui_thread( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """Mounting IndexerScreen and letting it tick must not call the - flat-PDF scan on the event-loop thread — that synchronous call was + flat-PDF scan on the event-loop thread: that synchronous call was the multi-second portal-open freeze. Any call must land on a worker thread.""" from fnd.config import load @@ -320,7 +320,7 @@ def _spy_scan(*, collection: str | None = None) -> list[flat_pdf_scan.Row]: await _await_condition(pilot, lambda: False, timeout=0.3) assert loop_thread not in threads_seen, ( - "flat-PDF scan ran on the event-loop thread — portal would freeze" + "flat-PDF scan ran on the event-loop thread: portal would freeze" ) diff --git a/tests/test_frontmatter_limits.py b/tests/test_frontmatter_limits.py index d0851461..1e207184 100644 --- a/tests/test_frontmatter_limits.py +++ b/tests/test_frontmatter_limits.py @@ -1,4 +1,4 @@ -"""Frontmatter size limits — caps protect the markdown extractor from +"""Frontmatter size limits: caps protect the markdown extractor from adversarial files whose YAML-like frontmatter expands into memory before markdown-it ever sees them. (S7)""" @@ -39,7 +39,7 @@ def test_frontmatter_no_fence_returns_none() -> None: def test_frontmatter_limit_respects_patched_value() -> None: - """Confirm the limit is consulted at call time, not import time — + """Confirm the limit is consulted at call time, not import time, so users can tune it from config or environment in future.""" short_limit = 32 with patch.object( @@ -48,7 +48,7 @@ def test_frontmatter_limit_respects_patched_value() -> None: wraps=read_frontmatter_from_text, ): doc = "---\n" + "k: v\n" * 20 + "---\n" - # 20 * 5 bytes = 100 bytes of body alone — guarantees we trip 32. + # 20 * 5 bytes = 100 bytes of body alone: guarantees we trip 32. from fnd.extract import _limits with patch.object(_limits, "LIMIT_FRONTMATTER_TOTAL_BYTES", short_limit): diff --git a/tests/test_full_document_preview.py b/tests/test_full_document_preview.py index 9dfa27d9..f8a6ce73 100644 --- a/tests/test_full_document_preview.py +++ b/tests/test_full_document_preview.py @@ -23,7 +23,7 @@ def built_index(fixtures_dir: Path, tmp_index_dir: Path) -> Path: def test_get_file_chunks_returns_all_pages_in_order(built_index: Path) -> None: - """The PDF fixture has 12 pages — all should come back in chunk_seq order.""" + """The PDF fixture has 12 pages: all should come back in chunk_seq order.""" s = Searcher(index_dir=built_index) # Find the PDF's parent_id via a regular search. hits = s.search("blue penguin sandwich", limit=1) @@ -61,7 +61,7 @@ def test_get_file_chunks_dedups_file_shared_across_collections( ) -> None: """A file covered by two collections (e.g. an Obsidian vault listed under nested source roots) is stored once per collection. The - full-document preview must still return each chunk_seq exactly once — + full-document preview must still return each chunk_seq exactly once, otherwise every chunk renders twice in the preview pane. """ build_index(roots=[fixtures_dir], index_dir=tmp_index_dir, collection="alpha") @@ -131,7 +131,7 @@ def test_render_document_uses_heading_path_when_available() -> None: def test_render_document_highlights_every_match_across_chunks() -> None: - """All occurrences across all chunks must be bolded — not just the first.""" + """All occurrences across all chunks must be bolded, not just the first.""" chunks = [ FileChunk( parent_id="z", diff --git a/tests/test_fusion.py b/tests/test_fusion.py index f0d53d78..0ecf4e6f 100644 --- a/tests/test_fusion.py +++ b/tests/test_fusion.py @@ -2,11 +2,11 @@ Three new pieces land here: -* :func:`fnd.fusion.rrf_fuse` — pure Reciprocal Rank Fusion of ranked Hit +* :func:`fnd.fusion.rrf_fuse`, pure Reciprocal Rank Fusion of ranked Hit lists with per-source weights and a small rank-1/2/3 position bonus. -* :func:`fnd.fusion.auto_subqueries` — derive sub-queries from a typed user +* :func:`fnd.fusion.auto_subqueries`, derive sub-queries from a typed user query (phrase + lex + optional synonym). -* :func:`fnd.fusion.parse_multi_input` — parse the ``:multi`` typed +* :func:`fnd.fusion.parse_multi_input`, parse the ``:multi`` typed multi-line syntax (``lex:`` / ``phrase:`` / ``syn:``). Plus :func:`fnd.fusion.fusion_search` orchestrates the above against an @@ -73,7 +73,7 @@ def test_rrf_fuse_dedupes_across_rankings() -> None: fused = rrf_fuse([r1, r2], weights=[1.0, 1.0], k=60) ids = [h.parent_id for h in fused] # Each id once; relative order: a and b have symmetric contributions, so - # tied — but one of them must come first deterministically. + # tied, but one of them must come first deterministically. assert sorted(ids) == ["a", "b"] assert len(fused) == 2 @@ -97,7 +97,7 @@ def test_equal_scores_in_a_pass_share_a_rank() -> None: def test_rrf_fuse_weight_dominance() -> None: """A doc only in the heavily-weighted sub-query must outrank a doc only - in the low-weight sub-query — the weight is the multiplier on each + in the low-weight sub-query: the weight is the multiplier on each sub-query's RRF contribution.""" heavy = [_hit("h")] light = [_hit("l")] @@ -152,8 +152,8 @@ def test_auto_subqueries_multiword_query_emits_phrase_and_lex() -> None: def test_auto_subqueries_single_word_skips_phrase() -> None: - """A single-word query has no meaningful phrase pass — phrase("foo") == - term("foo") — so we only emit the lex pass.""" + """A single-word query has no meaningful phrase pass: phrase("foo") == + term("foo"), so we only emit the lex pass.""" subs = auto_subqueries("foo", synonyms=None) sources = [s.source for s in subs] assert sources == ["lex"] @@ -161,7 +161,7 @@ def test_auto_subqueries_single_word_skips_phrase() -> None: def test_auto_subqueries_user_supplied_quotes_skip_auto_phrase() -> None: """When the user already wraps their query in quotes, the lex pass routes - that through Tantivy as a PhraseQuery on its own — adding an auto-phrase + that through Tantivy as a PhraseQuery on its own: adding an auto-phrase pass would double-wrap (``""man in the middle""``) and crash the parser. Skip the auto-phrase pass in that case; the lex pass already carries the phrase semantics the user asked for. @@ -241,7 +241,7 @@ def test_auto_subqueries_quoted_query_skips_synonym_pass() -> None: def test_auto_subqueries_plain_query_still_expands_synonyms() -> None: - """The guard must not suppress the syn pass for a plain bag-of-words query — + """The guard must not suppress the syn pass for a plain bag-of-words query: that is the whole point of the synonym feature.""" table = SynonymTable.from_groups([["mfa", "multi-factor authentication"]]) subs = auto_subqueries("reset mfa", synonyms=table) @@ -267,7 +267,7 @@ def test_parse_multi_input_basic() -> None: def test_parse_multi_input_uses_default_weights_per_source() -> None: - """`lex:` defaults to weight 1.0, `phrase:` to 2.0, `syn:` to 0.6 — + """`lex:` defaults to weight 1.0, `phrase:` to 2.0, `syn:` to 0.6, matching the auto-mode weights.""" result = parse_multi_input('lex: a\nphrase: "b c"\nsyn: a', synonyms=None) by_source = {s.source: s for s in result.subqueries} @@ -304,10 +304,10 @@ def test_parse_multi_input_skips_blank_lines_and_unknown_prefixes() -> None: def fusion_corpus(tmp_path: Path, tmp_index_dir: Path) -> Path: """Three docs that exercise phrase / lex / syn distinction. - * adjacent.md — both query terms next to each other (phrase pass wins) - * scattered.md — both terms in the doc but not adjacent (lex matches, + * adjacent.md: both query terms next to each other (phrase pass wins) + * scattered.md, both terms in the doc but not adjacent (lex matches, phrase does not) - * synonym.md — only the long-form synonym appears (syn pass surfaces) + * synonym.md: only the long-form synonym appears (syn pass surfaces) """ root = tmp_path / "docs" root.mkdir(parents=True) @@ -328,7 +328,7 @@ def fusion_corpus(tmp_path: Path, tmp_index_dir: Path) -> Path: def test_fusion_search_returns_phrase_match_first(fusion_corpus: Path) -> None: - """The doc with adjacent terms must rank above the scattered one — the + """The doc with adjacent terms must rank above the scattered one: the phrase sub-query (weight 2.0) lifts it past the lex-only contribution.""" s = Searcher(index_dir=fusion_corpus) hits = fusion_search(s, query="susy breaking", limit=10) @@ -367,7 +367,7 @@ def test_fusion_search_subqueries_override_auto_derivation( fusion_corpus: Path, ) -> None: """Passing explicit sub-queries (e.g. from `:multi`) bypasses - auto_subqueries — only the supplied list runs. + auto_subqueries: only the supplied list runs. With a single ``lex`` sub-query, no hit can be primarily attributed to the synonym or phrase passes (those weren't issued). @@ -406,7 +406,7 @@ def test_fusion_search_pass_index_2_for_synonym_primary( def test_fusion_search_degrades_malformed_subquery(fusion_corpus: Path) -> None: """A malformed sub-query must degrade to an empty ranking, never abort the - whole fused search — the other sub-queries still return their hits.""" + whole fused search: the other sub-queries still return their hits.""" s = Searcher(index_dir=fusion_corpus) subs = [ SubQuery(query="susy breaking", weight=1.0, source="lex"), diff --git a/tests/test_fusion_phrase_pass.py b/tests/test_fusion_phrase_pass.py index c75a83bc..2286e599 100644 --- a/tests/test_fusion_phrase_pass.py +++ b/tests/test_fusion_phrase_pass.py @@ -21,13 +21,13 @@ def test_unquoted_multiword_emits_phrase_and_lex() -> None: by = {s.source: s for s in subs} assert by["phrase"].query == '"alpha beta gamma"' # exact, in order assert "lex" in by - # No graded slop passes — exactly the phrase + lex pair. + # No graded slop passes: exactly the phrase + lex pair. assert {s.source for s in subs} == {"phrase", "lex"} assert by["phrase"].weight > by["lex"].weight def test_quoted_query_keeps_exact_intent_no_broadening() -> None: - """A user-quoted phrase stays exact — the lex pass already carries it as a + """A user-quoted phrase stays exact: the lex pass already carries it as a PhraseQuery, so we don't add a second phrase pass that would double-quote.""" subs = auto_subqueries('"alpha beta gamma"', synonyms=None) assert [s.source for s in subs] == ["lex"] @@ -38,8 +38,8 @@ def test_single_word_has_no_phrase_pass() -> None: def test_structured_queries_skip_the_phrase_pass() -> None: - """A query that already carries structure — booleans, ``+``/``-``, wildcard, - fuzzy, regex, grouping, or a ``^`` boost — runs lex-only. Wrapping it as a + """A query that already carries structure: booleans, ``+``/``-``, wildcard, + fuzzy, regex, grouping, or a ``^`` boost: runs lex-only. Wrapping it as a phrase mangles the operators (``"crypto* -wallet"`` → phrase "crypto wallet") and skews ranking toward adjacency the user never asked for.""" for q in ( diff --git a/tests/test_get_file_chunks_parallel.py b/tests/test_get_file_chunks_parallel.py index 9794e35b..8713cacc 100644 --- a/tests/test_get_file_chunks_parallel.py +++ b/tests/test_get_file_chunks_parallel.py @@ -3,8 +3,8 @@ The contract these tests pin: * Serial and parallel decode produce **identical** chunk lists. Decode - order isn't a contract — the result is sorted by ``chunk_seq`` either - way — so test on the sorted list directly. + order isn't a contract (the result is sorted by ``chunk_seq`` either + way) so test on the sorted list directly. * Below the parallel-decode threshold the worker pool is not spun up, even when ``max_workers > 1``. This avoids the thread-startup tax for small markdown files / short PDFs that don't benefit. @@ -47,7 +47,7 @@ def _find_parent_id(searcher: Searcher, path_suffix: str) -> str: def test_serial_and_parallel_decode_produce_identical_chunks(built_index: Path) -> None: - """Same inputs, same chunks — regardless of decode path.""" + """Same inputs, same chunks: regardless of decode path.""" searcher = Searcher(index_dir=built_index) parent_id = _find_parent_id(searcher, "papers/test.pdf") @@ -132,7 +132,7 @@ def spy(searcher_arg: object, address: object) -> object: def test_max_workers_one_stays_serial(built_index: Path) -> None: - """``max_workers=1`` is an explicit opt-out — no thread pool even + """``max_workers=1`` is an explicit opt-out: no thread pool even when over the threshold.""" searcher = Searcher(index_dir=built_index) parent_id = _find_parent_id(searcher, "papers/test.pdf") diff --git a/tests/test_heading_fold.py b/tests/test_heading_fold.py index 34806e24..150bb289 100644 --- a/tests/test_heading_fold.py +++ b/tests/test_heading_fold.py @@ -1,7 +1,7 @@ """``HeadingFolder`` folds a chunk's OWN heading, into every representation. Pre-fix, every PDF page of a TOC section had that section's heading prepended -to the searchable ``body`` — but never to ``body_md``, which is what the +to the searchable ``body``, but never to ``body_md``, which is what the structural preview renders. Continuation pages therefore matched a heading they neither owned nor displayed, and selecting one scrolled the preview to text with no highlight anywhere. @@ -69,7 +69,7 @@ def test_new_heading_after_a_run_is_folded() -> None: def test_folder_state_does_not_span_documents() -> None: - """A fresh folder per document — otherwise the first chunk of file B + """A fresh folder per document, otherwise the first chunk of file B would be treated as a continuation of file A's last section.""" first = HeadingFolder().fold(_chunk("Ch 1 > Alpha", "a")) second = HeadingFolder().fold(_chunk("Ch 1 > Alpha", "b")) @@ -135,7 +135,7 @@ def test_emphasised_heading_is_recognised_as_already_present() -> None: """The structured extractor emits page headings with their typographic emphasis intact (``## **Foo**``) while the TOC gives the plain string. Comparing them literally folded a second copy in, and the preview showed - the same heading twice — seen live on the AWS guide's p.24.""" + the same heading twice: seen live on the AWS guide's p.24.""" folder = HeadingFolder() c = folder.fold( _chunk( @@ -179,7 +179,7 @@ def test_searchable_text_the_markdown_lost_is_appended() -> None: def test_mirroring_is_idempotent() -> None: - """Re-running must not duplicate — the repair also runs on cache-served + """Re-running must not duplicate: the repair also runs on cache-served chunks, which may already carry it.""" from fnd.extract.pdf import _mirror_body_into_md @@ -214,7 +214,7 @@ def test_dispersed_terms_do_not_stand_in_for_a_phrase() -> None: Asking whether a line's tokens appear anywhere in the markdown let scattered words satisfy a phrase: "virtual memory" was treated as present because "virtual" and "memory" each occurred elsewhere. A phrase query then matched - the chunk with no contiguous rendered text for the highlighter to paint — + the chunk with no contiguous rendered text for the highlighter to paint: the exact failure this repair exists to prevent. """ from fnd.extract.pdf import _mirror_body_into_md diff --git a/tests/test_highlight_covers_fuzzy_and_synonyms.py b/tests/test_highlight_covers_fuzzy_and_synonyms.py index cc1e2d0d..560823ed 100644 --- a/tests/test_highlight_covers_fuzzy_and_synonyms.py +++ b/tests/test_highlight_covers_fuzzy_and_synonyms.py @@ -1,4 +1,4 @@ -"""Highlights cover every match the cascade would surface — exact +"""Highlights cover every match the cascade would surface: exact literal stems, fuzzy-AUTO variants, and synonym-expanded forms. Pre-fix the highlighter only marked words whose Snowball stem matched @@ -37,7 +37,7 @@ def test_match_spec_exact_stem() -> None: def test_match_spec_fuzzy_variant() -> None: - """User typed a typo (Templatas) — the cascade's fuzzy pass would + """User typed a typo (Templatas): the cascade's fuzzy pass would have surfaced docs containing "templates" / "template". The spec must therefore highlight those words too.""" spec = MatchSpec.from_query("templatas") @@ -47,7 +47,7 @@ def test_match_spec_fuzzy_variant() -> None: def test_match_spec_two_typo_fuzzy() -> None: """An explicit ``~2`` highlights 2-edit typos. (AUTO-fuzzy highlighting is - capped at distance 1 — distance 2 lights up too many unrelated false friends + capped at distance 1: distance 2 lights up too many unrelated false friends on a clean query; the search-side cascade still uses AUTO distance 2.)""" spec = MatchSpec.from_query("tempplatas~2") assert word_matches("templates", spec) @@ -57,7 +57,7 @@ def test_match_spec_two_typo_fuzzy() -> None: def test_match_spec_synonym_expansion() -> None: """A query for "k8s" with a synonym group {k8s, kubernetes} - highlights both forms — the synonym pass would have surfaced docs + highlights both forms: the synonym pass would have surfaced docs that only mention "kubernetes".""" syns = SynonymTable.from_groups([["k8s", "kubernetes"]]) spec = MatchSpec.from_query("k8s pod", synonyms=syns) @@ -85,7 +85,7 @@ def test_match_spec_joined_compound_paints_exactly() -> None: def test_match_spec_short_term_skips_fuzzy() -> None: - """Stems of 1-2 chars get AUTO distance 0 — typos at that length + """Stems of 1-2 chars get AUTO distance 0: typos at that length almost always change meaning, and any-1-edit explodes false positives. Only exact matches highlight.""" spec = MatchSpec.from_query("ai") @@ -118,7 +118,7 @@ def test_word_runs_exact_match_is_one_yellow_run() -> None: def test_word_runs_fuzzy_match_marks_only_diverging_char_orange() -> None: """User types "Templatas"; the doc word "templates" matches via the fuzzy pass. Char-level alignment marks the matching prefix and - suffix yellow and the one diverging char orange — exactly the + suffix yellow and the one diverging char orange: exactly the spec the user asked for.""" from fnd.render import HIGHLIGHT_STYLE, MISMATCH_STYLE, word_highlight_runs @@ -129,13 +129,13 @@ def test_word_runs_fuzzy_match_marks_only_diverging_char_orange() -> None: # Aligned to "templatas": match through index 6, sub at 7, match at 8. assert runs == [ (0, 7, HIGHLIGHT_STYLE), # "templat" - (7, 8, MISMATCH_STYLE), # "e" — divergent from query "a" + (7, 8, MISMATCH_STYLE), # "e": divergent from query "a" (8, 9, HIGHLIGHT_STYLE), # "s" ] def test_word_runs_synonym_match_is_all_yellow() -> None: - """A synonym hit has no char-level discrepancy story — the user + """A synonym hit has no char-level discrepancy story: the user typed "k8s" but a synonym group expanded to "kubernetes". The whole synonym word should read as a clean match (yellow) rather than mostly-orange because of low char-level overlap.""" @@ -148,7 +148,7 @@ def test_word_runs_synonym_match_is_all_yellow() -> None: def test_word_runs_two_typo_fuzzy_marks_both_diverging_chars() -> None: - """An explicit ``~2`` for a 2-edit typo ("tempplatas" — extra 'p' AND 'a' + """An explicit ``~2`` for a 2-edit typo ("tempplatas": extra 'p' AND 'a' for 'e') still surfaces "templates", and alignment marks both diverging positions orange. (AUTO-fuzzy highlighting caps at distance 1.)""" from fnd.render import MISMATCH_STYLE, word_highlight_runs @@ -170,7 +170,7 @@ def test_word_runs_non_match_returns_empty() -> None: def test_word_runs_case_difference_alone_is_not_a_mismatch() -> None: - """Matching is case-insensitive — "Templates" against query + """Matching is case-insensitive: "Templates" against query "templates" is a literal stem hit, all yellow. No orange even though 'T' is uppercase in the doc and lowercase in the query.""" from fnd.render import HIGHLIGHT_STYLE, MISMATCH_STYLE, word_highlight_runs @@ -236,7 +236,7 @@ async def test_fuzzy_match_chunk_highlights_the_actual_word( word_lower = "templates" def word_rendered() -> bool: - """The widget existing is not the word being in it — Windows mounted + """The widget existing is not the word being in it: Windows mounted the FNDMarkdown a frame before its blocks carried any content, and the search below then read -1.""" for md in pane.query(FNDMarkdown): @@ -255,7 +255,7 @@ def word_rendered() -> bool: md_widgets = list(pane.query(FNDMarkdown)) assert md_widgets, "expected fuzzy-hit chunk to mount" # The chars of the doc word "templates" must be COVERED by - # highlight spans (collectively — for a fuzzy hit the word + # highlight spans (collectively: for a fuzzy hit the word # is split into yellow / orange runs, so a single span won't # span the whole word). all_styled_chars: set[int] = set() diff --git a/tests/test_highlight_operators.py b/tests/test_highlight_operators.py index 2c8c9d17..5f265aca 100644 --- a/tests/test_highlight_operators.py +++ b/tests/test_highlight_operators.py @@ -1,4 +1,4 @@ -"""Highlighting must mark exactly the words the search surfaced — across every +"""Highlighting must mark exactly the words the search surfaced: across every operator. Regression guard for the wildcard/regex/fuzzy highlight expansion (``MatchSpec`` carries prefix/regex patterns and uses OSA fuzzy, mirroring the search-side resolvers). @@ -24,7 +24,7 @@ def _highlighted(query: str, text: str) -> set[str]: def _coloured(query: str, text: str) -> list[tuple[str, str]]: - """Return (substring, 'Y'|'O') spans — Y = yellow match, O = orange variance. + """Return (substring, 'Y'|'O') spans: Y = yellow match, O = orange variance. Captures the convention: matching/literal chars yellow, fuzzy/wildcard-filled chars orange.""" spec = MatchSpec.from_query(query, auto_fuzzy=True, min_term_chars=0) @@ -74,7 +74,7 @@ def test_wildcard_excludes_non_prefix() -> None: ("query", "text", "expected"), [ # wildcard with another term: the all-orange regression is gone, and each - # term gets its own colour — strategy(slot 0 Y), discoun(slot 1 C) + t(O). + # term gets its own colour: strategy(slot 0 Y), discoun(slot 1 C) + t(O). ( "strategy discoun*", "the strategy gave a discount", @@ -128,13 +128,13 @@ def test_per_term_colours() -> None: def test_field_qualifier_does_not_consume_a_colour_slot() -> None: - """`kind:pdf` is a filter, not a body term — the real term keeps slot-0 + """`kind:pdf` is a filter, not a body term; the real term keeps slot-0 yellow instead of being pushed to a later colour by `kind`/`pdf`.""" assert _coloured("kind:pdf strategy", "a strategy doc") == [("strategy", "Y")] def test_excluded_terms_are_not_highlighted() -> None: - """`-x` / `NOT x` are prohibited — they must never highlight.""" + """`-x` / `NOT x` are prohibited: they must never highlight.""" for q in ("crypto -wallet", "crypto NOT wallet"): spec = MatchSpec.from_query(q) assert word_matches("crypto", spec) @@ -142,7 +142,7 @@ def test_excluded_terms_are_not_highlighted() -> None: def test_negated_group_terms_are_not_highlighted() -> None: - """`NOT (a OR b)` / `-(a OR b)` excludes the whole group — none of its terms + """`NOT (a OR b)` / `-(a OR b)` excludes the whole group: none of its terms highlight (a single-token negation flag would leak everything past the first).""" for q in ("crypto NOT (wallet OR purse)", "crypto -(wallet OR purse)"): spec = MatchSpec.from_query(q) @@ -157,7 +157,7 @@ def test_negated_group_terms_are_not_highlighted() -> None: def test_field_group_value_is_not_highlighted() -> None: - """A scoped field group (`title:(a OR b)`) is a filter — its values must not + """A scoped field group (`title:(a OR b)`) is a filter; its values must not highlight in the body or consume a colour slot, even split across tokens.""" spec = MatchSpec.from_query("networks title:(wallet OR purse)") assert word_matches("networks", spec) @@ -166,7 +166,7 @@ def test_field_group_value_is_not_highlighted() -> None: def test_regex_metacharacters_are_not_corrupted() -> None: - """A `/regex/` is kept verbatim — lowercasing would flip `\\D`→`\\d` etc. — + """A `/regex/` is kept verbatim: lowercasing would flip `\\D`→`\\d` etc. and matched case-insensitively against the (lowercased) doc word.""" spec = MatchSpec.from_query(r"/\D+/") # non-digits assert word_matches("abc", spec) diff --git a/tests/test_highlight_punctuation.py b/tests/test_highlight_punctuation.py index 85104ac1..a004d93a 100644 --- a/tests/test_highlight_punctuation.py +++ b/tests/test_highlight_punctuation.py @@ -4,7 +4,7 @@ punctuation). ``_terms_from_query`` split the query on whitespace only, so a term like ``3.`` or ``Monitoring,`` kept its trailing punctuation; its stem (``3.`` / ``monitoring,``) then never equalled the clean -doc-word stem (``3`` / ``monitor``) — so those words were silently left +doc-word stem (``3`` / ``monitor``), so those words were silently left un-highlighted while neighbouring clean words lit up. """ @@ -28,7 +28,7 @@ def test_punctuated_unquoted_terms_match_doc_words() -> None: """Unquoted terms with adjacent punctuation still highlight doc-wide. (For a *quoted* phrase the words highlight via the phrase span, not - document-wide — covered in test_phrase_matching.)""" + document-wide: covered in test_phrase_matching.)""" spec = MatchSpec.from_query("3. Monitoring, segmentation depth", auto_fuzzy=True) # The tokens that sat before punctuation. assert word_matches("Monitoring", spec) diff --git a/tests/test_highlight_tui_e2e.py b/tests/test_highlight_tui_e2e.py index 8139e90c..97352fb8 100644 --- a/tests/test_highlight_tui_e2e.py +++ b/tests/test_highlight_tui_e2e.py @@ -1,5 +1,5 @@ """End-to-end: a wildcard query, driven through the real TUI, must colour the -matched word in the preview as literal-prefix-yellow + wildcard-fill-orange — +matched word in the preview as literal-prefix-yellow + wildcard-fill-orange, not the all-orange regression. Exercises the live query → MatchSpec → highlight path (the unit tests build the spec directly; this proves the app does too). """ diff --git a/tests/test_highlights_toggle.py b/tests/test_highlights_toggle.py index f6aea85b..65ca3625 100644 --- a/tests/test_highlights_toggle.py +++ b/tests/test_highlights_toggle.py @@ -70,7 +70,7 @@ def _has_highlight_span(pane: VerticalScroll) -> bool: @pytest.mark.asyncio async def test_highlights_default_on(cfg: Config, md_index: Path) -> None: """Spawning the app with a query yields highlighted spans without - any user interaction — the default state is "highlights on".""" + any user interaction: the default state is "highlights on".""" app = FNDApp(index_dir=md_index, config=cfg, collection="notes", initial_query="templates") async with app.run_test() as pilot: await safe_pause(pilot) @@ -89,7 +89,7 @@ async def test_highlights_default_on(cfg: Config, md_index: Path) -> None: @pytest.mark.asyncio async def test_h_key_toggles_highlights_off_then_on(cfg: Config, md_index: Path) -> None: """Pressing ``h`` once removes every highlight span; pressing it - again restores them — same query, no re-search.""" + again restores them: same query, no re-search.""" app = FNDApp(index_dir=md_index, config=cfg, collection="notes", initial_query="templates") async with app.run_test() as pilot: await safe_pause(pilot) @@ -112,7 +112,7 @@ async def test_h_key_toggles_highlights_off_then_on(cfg: Config, md_index: Path) message="highlights didn't clear after first toggle", ) - # Toggle back on — same query, highlights restored. + # Toggle back on: same query, highlights restored. await safe_press(pilot, "h") await wait_until( pilot, diff --git a/tests/test_hit_field_preservation.py b/tests/test_hit_field_preservation.py index caf49852..06eef8fe 100644 --- a/tests/test_hit_field_preservation.py +++ b/tests/test_hit_field_preservation.py @@ -1,7 +1,7 @@ """Regression guard: every Hit-rebuild helper preserves every field. ``Hit.line`` was added but the cascade / fusion / rerank rebuild -helpers were not audited — they kept the legacy field list, silently +helpers were not audited: they kept the legacy field list, silently defaulting ``line`` to 0 every time they cloned a Hit. The TUI's search path always goes through cascade + rerank, so by the time a Hit reached the opener, ``line=0``, and the vscode/sublime handlers @@ -32,7 +32,7 @@ def _populated_hit() -> Hit: Every field must be listed: the guard compares field-by-field, so any field left at its default compares equal to a dropped one and the guard passes while the field is being silently lost. That is exactly what happened to - ``body_text`` and ``body_md`` — see + ``body_text`` and ``body_md``: see :func:`test_every_hit_field_is_populated_by_the_fixture`, which now makes the omission impossible to repeat. """ diff --git a/tests/test_index_commit_retry.py b/tests/test_index_commit_retry.py index e1274bf7..836005b2 100644 --- a/tests/test_index_commit_retry.py +++ b/tests/test_index_commit_retry.py @@ -78,7 +78,7 @@ def test_commit_does_not_retry_a_neighbouring_error_code( ) -> None: """The numeric markers must be anchored: bare `os error 5` also matches 50-59, six of which are network-share failures that a NAS-hosted index will - really see. `os error 2` cannot detect that — it is rejected by the narrow + really see. `os error 2` cannot detect that: it is rejected by the narrow predicate and the wide one alike, so it pins the string, not the contract.""" _fast_backoff(monkeypatch) network = ValueError("An IO error occurred: 'The network path was not found. (os error 53)'") diff --git a/tests/test_index_runner.py b/tests/test_index_runner.py index c2fe341b..4bc75fea 100644 --- a/tests/test_index_runner.py +++ b/tests/test_index_runner.py @@ -81,11 +81,11 @@ def test_load_state_returns_none_when_corrupt(tmp_path: Path) -> None: def test_clear_state_idempotent(tmp_path: Path) -> None: """clear_state must be safe to call when file doesn't exist.""" p = tmp_path / "ghost.toml" - clear_state(p) # missing — no error + clear_state(p) # missing: no error p.write_text("[state]\ncollection = 'x'\n") clear_state(p) assert not p.exists() - clear_state(p) # already gone — still no error + clear_state(p) # already gone: still no error @pytest.mark.asyncio @@ -251,7 +251,7 @@ async def test_cache_hit_reported_on_warm_run(tmp_path: Path, papers_dir: Path) state_path = tmp_path / "state.toml" cfg = CollectionConfig(sources=[SourceConfig(path=papers_dir)]) - # Cold run — populates the cache. + # Cold run: populates the cache. async for _ev in run_indexer( config=cfg, collection="test", @@ -260,7 +260,7 @@ async def test_cache_hit_reported_on_warm_run(tmp_path: Path, papers_dir: Path) ): pass - # Warm run — every file_complete should report cache_hit=True. + # Warm run: every file_complete should report cache_hit=True. hits = 0 misses = 0 async for ev in run_indexer( @@ -412,7 +412,7 @@ def test_load_state_tolerates_pre_upgrade_file(tmp_path: Path) -> None: @pytest.mark.asyncio async def test_warm_run_skips_unchanged_files(tmp_path: Path, papers_dir: Path) -> None: """Incremental: a second run over the SAME index_dir with no file - changes skips every file — counted as indexed_already with ZERO cache + changes skips every file: counted as indexed_already with ZERO cache activity (the extractor is never invoked, proving a true skip rather than a re-extract-with-cache-hit).""" cfg = CollectionConfig(sources=[SourceConfig(path=papers_dir)]) @@ -437,7 +437,7 @@ async def test_warm_run_skips_unchanged_files(tmp_path: Path, papers_dir: Path) def test_run_sync_returns_chunks_written(tmp_path: Path) -> None: """The CLI's `indexed N chunks for collection X` line needs a chunk - count, not a file count — one file split into two heading-sections + count, not a file count: one file split into two heading-sections must report 2, proving this isn't just len(files).""" notes = tmp_path / "notes" notes.mkdir() @@ -452,7 +452,7 @@ def test_run_sync_returns_chunks_written(tmp_path: Path) -> None: def test_chunks_written_excludes_rolled_back_chunks( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - """A file that yields chunks and then fails has those chunks deleted — + """A file that yields chunks and then fails has those chunks deleted: the reported total must not count chunks that aren't in the index.""" import fnd.index_runner as runner_module @@ -478,7 +478,7 @@ def fake_process_one_file(*, path: Path, **kwargs: object) -> runner_module._Fil @pytest.mark.asyncio async def test_changed_mtime_reprocesses(tmp_path: Path, papers_dir: Path) -> None: - """A file whose mtime changed must NOT be skipped — it is re-extracted + """A file whose mtime changed must NOT be skipped: it is re-extracted (here the content is unchanged, so it cache-hits, proving the extractor actually ran rather than the file being skipped).""" import os @@ -575,7 +575,7 @@ def test_not_resumable_unknown_collection() -> None: def test_not_resumable_when_stale() -> None: - # Six days old — abandoned, not a just-interrupted run. + # Six days old: abandoned, not a just-interrupted run. st = _state(last_update="2026-05-29T13:40:00+00:00") assert is_state_resumable(st, known_collections={"default"}, now=_now()) is False diff --git a/tests/test_index_tags.py b/tests/test_index_tags.py index 1b34d91c..11d216a1 100644 --- a/tests/test_index_tags.py +++ b/tests/test_index_tags.py @@ -122,7 +122,7 @@ def test_build_index_expands_nested_tags(tmp_path: Path) -> None: def test_build_index_from_config_writes_tags(tmp_path: Path) -> None: - """`fnd collection reindex` goes through build_index_from_config — a third + """`fnd collection reindex` goes through build_index_from_config: a third index path. It must write tags too, or a CLI reindex silently produces a tagless collection.""" from fnd.config import CollectionConfig, SourceConfig diff --git a/tests/test_indexer_cancel_race.py b/tests/test_indexer_cancel_race.py index 1e76d6a0..c42ba8a1 100644 --- a/tests/test_indexer_cancel_race.py +++ b/tests/test_indexer_cancel_race.py @@ -2,7 +2,7 @@ clobber the chain queue a newer run just set up. Reproduces the user-reported bug where cancelling a single-collection -rebuild and immediately launching 'Rebuild all collections' stalled — +rebuild and immediately launching 'Rebuild all collections' stalled: the dying run's teardown reset _indexer_chain_remaining, wiping the new chain. The run-generation guard fixes it. """ diff --git a/tests/test_indexer_cloud_policy.py b/tests/test_indexer_cloud_policy.py index 16d02664..020947b2 100644 --- a/tests/test_indexer_cloud_policy.py +++ b/tests/test_indexer_cloud_policy.py @@ -2,7 +2,7 @@ The policy, from the 7-minute Update-all stall: a file the user can see in their file manager should end up in the index, so the indexer pulls it down -rather than refusing it. What must never happen again is the *silent* wait — +rather than refusing it. What must never happen again is the *silent* wait, so every fetch is announced through :func:`fnd.cloud_files.current_wait`, bounded by ``defaults.cloud_fetch_timeout_s``, and abandonable mid-run via the modal's "Skip cloud-only" action. @@ -100,7 +100,7 @@ def test_policy_default_fetches_and_opt_out_skips(tmp_path: Path) -> None: async def test_cloud_only_file_is_fetched_not_refused( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - """A placeholder is materialised and then indexed — the old behaviour + """A placeholder is materialised and then indexed: the old behaviour refused it outright with "download in Finder before indexing".""" doc = tmp_path / "cloud.md" doc.write_text("# heading\n\nbody text\n", encoding="utf-8") @@ -130,7 +130,7 @@ async def test_opt_out_skips_cloud_only_files_mid_run( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """With the opt-out set, the placeholder is reported as skipped rather - than fetched — and the reason names the provider.""" + than fetched, and the reason names the provider.""" doc = tmp_path / "cloud.md" doc.write_text("# heading\n\nbody\n", encoding="utf-8") monkeypatch.setattr("fnd.index_runner.is_placeholder", lambda _p: True) @@ -194,7 +194,7 @@ async def test_opt_out_does_not_flag_already_indexed_cloud_files( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """An unchanged, already-indexed cloud-only file needs no work either - way — with the opt-out on it must still read as "already indexed", not + way, with the opt-out on it must still read as "already indexed", not as something the user was denied.""" # Source root holds only the document: the state file and index must # live outside it, or the second run enumerates them as corpus files. @@ -239,7 +239,7 @@ async def test_scan_phase_blocks_are_counted_not_dropped( Frontmatter-filter candidates are dropped from the walk when the fetch is declined or fails, so they never reach the per-file loop. Without an - explicit flush they vanish silently — no count, no event, no log entry — + explicit flush they vanish silently (no count, no event, no log entry) which is the exact failure mode this change exists to prevent. """ corpus = tmp_path / "vault" @@ -278,7 +278,7 @@ def test_fetch_aborts_promptly_when_the_run_is_cancelled(tmp_path: Path) -> None """A stalled provider must not hold Cancel for the whole timeout. Without polling, `fetch` joined for `timeout_s` (60s by default), so a - Cancel pressed while a download was wedged sat unanswered for a minute — + Cancel pressed while a download was wedged sat unanswered for a minute: undoing the responsiveness this change set exists to deliver. """ target = tmp_path / "wedged.pdf" @@ -297,7 +297,7 @@ def _never() -> str: cloud_files.fetch(_never, path=target, timeout_s=30.0, cancel=cancel) elapsed = time.monotonic() - started - assert elapsed < 5.0, f"cancel took {elapsed:.1f}s — not polled during the join" + assert elapsed < 5.0, f"cancel took {elapsed:.1f}s, not polled during the join" assert cloud_files.current_wait() is None diff --git a/tests/test_indexer_modal_status_lines.py b/tests/test_indexer_modal_status_lines.py index f1d6e38c..5d2f0d62 100644 --- a/tests/test_indexer_modal_status_lines.py +++ b/tests/test_indexer_modal_status_lines.py @@ -88,7 +88,7 @@ def test_current_line_names_the_provider_and_wait_while_fetching() -> None: assert "Fetching from iCloud Drive" in out assert "Team Meeting.md" in out assert "waiting 9s" in out - # The fetch owns the line — the unrelated per-page stall tag would be + # The fetch owns the line: the unrelated per-page stall tag would be # misleading while the extractor hasn't even been handed the file. assert "stuck" not in out diff --git a/tests/test_indexer_prepare_backstop.py b/tests/test_indexer_prepare_backstop.py index d02d2df5..4a192bf1 100644 --- a/tests/test_indexer_prepare_backstop.py +++ b/tests/test_indexer_prepare_backstop.py @@ -63,7 +63,7 @@ def boom(*_args: Any, **_kwargs: Any) -> Any: async def test_prepare_unexpected_exception_also_recovered( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - """Any exception type, not just LockBusy — the modal must never + """Any exception type, not just LockBusy: the modal must never hang on a silent task death.""" def boom(*_args: Any, **_kwargs: Any) -> Any: diff --git a/tests/test_indexer_resume_all_collections.py b/tests/test_indexer_resume_all_collections.py index f453db03..7fc1f507 100644 --- a/tests/test_indexer_resume_all_collections.py +++ b/tests/test_indexer_resume_all_collections.py @@ -1,7 +1,7 @@ """Auto-resume covers every collection, not one named ``default``. ``maybe_resume`` used to load exactly ``default.state.toml``. Anyone whose -collections are all named — the normal case — had the "Auto-resume on launch" +collections are all named (the normal case) had the "Auto-resume on launch" toggle read as on while doing nothing, and their interrupted runs left state files that were never resumed and never cleaned up. """ @@ -163,7 +163,7 @@ def test_completed_state_is_cleaned_up(tmp_path: Path, monkeypatch: pytest.Monke def test_opt_out_still_sweeps_but_never_starts( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - """Indexing is heavy: with auto-resume off nothing may start — but the + """Indexing is heavy: with auto-resume off nothing may start, but the dead state file is still tidied.""" cfg = _cfg(["Reading"], auto_resume=False) save_state(tmp_path / "Reading.state.toml", _state("Reading", done=3, total=97)) diff --git a/tests/test_indexer_scan_phase.py b/tests/test_indexer_scan_phase.py index addb4088..0f4060a3 100644 --- a/tests/test_indexer_scan_phase.py +++ b/tests/test_indexer_scan_phase.py @@ -5,7 +5,7 @@ evaluate the filter, and on a cloud-backed folder each open blocks while the provider materialises the file (~220 notes at ~2 s each = 6m52s). -The scan itself is allowed to take that long — the file genuinely has to +The scan itself is allowed to take that long: the file genuinely has to come down. What broke was the reporting: the whole scan ran as one opaque ``asyncio.to_thread`` hop, so the cancel event was not read until the scan finished and the modal showed a static "Scanning sources…" with no @@ -62,7 +62,7 @@ async def test_cancel_during_scan_stops_before_indexing( # threading, not asyncio: this is set from inside the to_thread worker, # and asyncio.Event.set() is not thread-safe. Under asyncio debug mode the # loop's thread check raises inside the generator, which the runner turns - # into a scan error + cancelled — the test would then pass for the wrong + # into a scan error + cancelled: the test would then pass for the wrong # reason. slow_started = threading.Event() diff --git a/tests/test_indexer_skip_echo.py b/tests/test_indexer_skip_echo.py index 84ba0112..74babcaf 100644 --- a/tests/test_indexer_skip_echo.py +++ b/tests/test_indexer_skip_echo.py @@ -3,8 +3,8 @@ ``run_indexer`` printed ``[fnd skip …]`` to stderr unconditionally. Under the TUI that is the same terminal Textual owns, so a run with failures (one real corpus produced ~40 in a single collection) scribbles over the -rendered UI. The skip already reaches the user twice — as a ``file_error`` -event and in the failure log — so the print belongs to the CLI alone. +rendered UI. The skip already reaches the user twice, as a ``file_error`` +event and in the failure log, so the print belongs to the CLI alone. """ from __future__ import annotations diff --git a/tests/test_intent_line.py b/tests/test_intent_line.py index c222804b..8d61ef05 100644 --- a/tests/test_intent_line.py +++ b/tests/test_intent_line.py @@ -93,14 +93,14 @@ def test_make_snippet_prefers_intent_match() -> None: snippet_web_intent = _make_snippet(body, "performance", intent="web page load") # The intent-aware snippet should mention 'web page' or 'load'. assert "web page" in snippet_web_intent.lower() or "load" in snippet_web_intent.lower() - # The intent-less snippet just picks the first match — no constraint + # The intent-less snippet just picks the first match: no constraint # on its content beyond containing 'performance'. assert "performance" in snippet_no_intent.lower() def test_make_snippet_proximity_anchors_on_cooccurrence() -> None: """A proximity/phrase query must anchor the snippet where its terms - co-occur (the real match), not on an earlier lone-term hit — and it must + co-occur (the real match), not on an earlier lone-term hit, and it must see through the ``{N}`` / ``"…"~N`` / ``*`` DSL sigils that a raw split misses.""" from fnd.query import _make_snippet @@ -120,7 +120,7 @@ def test_make_snippet_falls_back_when_no_intent_match() -> None: from fnd.query import _make_snippet body = "Performance varies. Athletic performance is well-studied." - # Intent tokens won't appear in the body — fallback to first match. + # Intent tokens won't appear in the body: fallback to first match. snippet = _make_snippet(body, "performance", intent="quantum chromodynamics") assert "performance" in snippet.lower() @@ -128,7 +128,7 @@ def test_make_snippet_falls_back_when_no_intent_match() -> None: def test_make_snippet_sanitises_tabs_and_control_chars_at_source() -> None: """Extracted PDF body text carries tabs (exercise numbering: ``5.\\tExplain``) and stray controls. A snippet is a single-line display string, so the source - must strip them — the CLI and every label consumer inherit clean text.""" + must strip them: the CLI and every label consumer inherit clean text.""" from fnd.query import _make_snippet body = "of null. 5.\tExplain what is wrong\x07 with the hash table strategy" diff --git a/tests/test_invisible_text_tier.py b/tests/test_invisible_text_tier.py index 9d8640e1..f7de3bbf 100644 --- a/tests/test_invisible_text_tier.py +++ b/tests/test_invisible_text_tier.py @@ -83,7 +83,7 @@ def test_gate_skips_when_production_empty() -> None: def test_gate_skips_below_token_floor() -> None: - """Too few flat tokens to judge coverage — leave production alone.""" + """Too few flat tokens to judge coverage: leave production alone.""" calls: list[int] = [] flat = "one two three four five six seven eight nine ten" # 10 tokens < 20 out = _tier("xxx", calls).refine(_ctx(flat), PageExtraction(markdown="prod")) @@ -92,7 +92,7 @@ def test_gate_skips_below_token_floor() -> None: def test_gate_skips_when_coverage_above_floor() -> None: - """Born-digital: production already covers the flat layer — no fallback.""" + """Born-digital: production already covers the flat layer; no fallback.""" calls: list[int] = [] flat = _distinct_words(30) prod = flat # 100% coverage diff --git a/tests/test_keybindings_help_overlay.py b/tests/test_keybindings_help_overlay.py index e48ed57b..c9be4063 100644 --- a/tests/test_keybindings_help_overlay.py +++ b/tests/test_keybindings_help_overlay.py @@ -3,7 +3,7 @@ Items (6)-(8) from the app-routing branch fix queue: * (6) ``?`` pushes Keybindings ON TOP of the current screen, ``?`` again - or ``Esc`` pops back to that same screen — not the main app. + or ``Esc`` pops back to that same screen, not the main app. * (6) The hint resolved from the calling screen surfaces the matching section right after Global. * (7) The inner list body is a ``VerticalScroll`` so 30+ rows scroll. @@ -65,7 +65,7 @@ async def test_question_mark_from_source_form_returns_to_source_form( built_index: Path, fixtures_dir: Path ) -> None: """The whole point of the fix: ``?`` must not drop the SourceFormScreen - when toggling off — the user is in the middle of editing a source.""" + when toggling off: the user is in the middle of editing a source.""" app = FNDApp(index_dir=built_index, config=_seed_cfg(fixtures_dir)) async with app.run_test() as pilot: await pilot.pause() @@ -136,7 +136,7 @@ async def test_hint_section_class_lands_on_header_and_body_rows( ) -> None: """(8) The header for the resolved hint section and every body row under it (until the next header) carry the ``-hint-section`` CSS - class — that's the hook the CSS rule paints from.""" + class: that's the hook the CSS rule paints from.""" app = FNDApp(index_dir=built_index, config=_seed_cfg(fixtures_dir)) async with app.run_test() as pilot: await pilot.pause() @@ -194,7 +194,7 @@ async def test_no_hint_means_no_hint_section_class_anywhere( app = FNDApp(index_dir=built_index, config=_seed_cfg(fixtures_dir)) async with app.run_test() as pilot: await pilot.pause() - # Main app, default focus — _keybindings_context_hint returns None. + # Main app, default focus: _keybindings_context_hint returns None. # Drop focus explicitly so no pane wins. try: app.set_focus(None) diff --git a/tests/test_keybindings_os_vocabulary.py b/tests/test_keybindings_os_vocabulary.py index 48324096..5311e5a9 100644 --- a/tests/test_keybindings_os_vocabulary.py +++ b/tests/test_keybindings_os_vocabulary.py @@ -1,7 +1,7 @@ """The Keybindings page (and the footer hint bar) speak the running OS. Before this, the cheat sheet hardcoded macOS: "Reveal in Finder", the ⌥ glyph, -"hold Option", an Apple-Terminal tip, and a macOS-only Accessibility section — +"hold Option", an Apple-Terminal tip, and a macOS-only Accessibility section, all shown verbatim to Linux and Windows users after the cross-platform port. Help text carries ``{file_manager}`` / ``{alt_key}`` / ``{alt_word}`` @@ -66,7 +66,7 @@ def test_localise_is_a_no_op_without_placeholders(monkeypatch: pytest.MonkeyPatc def test_localise_leaves_unrelated_braces_alone(monkeypatch: pytest.MonkeyPatch) -> None: - """Query-syntax help mentions `{60}` proximity and `{path}` app templates — + """Query-syntax help mentions `{60}` proximity and `{path}` app templates: substitution must be literal token replacement, not str.format.""" _as(monkeypatch, "Darwin") text = "Proximity {60} and argv template {path} stay literal" @@ -185,7 +185,7 @@ def test_accessibility_context_hint_degrades_off_macos( def test_footer_hints_use_the_platform_modifier(monkeypatch: pytest.MonkeyPatch) -> None: - """The footer and the cheat sheet must agree — one renderer, one + """The footer and the cheat sheet must agree: one renderer, one vocabulary, so a Windows user isn't told to press ⌥.""" _as(monkeypatch, "Darwin") assert "⌥↑↓" in render_hint_bar((), (("{alt_key}↑↓", "Skip"),)).plain @@ -201,7 +201,7 @@ def test_footer_hint_labels_are_localised(monkeypatch: pytest.MonkeyPatch) -> No def test_results_footer_table_has_no_baked_in_glyph() -> None: - """The table must hold the placeholder, not ⌥ — otherwise the localise + """The table must hold the placeholder, not ⌥, otherwise the localise call in the renderer has nothing to work with.""" results = FNDApp._FOOTER_CONTEXTUAL["results"] keys = [k for k, _ in results] diff --git a/tests/test_keybindings_provider.py b/tests/test_keybindings_provider.py index 4c9371a8..a4d4339c 100644 --- a/tests/test_keybindings_provider.py +++ b/tests/test_keybindings_provider.py @@ -62,7 +62,7 @@ def test_pretty_key_substitutions_are_applied() -> None: def test_static_sections_present() -> None: """Widget bindings outside the action registry (Settings menu, source form, - Open-with modal) are listed too. The AX-modal section is macOS-only — see + Open-with modal) are listed too. The AX-modal section is macOS-only: see ``tests/test_keybindings_os_vocabulary.py`` for its per-platform gate.""" items = _provider_keybindings(_fake_app()) headers = _headers(items) @@ -106,7 +106,7 @@ def test_context_hint_none_keeps_default_order() -> None: headers = _headers(items) assert headers[0] == "Global" # When no hint is supplied, the order should follow the bucket - # declaration order — Results pane comes before Preview pane. + # declaration order: Results pane comes before Preview pane. if "Results pane" in headers and "Preview pane" in headers: assert headers.index("Results pane") < headers.index("Preview pane") @@ -119,7 +119,7 @@ def test_unknown_context_hint_falls_back_to_default_order() -> None: def test_context_hint_marks_matching_header_for_highlight() -> None: """The hint section's header carries the ``_hint_section_`` sentinel - in its ``keywords`` — SettingsList reads that to apply the + in its ``keywords``: SettingsList reads that to apply the ``-hint-section`` CSS class so the band paints with the accent border + bg. Without this marker the section just sits at the top of the list with no visual cue.""" @@ -136,7 +136,7 @@ def test_no_context_hint_means_no_highlighted_header() -> None: def test_every_key_row_label_differs_from_description() -> None: - """The DetailStrip surfaces ``description`` when a row is focused — + """The DetailStrip surfaces ``description`` when a row is focused, if label == description, the strip just echoes the row label and adds zero value. Every row must carry a short label AND a distinct long-form description.""" @@ -152,7 +152,7 @@ def test_every_key_row_label_differs_from_description() -> None: def test_every_key_row_has_a_description() -> None: """A row with no description leaves the DetailStrip empty when - focused — every keybinding should have a long-form explanation.""" + focused: every keybinding should have a long-form explanation.""" items = _provider_keybindings(_fake_app()) missing = [it.label for it in items if not it.is_header and not it.description] assert not missing, missing diff --git a/tests/test_last_match_landing.py b/tests/test_last_match_landing.py index 7bd2645f..ba92e3cd 100644 --- a/tests/test_last_match_landing.py +++ b/tests/test_last_match_landing.py @@ -2,7 +2,7 @@ The ordering trap this exists for: ``FNDMarkdown.match_blocks`` is registration order, and a table registers from ``compose`` (at mount) while every text block -registers pre-mount — so the table is always last, whatever follows it in the +registers pre-mount, so the table is always last, whatever follows it in the document. The rows are the only ordering that matches what the reader sees. """ @@ -48,7 +48,7 @@ async def test_the_last_stop_row_is_the_tail_paragraph_not_the_table() -> None: table = pilot.app.query_one(FNDMarkdownTableDT) table_row = table.region.y - md.region.y assert rows[-1] > table_row, ( - f"the last stop {rows[-1]} is at or above the table at row {table_row} — " + f"the last stop {rows[-1]} is at or above the table at row {table_row}: " "match_blocks ordering, not painted order" ) assert rows[-1] == max(rows), "rows are not sorted" @@ -56,7 +56,7 @@ async def test_the_last_stop_row_is_the_tail_paragraph_not_the_table() -> None: @pytest.mark.asyncio async def test_the_table_cell_is_one_of_the_stops() -> None: - """The table is not skipped for being out of registration order — its cell + """The table is not skipped for being out of registration order: its cell is a stop like any other, just not the last one here.""" async with _Harness().run_test(size=(80, 24)) as pilot: md = pilot.app.query_one(FNDMarkdown) @@ -159,8 +159,8 @@ async def test_a_last_match_landing_reaches_the_tail_paragraph_past_the_table() def test_a_landing_intent_serves_every_arm_of_its_own_navigation() -> None: - """A navigation arms more than once — the flat path arms again after the - structural one — so a read must not consume the request. It is dropped when + """A navigation arms more than once (the flat path arms again after the + structural one) so a read must not consume the request. It is dropped when a load for a DIFFERENT target starts, which is the navigation ending.""" from fnd.tui.preview.presenter import PreviewPresenter @@ -214,7 +214,7 @@ async def test_a_last_match_landing_waits_for_an_unresolved_table_cell() -> None async def test_two_tables_matching_the_same_cell_keep_both_stops() -> None: """Cell rows are per-table: two tables in one chunk can both match at the same local coordinate, and keying the stop set on the coordinate alone drops - the first table's row — a match n/b could then never reach.""" + the first table's row: a match n/b could then never reach.""" class _Harness(App[None]): def compose(self) -> ComposeResult: diff --git a/tests/test_launch_command_serialize.py b/tests/test_launch_command_serialize.py index 619d52a3..1ff5103d 100644 --- a/tests/test_launch_command_serialize.py +++ b/tests/test_launch_command_serialize.py @@ -1,6 +1,6 @@ """Serialising a search into a runnable ``fnd`` launch command. -Pure unit tests over ``SearchSnapshot`` — no app, no index. +Pure unit tests over ``SearchSnapshot``: no app, no index. """ from __future__ import annotations @@ -76,7 +76,7 @@ def test_tag_match_any_only_with_includes() -> None: _cmd(query="q", tag_include={"f": frozenset({"red"})}, tag_match_all=False) == "fnd q --tag red --tag-match any" ) - # match_all is a mode — meaningless (and omitted) without include tags. + # match_all is a mode: meaningless (and omitted) without include tags. assert _cmd(query="q", tag_match_all=False) == "fnd q" diff --git a/tests/test_launcher.py b/tests/test_launcher.py index 2779555e..97875210 100644 --- a/tests/test_launcher.py +++ b/tests/test_launcher.py @@ -1,7 +1,7 @@ """Per-platform unit tests for the OS launcher seam (``fnd.launcher``). Every branch is exercised by injecting a spy runner / ``startfile`` / ``which`` -into the concrete launcher — no process is ever spawned, so these run +into the concrete launcher: no process is ever spawned, so these run identically on any OS (the CI matrix then confirms the real spawns). """ diff --git a/tests/test_lazy_mount_on_scroll.py b/tests/test_lazy_mount_on_scroll.py index 65ef9f3a..39ca369a 100644 --- a/tests/test_lazy_mount_on_scroll.py +++ b/tests/test_lazy_mount_on_scroll.py @@ -49,7 +49,7 @@ def cfg(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Config: @pytest.fixture def long_md_index(tmp_path: Path, tmp_index_dir: Path) -> Path: - """A 120-section markdown file — large enough that one lazy-mount + """A 120-section markdown file: large enough that one lazy-mount batch can't fill from the visible window to either end of the document. Only section 50 mentions the search term so the auto- load lands deterministically.""" @@ -78,7 +78,7 @@ async def test_scroll_below_boundary_triggers_lazy_mount( ) -> None: """Scrolling close to the bottom of the mounted region mounts more chunks below it. Disable the active-file full-mount so the file stays - windowed — scroll-driven lazy-mount is the path for files beyond the + windowed: scroll-driven lazy-mount is the path for files beyond the full-mount budget (monster docs); for in-budget files the eager fill has already mounted everything below. Coverage is disabled too, so the chunks this mounts are built here rather than served from a capture.""" @@ -106,7 +106,7 @@ async def test_scroll_below_boundary_triggers_lazy_mount( max_before = max(mounted_before) assert max_before < container.total_chunks - 1, "test setup needs unmounted below" target = max(0, pane.virtual_size.height - pane.size.height) - # release() models a focused user scroll taking control — one of the two + # release() models a focused user scroll taking control: one of the two # ways the gate opens (the other is the nav settling, covered by # test_lazy_mount_fires_after_settle_without_explicit_release). app._preview_scroll.release() @@ -127,7 +127,7 @@ async def test_scroll_below_boundary_triggers_lazy_mount( ) mounted_after = set(container.mounted_indices) new_below = {i for i in mounted_after - mounted_before if i > max_before} - # Batch size is bounded — not unbounded fill. + # Batch size is bounded, not unbounded fill. assert len(new_below) <= LAZY_MOUNT_BATCH * 4 @@ -140,7 +140,7 @@ async def test_lazy_mount_fires_after_settle_without_explicit_release( The dead-end bug gated lazy-mount on the anchor being armed; the anchor stays armed across navs and only a *focused* user scroll called release(), so wheel-scrolling the unfocused pane never extended the window. Now the - gate is is_settling — it clears when the nav's scroll commits — so this + gate is is_settling (it clears when the nav's scroll commits) so this test does NOT call release() and lazy-mount must still fire. Full-mount disabled so the file stays windowed (the >budget monster-file @@ -166,7 +166,7 @@ async def test_lazy_mount_fires_after_settle_without_explicit_release( ) # The nav has landed: the gate must already be open, with NO release(). assert not app._preview_scroll.is_settling, ( - "nav settled but is_settling still True — lazy-mount would dead-end" + "nav settled but is_settling still True: lazy-mount would dead-end" ) container = app._preview.active assert container is not None @@ -221,7 +221,7 @@ async def test_scroll_above_after_settled_triggers_lazy_mount( assert min_initial > 0, ( f"test setup needs unmounted above; mounted={sorted(mounted_initial)}" ) - # Force scroll to the absolute top of the mounted region — that + # Force scroll to the absolute top of the mounted region: that # puts scroll_y inside the trigger margin so the watcher fires. # release() models a focused user scroll taking control (one of the two # ways the is_settling gate opens). @@ -244,7 +244,7 @@ async def test_far_in_file_nav_lands_in_fresh_contiguous_region( cfg: Config, long_md_index: Path ) -> None: """Navigating to a far-apart match in the same file lands on the new - match in a FRESH, contiguous mounted region — not a second window + match in a FRESH, contiguous mounted region, not a second window prepended above the current one. The old same-container resume left a gap between two regions and, on an upward jump, slid the visible content (the reflow). A fresh container is built below the outgoing one and @@ -260,7 +260,7 @@ async def test_far_in_file_nav_lands_in_fresh_contiguous_region( lambda: app._preview.active is not None and bool(app._preview.active.mounted_indices), message="active preview never mounted any chunks", ) - # Auto-load focuses on section 50. Jump far UP to section 10 — the + # Auto-load focuses on section 50. Jump far UP to section 10: the # case that used to prepend a window above the view and reflow. group = app._search.groups[0] app._preview.render_full_doc(group.parent_id, focus_chunk_seq=10) @@ -293,7 +293,7 @@ async def test_scroll_watcher_bridges_to_lazy_mounter( """``MatchAwareScroll.watch_scroll_y`` must actually reach the LazyMounter. The app-level delegation shim was removed and this watcher kept calling the dropped name, silently severing scroll- - driven lazy mount — undetected because the other tests drive + driven lazy mount: undetected because the other tests drive ``app._lazy.check()`` directly. This test exercises ONLY the bridge: a real scroll on the focused pane must invoke ``schedule_check``.""" monkeypatch.setattr("fnd.tui.preview.tuning.FULLMOUNT_CHUNK_BUDGET", 0) @@ -320,7 +320,7 @@ def _spy(*, user_initiated: bool = False) -> None: pane.focus() await pilot.pause() # A focused user scroll must bridge through to the mounter with - # user_initiated=True — NOT a direct check() call. + # user_initiated=True, NOT a direct check() call. pane.scroll_to(y=pane.scroll_y + 5, animate=False, immediate=True) await pilot.pause() assert calls, "watch_scroll_y never reached LazyMounter.schedule_check" @@ -332,7 +332,7 @@ async def test_file_switch_cancels_lazy_mount( cfg: Config, tmp_path: Path, tmp_index_dir: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """A pending lazy-mount task is dropped when the user navigates to - a different file — no stray mounts on the previous container.""" + a different file: no stray mounts on the previous container.""" cfg_path = tmp_path / "config.toml" cfg_path.write_text( textwrap.dedent(""" @@ -402,7 +402,7 @@ async def test_a_fully_warmed_file_fills_downward_and_never_prepends_unattended( Content inserted above the viewport shoves the reader down and the scroll can only re-pin it a layout later, so an unattended above-fill jitters for - as long as it runs — measured at 19 of 21 painted frames showing the wrong + as long as it runs: measured at 19 of 21 painted frames showing the wrong part of the document. Upward stays user-driven, one batch per scroll. """ app = FNDApp(index_dir=long_md_index, config=cfg, collection="notes") @@ -439,7 +439,7 @@ async def test_a_reader_scrolling_up_is_served_while_the_fill_runs( ) -> None: """The background fill must not wall the reader for its own duration. - It runs downward for seconds — 3.6s on a 727-chunk file — and upward is + It runs downward for seconds, 3.6s on a 727-chunk file, and upward is reachable only on demand, so standing the reader down until it finishes is the wall the windowed mount was built to remove. """ @@ -501,7 +501,7 @@ async def test_a_warmed_file_over_the_mount_ceiling_stays_windowed( assert len(container.mounted_indices) < len(chunks), ( "the ceiling did not hold: a file over it mounted whole" ) - # Only meaningful if a batch actually ran — if the scroll left the + # Only meaningful if a batch actually ran, if the scroll left the # viewport inside the mounted region no batch is spawned and a size # assertion would hold trivially. grew = len(container.mounted_indices) - len(before) @@ -587,7 +587,7 @@ def test_a_run_already_in_the_store_gets_the_served_batch() -> None: def test_a_run_that_must_be_built_gets_the_small_batch() -> None: """FULL is counted over CAPTURABLE chunks, so a mostly flat-path file reads - FULL with almost none of it in the store — and the served batch is ten times + FULL with almost none of it in the store, and the served batch is ten times what a build batch can afford.""" lazy, container, chunks = _stub_lazy(all_captured=False) assert lazy._served_batch(container, chunks, 50, -1) == tuning.LAZY_MOUNT_BATCH diff --git a/tests/test_legibility_tier.py b/tests/test_legibility_tier.py index ca6ec31b..9f364132 100644 --- a/tests/test_legibility_tier.py +++ b/tests/test_legibility_tier.py @@ -66,7 +66,7 @@ def test_legr_none_when_no_prose() -> None: def test_legr_abstains_when_system_dict_missing(monkeypatch: pytest.MonkeyPatch) -> None: """No injected dict + an absent system word list (Windows / minimal - container) must abstain (None), not score every page 0.0 — otherwise + container) must abstain (None), not score every page 0.0, otherwise the tier would reprocess all legible prose.""" from fnd.extract.recovery import evaluators diff --git a/tests/test_ligature_repair_tier.py b/tests/test_ligature_repair_tier.py index 590ff11a..a7e54338 100644 --- a/tests/test_ligature_repair_tier.py +++ b/tests/test_ligature_repair_tier.py @@ -3,7 +3,7 @@ The layout text engine emits U+FFFD for ligature glyphs (ff/fi/fl/ffi/ffl) in fonts whose ToUnicode CMap lacks entries for them. The flat text layer (MuPDF's standard engine) resolves the same glyphs, so it is used as -ground truth to rebuild only the words a ligature broke — standalone +ground truth to rebuild only the words a ligature broke: standalone U+FFFD (non-ligature unmapped glyphs) is left untouched. """ @@ -52,7 +52,7 @@ def test_noop_when_no_replacement_char() -> None: def test_leaves_standalone_replacement_char_untouched() -> None: """A bare U+FFFD (a non-ligature unmapped glyph) has no word skeleton - to match — leave it rather than inventing 'ff'/'fi'.""" + to match: leave it rather than inventing 'ff'/'fi'.""" r = LigatureRepairer() md = f"see {FFFD} here" assert r.repair(md, "see X here") == md @@ -60,21 +60,21 @@ def test_leaves_standalone_replacement_char_untouched() -> None: def test_bare_replacement_not_rewritten_even_when_vocab_has_ligature() -> None: """A standalone U+FFFD must stay put even when the flat layer contains - a bare 'ff'/'fi'/'fl' token — repairing only fires inside a word.""" + a bare 'ff'/'fi'/'fl' token: repairing only fires inside a word.""" r = LigatureRepairer() md = f"tempo {FFFD} marking ff dynamics" assert r.repair(md, "tempo X marking ff dynamics") == md def test_repair_preserves_capital_on_midword_ligature() -> None: - """Capitalised words with a mid-word ligature keep the capital — the + """Capitalised words with a mid-word ligature keep the capital: the real corpus case ('Miscongured', 'Condentiality').""" r = LigatureRepairer() assert r.repair(f"Miscon{FFFD}gured CAs", "a misconfigured ca") == "Misconfigured CAs" def test_repair_uppercases_ligature_in_all_caps_word() -> None: - """An all-caps token stays all-caps — the recovered ligature letters + """An all-caps token stays all-caps: the recovered ligature letters are not left lowercase mid-word.""" r = LigatureRepairer() assert r.repair(f"TRA{FFFD}C ALERT", "inbound traffic alert") == "TRAFFIC ALERT" @@ -89,7 +89,7 @@ def test_repair_capitalises_leading_ligature_from_ground_truth() -> None: def test_repair_handles_non_f_ligatures() -> None: """Not limited to f-ligatures: the standard 'st' and old-style 'ct' - ligatures (and the extended f-set) are recovered too — the repairer is + ligatures (and the extended f-set) are recovered too: the repairer is not hard-coded to this PDF's ff/fi/fl.""" r = LigatureRepairer() assert r.repair(f"the fir{FFFD} step", "the first step") == "the first step" # st @@ -98,14 +98,14 @@ def test_repair_handles_non_f_ligatures() -> None: def test_repair_disambiguates_ligature_gap_from_non_ligature_twin() -> None: """A skeleton twin whose gap is NOT a ligature is ignored: 'ows' - fits 'flows' (fl, a ligature) and 'allows' (all, not) — pick 'flows'.""" + fits 'flows' (fl, a ligature) and 'allows' (all, not): pick 'flows'.""" r = LigatureRepairer() assert r.repair(f"mail {FFFD}ows here", "flows and allows") == "mail flows here" def test_repair_leaves_ambiguous_skeleton_untouched() -> None: """When two flat words fit AND both gaps are real ligatures, refuse to - guess — 'uff' fits 'fluff' (fl) and 'stuff' (st).""" + guess: 'uff' fits 'fluff' (fl) and 'stuff' (st).""" r = LigatureRepairer() md = f"some {FFFD}uff here" assert r.repair(md, "fluff and stuff") == md @@ -120,7 +120,7 @@ def test_leaves_unmatched_word_untouched() -> None: def test_caps_pathological_multifffd_token() -> None: """A token with many U+FFFD slots would explode the candidate - product — leave it untouched past the cap rather than churn.""" + product: leave it untouched past the cap rather than churn.""" r = LigatureRepairer() md = "a" + FFFD * 8 + "b" assert r.repair(md, "anything here") == md @@ -152,7 +152,7 @@ def test_tier_passthrough_when_markdown_empty() -> None: def test_tier_keeps_current_when_nothing_repairable() -> None: - """U+FFFD present but unrepairable (standalone) — don't restamp the + """U+FFFD present but unrepairable (standalone): don't restamp the tier or set the flag for a no-op.""" tier = LigatureRepairTier(LigatureRepairer()) current = PageExtraction(markdown=f"a {FFFD} b", tier="production-layout") diff --git a/tests/test_line_buffer.py b/tests/test_line_buffer.py index 4515898d..88c5a7ce 100644 --- a/tests/test_line_buffer.py +++ b/tests/test_line_buffer.py @@ -2,8 +2,8 @@ The widget replaces ~250,000 ``Static`` widgets (one per line of a 1000-page PDF) with a single ``ScrollView`` subclass that owns a -``list[Strip]``. These tests exercise the widget in isolation — -no FNDApp coupling, no real Tantivy index — so they can pin down +``list[Strip]``. These tests exercise the widget in isolation ( +no FNDApp coupling, no real Tantivy index) so they can pin down the data model + rendering contract before the host app starts calling it. @@ -66,12 +66,12 @@ def test_build_file_view_match_spans_register_per_line() -> None: bolded, the whole line gets the subtle-accent style, and first_hit_line_in_chunk points at the first matched line in the chunk.""" - # Chunk "alpha\nbravo\ncharlie" — substring "bra" lives in line 1 + # Chunk "alpha\nbravo\ncharlie": substring "bra" lives in line 1 # at offsets 6..9 (b after 'alpha\n' which is 6 chars). fv = build_file_view([_chunk(7, "alpha\nbravo\ncharlie", (6, 9))]) assert fv.match_lines == {1} assert fv.first_hit_line_in_chunk == {7: 1} - # The matched line carries a bold span over (0, 3) — local offsets. + # The matched line carries a bold span over (0, 3): local offsets. bra_line = fv.lines[1] bold_spans = [s for s in bra_line.spans if "bold" in str(s.style).lower()] assert bold_spans, "expected a bold span over the matched substring" @@ -80,7 +80,7 @@ def test_build_file_view_match_spans_register_per_line() -> None: def test_build_file_view_first_hit_prefers_full_over_dimmed() -> None: """A proximity query dims lone-term occurrences (#76); the auto-scroll target must skip a dim-only line and land on the first FULL co-occurrence - match — otherwise a ``{N}`` query strands the preview on a faint stray hit.""" + match, otherwise a ``{N}`` query strands the preview on a faint stray hit.""" from fnd.render import DIM_MATCH_STYLES, MATCH_STYLES # "alpha\nbravo\ncharlie": alpha 0..5 (dim, line 0), charlie 12..19 (full, line 2). @@ -102,7 +102,7 @@ def test_build_file_view_first_hit_falls_back_to_dim_only() -> None: def test_build_file_view_handles_multiple_matches_on_same_line() -> None: """Two match spans on the same line both register; only one entry is added to ``match_lines`` (it's a set).""" - # "alpha bravo alpha" — two "alpha" matches at (0,5) and (12,17). + # "alpha bravo alpha": two "alpha" matches at (0,5) and (12,17). fv = build_file_view([_chunk(1, "alpha bravo alpha", (0, 5), (12, 17))]) assert fv.match_lines == {0} bold_spans = [s for s in fv.lines[0].spans if "bold" in str(s.style).lower()] @@ -176,7 +176,7 @@ def compose(self) -> ComposeResult: fv = build_file_view([_chunk(0, "apple\nbanana\ncherry")]) buf.set_file_view(fv) await pilot.pause() - # Render y=0 from the top — the scroll offset is 0, so y=0 maps + # Render y=0 from the top: the scroll offset is 0, so y=0 maps # to line 0. strip = buf.render_line(0) assert isinstance(strip, Strip) @@ -207,7 +207,7 @@ def compose(self) -> ComposeResult: assert int(buf.scroll_offset.y) == 50 buf.scroll_to_line(100, center=True) await pilot.pause() - # Centred — line 100 sits at viewport mid, so scroll y is line + # Centred: line 100 sits at viewport mid, so scroll y is line # 100 minus half the viewport height. assert int(buf.scroll_offset.y) == 100 - (buf.size.height // 2) @@ -238,7 +238,7 @@ def compose(self) -> ComposeResult: @pytest.mark.asyncio async def test_widget_scroll_to_chunk_prefers_first_match_line() -> None: """Clicking a section in the sidebar should land the user on the - matched line within the chunk, not the chunk's first line — this + matched line within the chunk, not the chunk's first line: this is feature 2 in the audit.""" from textual.app import App, ComposeResult @@ -266,7 +266,7 @@ def compose(self) -> ComposeResult: @pytest.mark.asyncio async def test_widget_focused_chunk_overlay_is_per_render() -> None: - """``set_focused_chunk`` doesn't rebuild Strips — focused-chunk + """``set_focused_chunk`` doesn't rebuild Strips: focused-chunk accent is applied at render time via the component class. The cached Strip array stays identical across focus changes (cheap to flip back and forth), and the row overlay style is non-None only @@ -315,7 +315,7 @@ def compose(self) -> ComposeResult: @pytest.mark.asyncio async def test_widget_match_lines_property_exposes_sorted_positions() -> None: - """``match_lines`` returns a sorted list of global line indices — + """``match_lines`` returns a sorted list of global line indices: the scrollbar renderer consumes this directly to paint markers.""" from textual.app import App, ComposeResult @@ -327,7 +327,7 @@ def compose(self) -> ComposeResult: async with app.run_test() as pilot: await pilot.pause() buf = app.query_one(LineBufferPreview) - # Two chunks each with one match — global lines 0 and 4 + # Two chunks each with one match: global lines 0 and 4 # (chunk2 = gap row + 2 chunk lines, match on second of those). fv = build_file_view( [ @@ -425,7 +425,7 @@ def _md_chunk(seq: int, md: str) -> FileChunk: def test_rendered_document_packs_strips_and_indexes() -> None: - """build_rendered_document is the prefetch-bundle constructor — + """build_rendered_document is the prefetch-bundle constructor: strips and per-row indexes must agree with what set_prebuilt_view consumes.""" fv = build_file_view([_chunk(0, "alpha\nbravo\ncharlie")]) @@ -454,7 +454,7 @@ def test_rendered_document_wraps_long_lines() -> None: @pytest.mark.asyncio async def test_set_prebuilt_view_consumes_rendered_document() -> None: """A widget installed with the RenderedDocument's fields paints the - expected line count — the value type is a faithful prebuilt bundle.""" + expected line count: the value type is a faithful prebuilt bundle.""" from textual.app import App, ComposeResult class _Host(App[None]): @@ -529,7 +529,7 @@ def compose(self) -> ComposeResult: @pytest.mark.asyncio async def test_widget_wrap_mode_match_lines_project_to_visual_rows() -> None: """In wrap mode ``match_lines`` returns the visual row indices that - contain a match — not the logical line indices. The scrollbar's + contain a match, not the logical line indices. The scrollbar's line-precise math expects visual offsets so this is the right interpretation.""" from textual.app import App, ComposeResult @@ -551,7 +551,7 @@ def compose(self) -> ComposeResult: fv = build_file_view([_chunk(5, body, (match_start, match_start + 5))]) buf.set_file_view(fv) await pilot.pause() - # The logical match line is 1 — but in wrap mode the visual + # The logical match line is 1, but in wrap mode the visual # row index is past the wrap of line 0. assert buf.match_lines == [buf._logical_to_visual_y(1)] @@ -588,7 +588,7 @@ def compose(self) -> ComposeResult: @pytest.mark.asyncio async def test_widget_wrap_mode_get_selection_uses_visual_rows() -> None: """Drag-select across wrapped visual rows extracts the wrapped - plain text — not the logical-line-indexed text the no-wrap path + plain text, not the logical-line-indexed text the no-wrap path used to assume.""" from textual.app import App, ComposeResult diff --git a/tests/test_markdown_callout_render.py b/tests/test_markdown_callout_render.py index 95c6dd53..22dd4942 100644 --- a/tests/test_markdown_callout_render.py +++ b/tests/test_markdown_callout_render.py @@ -137,7 +137,7 @@ async def test_every_callout_type_is_tinted_apart_from_plain_blockquotes() -> No assert sampled.has_class("callout-example") assert not plain.has_class("callout") # $boost resolves fully transparent here, so a callout that inherits it - # renders with no fill — every callout type must carry its own tint. + # renders with no fill: every callout type must carry its own tint. assert plain.styles.background.a == 0 assert quoted.styles.background.a > 0 assert sampled.styles.background.a > 0 diff --git a/tests/test_markdown_callouts.py b/tests/test_markdown_callouts.py index b755dbed..a291929f 100644 --- a/tests/test_markdown_callouts.py +++ b/tests/test_markdown_callouts.py @@ -82,7 +82,7 @@ def test_nested_callouts_are_each_retagged() -> None: def test_title_keeps_inline_markup_children() -> None: - # Token .content joins child text only — the ** markup is carried by the + # Token .content joins child text only: the ** markup is carried by the # strong_open/strong_close children, not by any child's content. tokens = _tokens("> [!tip] A **bold** title\n> Body.\n") assert _inlines(tokens)[0] == "A bold title" diff --git a/tests/test_markdown_heading_marker.py b/tests/test_markdown_heading_marker.py index a2630e55..9be98e91 100644 --- a/tests/test_markdown_heading_marker.py +++ b/tests/test_markdown_heading_marker.py @@ -85,7 +85,7 @@ async def test_every_heading_level_uses_accent_color() -> None: # after alpha blend; the other five are flat $accent. if cls is not FNDMarkdownH6: t = color_seen - # Tokyo-night-ish accent — peach (254, 166, 43); guard + # Tokyo-night-ish accent: peach (254, 166, 43); guard # against the default Textual palette regression where H4-H6 # silently fell back to $text and lost colour entirely. assert (t.red, t.green, t.blue) != (224, 224, 224), cls.__name__ @@ -110,7 +110,7 @@ async def test_bold_carries_contrasting_color_not_just_bold_weight() -> None: t = bold_seg.style.color.triplet assert t is not None # Body text colour for the rest of the paragraph; bold must not - # share that triplet — i.e. .strong picked up a contrasting + # share that triplet: i.e. .strong picked up a contrasting # colour from $primary, not just text-style: bold. body_seg = next( (s for s in strip if s.text.startswith("A line with ") and s.style), @@ -153,7 +153,7 @@ async def test_italic_also_carries_contrasting_color() -> None: @pytest.mark.asyncio async def test_heading_marker_does_not_break_highlight_offsets() -> None: - """A query term inside a heading still highlights the right word — + """A query term inside a heading still highlights the right word: the marker prefix shifts the plain content but the highlight span is computed against the post-prefix plain, so offsets stay aligned. """ diff --git a/tests/test_match_evidence.py b/tests/test_match_evidence.py index 0ffd7b51..aad62fe0 100644 --- a/tests/test_match_evidence.py +++ b/tests/test_match_evidence.py @@ -2,7 +2,7 @@ The load-bearing property is what it does NOT do: it never filters. The engine's match is what makes a result a result; if the highlighter can't paint it, the -row is marked, never withheld — otherwise a highlighting bug would silently +row is marked, never withheld, otherwise a highlighting bug would silently subtract matches the user never learns they missed. """ @@ -81,7 +81,7 @@ def test_reports_visible_when_the_rendered_substrate_matches() -> None: def test_stem_variants_count_as_visible() -> None: """The highlighter paints "testing" for a query of "test", so evidence - must agree — otherwise every stemmed hit would be marked unlocatable.""" + must agree, otherwise every stemmed hit would be marked unlocatable.""" spec = MatchSpec.from_query("test") assert has_paintable_match(_chunk(body_md="Unit testing matters"), spec) is True @@ -105,7 +105,7 @@ def test_auto_fuzzy_near_misses_are_not_evidence() -> None: def test_an_explicit_fuzzy_request_is_evidence() -> None: - """``~N`` is the user asking for fuzzy, so a fuzzy hit IS their match — + """``~N`` is the user asking for fuzzy, so a fuzzy hit IS their match: the evidence spec drops only the AUTO safety net, never an explicit one.""" chunk = _chunk(body_md="D. Encryption of the text") evidence = MatchSpec.from_query("test~1", auto_fuzzy=False) @@ -137,7 +137,7 @@ def test_module_exposes_no_filter() -> None: def test_a_fuzzy_pass_hit_is_judged_by_what_gets_painted() -> None: """A hit the FUZZY pass produced exists because of fuzzy, so a fuzzy - highlight is its evidence — marking it unlocatable would flag every result + highlight is its evidence: marking it unlocatable would flag every result of a typo search. Hits from any other pass must show a real match.""" from fnd.tui.match_evidence import FUZZY_PASS_INDEX, evidence_spec_for_pass diff --git a/tests/test_match_nav_cross_section.py b/tests/test_match_nav_cross_section.py index 49635d04..d362ea70 100644 --- a/tests/test_match_nav_cross_section.py +++ b/tests/test_match_nav_cross_section.py @@ -127,7 +127,7 @@ def _nav_pressing(sections: list[int], *, chunk_has_stop: bool, stops: list[int] def test_a_press_before_the_layout_resolves_waits_rather_than_handing_over() -> None: """The chunk's stops are not measurable yet (mid-mount) but its match data is - there — the press must wait for the layout, not leave the section.""" + there: the press must wait for the layout, not leave the section.""" nav, taken = _nav_pressing([3, 7], chunk_has_stop=True, stops=[]) nav.next() assert taken == [] @@ -143,7 +143,7 @@ def test_a_section_whose_match_cannot_be_reached_hands_over() -> None: def test_a_press_before_the_chunk_mounts_waits_rather_than_handing_over() -> None: """The window rebuild() zeroes the count for: no widget for the focus chunk - and no cached count. Absence of evidence is not a dead end — wait.""" + and no cached count. Absence of evidence is not a dead end: wait.""" nav, taken = _nav_pressing([3, 7], chunk_has_stop=False, stops=[], mounted=False, count=0) nav.next() assert taken == [] @@ -159,7 +159,7 @@ def test_a_press_during_a_cross_file_navigation_does_not_hand_over() -> None: def test_a_press_while_the_chunk_is_still_building_waits() -> None: """Blocks register during the build, so an empty match set there means "not - yet" — the wide window, seconds on a big chunk.""" + yet": the wide window, seconds on a big chunk.""" nav, taken = _nav_pressing([3, 7], chunk_has_stop=False, stops=[], building=True) nav.next() assert taken == [] diff --git a/tests/test_match_nav_geometry.py b/tests/test_match_nav_geometry.py index 68e92a32..eb2ad44e 100644 --- a/tests/test_match_nav_geometry.py +++ b/tests/test_match_nav_geometry.py @@ -12,7 +12,7 @@ from fnd.tui.match_navigator import step_view, view_anchors VH = 20 -# stops at content-y 5, 8, 40, 45, 90 — three views: [5,25) [40,60) [90,110) +# stops at content-y 5, 8, 40, 45, 90: three views: [5,25) [40,60) [90,110) YS = [5, 8, 40, 45, 90] ANCHORS = [5, 40, 90] @@ -102,7 +102,7 @@ def test_b_retraces_exactly_the_viewports_n_visited() -> None: def test_one_press_moves_one_view_at_every_viewport_size() -> None: """A stride that differs between the two directions only shows up at some - sizes — the 12-stop fence at vh=40 is where it first did.""" + sizes: the 12-stop fence at vh=40 is where it first did.""" for vh in range(8, 61, 4): anchors = view_anchors(FENCE, vh) if len(anchors) < 3: @@ -120,6 +120,6 @@ def test_a_wrap_onto_a_visible_view_is_no_navigation() -> None: def test_a_wrap_onto_an_off_screen_view_still_wraps() -> None: - """The wrap itself is not what was wrong — only wrapping onto a visible view.""" + """The wrap itself is not what was wrong: only wrapping onto a visible view.""" assert step_view([5, 90], [5, 90], 90, 20, forward=True) == 5 assert step_view([5, 90], [5, 90], 5, 20, forward=False) == 90 diff --git a/tests/test_match_nav_indicator.py b/tests/test_match_nav_indicator.py index 256d3b12..5bcf49d8 100644 --- a/tests/test_match_nav_indicator.py +++ b/tests/test_match_nav_indicator.py @@ -2,7 +2,7 @@ * the ``n/b`` KEY hint lives in the footer keybinding area (like every key), and * the ``▲a ▼b`` off-screen VIEW markers live on the preview pane's bottom border - (``border_subtitle``) — but only when the current result has matches beyond + (``border_subtitle``), but only when the current result has matches beyond the viewport. A short result whose matches all fit on screen shows no markers. """ @@ -71,7 +71,7 @@ async def test_footer_hint_shows_and_short_result_has_no_view_markers( timeout=30.0, message="match-nav count never populated", ) - # This result is a few lines — every match fits on screen, so the border + # This result is a few lines: every match fits on screen, so the border # carries no off-screen view markers (nothing hidden to announce). pane = app.query_one("#preview_pane", MatchAwareScroll) subtitle = str(pane.border_subtitle or "") @@ -84,7 +84,7 @@ async def test_footer_hint_shows_and_short_result_has_no_view_markers( ) # Reading View: markers gone (border dropped) AND the n/b footer hint - # gone too — the keys are inert there, so advertising them misleads. + # gone too: the keys are inert there, so advertising them misleads. app.action_toggle_reading_mode() await pilot.pause() rv_subtitle = str(app.query_one("#preview_pane").border_subtitle or "") diff --git a/tests/test_match_nav_integration.py b/tests/test_match_nav_integration.py index d4a93228..cc0b6336 100644 --- a/tests/test_match_nav_integration.py +++ b/tests/test_match_nav_integration.py @@ -1,5 +1,5 @@ """End-to-end: n/b navigate between the two CRC matches in a flashcards -table taller than the viewport, in the real FNDApp — including reaching the +table taller than the viewport, in the real FNDApp, including reaching the second match cell that sits below the fold in a multi-section document. The reported bug: after the multi-chunk preview settled, the navigator held a @@ -63,7 +63,7 @@ def flashcards_index(tmp_path: Path, tmp_index_dir: Path) -> Path: """Two ADJACENT matching results: the flashcards table (two CRC cells, taller than the viewport) and a short paragraph (one CRC). Focusing the table mounts both, so the paragraph's match is a mounted stop that the table's scoped - ``n``/``b`` must exclude — the scoping guarantee, testable without relying on + ``n``/``b`` must exclude: the scoping guarantee, testable without relying on a distant chunk background-mounting.""" a = tmp_path / "notes" body = ( @@ -82,7 +82,7 @@ def _current_stop_count(app: FNDApp) -> int: ``_chunk_stops`` falls back to the UNSCOPED set when the chunk extent is unknown, so a bare count stops the walk on a transient match between two - different sets — and the extent assertion below it then fails.""" + different sets, and the extent assertion below it then fails.""" pane = app.query_one("#preview_pane", VerticalScroll) nav = app._match_nav if nav._current_chunk_extent(pane) is None: @@ -114,7 +114,7 @@ async def _walk_to_stop_count(pilot: Pilot[None], app: FNDApp, want: int, key: s @pytest.fixture def table_result_index(tmp_path: Path, tmp_index_dir: Path) -> Path: - """Same multi-section file, but only the flashcards table matches — so the + """Same multi-section file, but only the flashcards table matches, so the table IS the loaded result (the reported scenario). Card 32 is revealed on load; card 47's cell sits below the fold within the same result.""" a = tmp_path / "notes" @@ -230,7 +230,7 @@ async def test_n_reveals_second_table_match_below_the_fold( cfg: Config, table_result_index: Path ) -> None: """When the table IS the current result, n reveals its off-screen second - match cell — the reported bug, at the geometry level (cell truly visible).""" + match cell: the reported bug, at the geometry level (cell truly visible).""" app = FNDApp(index_dir=table_result_index, config=cfg, collection="notes", initial_query="CRC") async with app.run_test(size=(110, 24)) as pilot: await pilot.pause() @@ -242,7 +242,7 @@ async def test_n_reveals_second_table_match_below_the_fold( # settles low with nothing to re-fire it during a passive wait. Re-run # rebuild() each poll so the count re-samples the current subtree and # reflects the table the moment it finishes composing (rebuild is the same - # idempotent call the app makes on search/mount — no product change). + # idempotent call the app makes on search/mount: no product change). def _enumerated() -> bool: if app._match_nav.count >= 2: return True @@ -259,7 +259,7 @@ def _enumerated() -> bool: dt = next(t for t in pane.query(DataTable) if getattr(t, "_fnd_match_coords", [])) card47 = Coordinate(46, 1) - # Card 47 starts below the fold — unreachable without navigation. + # Card 47 starts below the fold: unreachable without navigation. assert not _cell_visible(pane, dt, card47), "card 47 unexpectedly already visible" for _ in range(app._match_nav.count + 1): @@ -289,11 +289,11 @@ async def test_n_never_scrolls_a_neighbours_match_under_the_current_result( pane = app.query_one("#preview_pane", VerticalScroll) # Scoping is active (the table's chunk extent resolved) and captures both - # of the table's matches — no more, no less. + # of the table's matches: no more, no less. assert nav._current_chunk_extent(pane) is not None, "chunk extent did not resolve" assert len(nav._chunk_stops(pane)) == 2, "scoped stops should be the table's two matches" # The adjacent Summary result's match is mounted too, so the UNSCOPED set - # is larger — proving the scope is actively excluding another result. + # is larger: proving the scope is actively excluding another result. # That neighbour arrives on the background fill, so it is waited for: # asserting it straight away tested how fast the runner mounts. await wait_until( @@ -306,7 +306,7 @@ async def test_n_never_scrolls_a_neighbours_match_under_the_current_result( table_seq = _focus_seq(app) for _ in range(5): # `_go` early-returns when the chunk's stops are not resolvable yet, - # which leaves `_last_rel` unset — so wait for the scope BEFORE + # which leaves `_last_rel` unset, so wait for the scope BEFORE # pressing. Gating after the press would make the assertion about # the layout's timing rather than about n. await wait_until( @@ -450,7 +450,7 @@ async def test_a_hand_over_lands_even_after_an_option_scan( cfg: Config, flashcards_index: Path ) -> None: """Only the tree's key handler clears scan mode, so the hand-over clears it - itself — the preview pane has focus for n/b just as often.""" + itself: the preview pane has focus for n/b just as often.""" app = FNDApp(index_dir=flashcards_index, config=cfg, collection="notes", initial_query="CRC") async with app.run_test(size=(110, 24)) as pilot: await pilot.pause() @@ -468,7 +468,7 @@ async def test_a_hand_over_lands_even_after_an_option_scan( @pytest.fixture def flat_index(tmp_path: Path, tmp_index_dir: Path) -> Path: - """A .txt file — no chunk is markdown-rendered, so the preview takes the + """A .txt file: no chunk is markdown-rendered, so the preview takes the FLAT path, which nulls ``preview.active`` and keeps its own buffer.""" a = tmp_path / "notes" filler = "".join(f"filler line {i} of no interest at all.\n" for i in range(60)) @@ -536,7 +536,7 @@ async def test_a_hand_over_onto_the_row_the_cursor_already_holds_still_lands( assert _focus_seq(app) == start, "the scan loaded the row it moved onto" # b at the landing has nothing above it inside this chunk, so it hands - # over — onto the row the cursor already holds. + # over: onto the row the cursor already holds. app.action_nav_prev_match() await wait_until( diff --git a/tests/test_match_nav_measure_scheduling.py b/tests/test_match_nav_measure_scheduling.py index 6be66931..daf6e623 100644 --- a/tests/test_match_nav_measure_scheduling.py +++ b/tests/test_match_nav_measure_scheduling.py @@ -137,7 +137,7 @@ def test_a_rebuild_clears_a_swallowed_request_too() -> None: @pytest.mark.parametrize("rounds", [1, 3, 7]) def test_the_scheduler_always_settles(rounds: int) -> None: - """Landing every poll must terminate — a follow-up that re-armed itself + """Landing every poll must terminate: a follow-up that re-armed itself would spin the event loop for as long as the preview is open.""" nav = _Nav() for _ in range(rounds): @@ -153,7 +153,7 @@ def test_the_scheduler_always_settles(rounds: int) -> None: def test_a_late_reflow_is_caught_by_a_confirmation_pass() -> None: """The counts come from LAYOUT, but every trigger is a scroll or a mount, so a reflow that lands after the last scroll would otherwise leave the border - describing a layout that is gone — with nothing pending to notice.""" + describing a layout that is gone, with nothing pending to notice.""" nav = _Nav() nav._open_confirmation_window() nav.readings = [(0, 0), (0, 1)] @@ -166,7 +166,7 @@ def test_a_late_reflow_is_caught_by_a_confirmation_pass() -> None: def test_a_confirmation_skips_a_settling_navigation() -> None: - """A confirmation must never read regions while a navigation is landing — + """A confirmation must never read regions while a navigation is landing: that is the one path the surrounding code requires be left clear.""" nav = _Nav() nav._open_confirmation_window() @@ -205,7 +205,7 @@ def test_a_rebuild_cancels_pending_confirmations() -> None: def test_the_window_keeps_re_measuring_until_it_closes() -> None: - """Agreement between two samples is not evidence the layout has stopped — + """Agreement between two samples is not evidence the layout has stopped: it has been wrong that way twice. The window is what stops the loop.""" nav = _Nav() nav._open_confirmation_window() @@ -233,7 +233,7 @@ def test_the_window_closes() -> None: def test_a_rebuild_leaves_the_new_navigation_with_a_chain() -> None: """A chain armed under the previous generation is dead on arrival, and a - bare "a chain exists" guard cannot say so — it just declines to arm the one + bare "a chain exists" guard cannot say so: it just declines to arm the one the new navigation needs, leaving an open window with nothing running.""" nav = _Nav() nav._open_confirmation_window() # generation 0 arms a chain @@ -276,7 +276,7 @@ def boom() -> None: def test_a_chunk_that_composes_late_still_gets_a_count() -> None: """``_count_tick``'s ladder is three refreshes and then stops if the count is still 0. A chunk that composes after that left the count stuck at 0 with - nothing pending — a real Windows failure, 30s of polling against + nothing pending: a real Windows failure, 30s of polling against `chunk_stops=[3, 5, 7]` that the navigator never counted.""" nav = _Nav() nav._open_confirmation_window() @@ -291,7 +291,7 @@ def test_a_chunk_that_composes_late_still_gets_a_count() -> None: def test_the_recount_stops_once_the_count_is_real() -> None: - """The walk is only paid while the count reads zero — a window ticking 40 + """The walk is only paid while the count reads zero: a window ticking 40 times must not re-walk the subtree once it has an answer.""" nav = _Nav() nav.stops = 2 diff --git a/tests/test_match_nav_offscreen_indicator.py b/tests/test_match_nav_offscreen_indicator.py index c3ad9ccb..17249478 100644 --- a/tests/test_match_nav_offscreen_indicator.py +++ b/tests/test_match_nav_offscreen_indicator.py @@ -20,7 +20,7 @@ # A CRC table taller than the viewport: CRC in card 32's answer and card 86's # question, ~50 rows apart so the second is well below the fold on load (the -# viewport can't hold both — the original bug's shape). +# viewport can't hold both; the original bug's shape). TALL_TABLE_MD = ( "# Flashcards\n\n| # | Q | A |\n| --- | --- | --- |\n" + "".join(f"| {i} | filler question {i} | filler answer {i} |\n" for i in range(1, 32)) @@ -74,7 +74,7 @@ async def test_offscreen_match_announced_and_flips_on_nav(cfg: Config, tall_inde await pilot.pause() app.query_one("#results_pane", Tree).focus() # On load the viewport lands on the first CRC; the second is a screenful - # below in the SAME result — the awareness signal the original bug lacked. + # below in the SAME result: the awareness signal the original bug lacked. await wait_until( pilot, lambda: app._match_nav.below >= 1, diff --git a/tests/test_match_nav_switch_staleness.py b/tests/test_match_nav_switch_staleness.py index 835983bf..3fd0ce05 100644 --- a/tests/test_match_nav_switch_staleness.py +++ b/tests/test_match_nav_switch_staleness.py @@ -1,4 +1,4 @@ -"""Switching results must refresh the ▲/▼ view markers immediately — even for +"""Switching results must refresh the ▲/▼ view markers immediately, even for a warm nav whose reveal doesn't move the scroll. The bug: markers were only re-measured on scroll, so moving from a multi-view result to a single-view one left the old ▼ lingering (and a multi-view result sometimes showed nothing @@ -130,7 +130,7 @@ async def test_the_markers_never_describe_a_layout_that_is_gone( cfg: Config, two_result_index: Path ) -> None: """The counts are a cache of a LAYOUT read, refreshed only by scroll and - mount events — so a reflow landing after the last scroll can strand it. + mount events, so a reflow landing after the last scroll can strand it. Asserted as the invariant rather than as a particular arrow: whatever the border says must be what a fresh measurement of the current viewport says. diff --git a/tests/test_match_navigator.py b/tests/test_match_navigator.py index b486c183..5d6a1f63 100644 --- a/tests/test_match_navigator.py +++ b/tests/test_match_navigator.py @@ -94,7 +94,7 @@ def test_next_advances_by_viewport_and_wraps() -> None: def test_a_stop_inside_a_view_is_shown_not_skipped() -> None: # vh=24 tiles [0,30,49,60] into views at 0, 30 and 60. 49 gets no view of - # its own because the view at 30 spans [30,54) and already shows it — the + # its own because the view at 30 spans [30,54) and already shows it: the # hop must not stop twice on one screenful, nor skip 49 out of sight. nav = _nav([0, 30, 49, 60], vh=24) nav.next() @@ -178,7 +178,7 @@ def test_a_frozen_focused_chunk_still_reports_its_stops() -> None: Serving a capture replaces the chunk's widget tree with a ``FrozenChunkView`` and pops its match target. ``enumerate_stop_rows`` handles that view, so - ``n``/``b`` keep working — but without a matching branch here the plain-chunk + ``n``/``b`` keep working, but without a matching branch here the plain-chunk fallback reads the popped target, returns False, and the app hides the ``n/b Matches`` hint on a chunk where both keys work. """ @@ -233,7 +233,7 @@ def test_a_view_past_the_last_screenful_merges_into_it() -> None: scrolling nowhere.""" # vh=20, document 120 rows → max_scroll_y=100. The view at 105 is past the # furthest the pane can scroll, so the clamped viewport [100,120) is what - # shows it — and 118 shares that view. + # shows it, and 118 shares that view. nav = _nav([0, 105, 118], vh=20, virtual_height=120) nav.next() @@ -280,7 +280,7 @@ def _extent_nav(widgets: dict[int, _StubWidget], seq: int, virtual_height: int = def test_an_unlaid_chunk_scopes_to_nothing_not_to_everything() -> None: """The background fill hides every above-window chunk. Returning None there means "no per-chunk widgets", which sends _chunk_stops to its whole-preview - fallback — and that is how n/b walked out of the current result.""" + fallback, and that is how n/b walked out of the current result.""" nav, pane = _extent_nav({5: _StubWidget(0, 0), 6: _StubWidget(120, 40)}, seq=5) assert nav._current_chunk_extent(pane) == (0, 0) # type: ignore[arg-type] diff --git a/tests/test_match_row_within_block.py b/tests/test_match_row_within_block.py index ae4f468a..5aee97dc 100644 --- a/tests/test_match_row_within_block.py +++ b/tests/test_match_row_within_block.py @@ -1,7 +1,7 @@ """A block's match row is counted in RENDERED rows, not source newlines. A wrapped block renders many rows from one source line, so a newline count -reports 0 and the anchor lands on the block's top — measured on a real PDF +reports 0 and the anchor lands on the block's top: measured on a real PDF contents page, a 63-row paragraph whose match paints on row 32. """ @@ -78,7 +78,7 @@ def test_padding_above_the_content_shifts_the_row() -> None: def test_highlight_spans_win_over_a_spec_scan() -> None: - """The spans are what the user can SEE, so they decide the row — the scan is + """The spans are what the user can SEE, so they decide the row: the scan is only the fallback for a block that carries none.""" from textual.content import Span diff --git a/tests/test_match_stop_rows.py b/tests/test_match_stop_rows.py index f8ec5fa1..f0f483f4 100644 --- a/tests/test_match_stop_rows.py +++ b/tests/test_match_stop_rows.py @@ -13,7 +13,7 @@ from fnd.tui.widgets.markdown import FNDMarkdown # A table ~4x the viewport height with CRC in row 32's answer and row 47's -# question — far apart, second cell well below the fold. +# question: far apart, second cell well below the fold. TABLE_MD = ( "| # | Q | A |\n| --- | --- | --- |\n" + "".join( @@ -51,7 +51,7 @@ async def on_mount(self) -> None: assert rows[0] < rows[1] -# One source line wrapping over several screenfuls, its match late in it — a PDF +# One source line wrapping over several screenfuls, its match late in it: a PDF # contents page's shape, and the case where a block's top is nowhere near it. WRAPPED_MD = "# Contents\n\n" + " ".join( f"Step {i:02d} - a deployment guide entry dotted to a page number" for i in range(90) @@ -96,7 +96,7 @@ async def on_mount(self) -> None: ) -# One fence taller than the viewport carrying two matches far apart — the shape +# One fence taller than the viewport carrying two matches far apart: the shape # of a long code block, where every match after the first used to be unreachable. FENCE_LINES = [f" filler_value_{i} = {i}" for i in range(120)] FENCE_LINES[10] = " first = quartzfin(1)" diff --git a/tests/test_matching_caches.py b/tests/test_matching_caches.py index de2d261f..945f0c90 100644 --- a/tests/test_matching_caches.py +++ b/tests/test_matching_caches.py @@ -2,7 +2,7 @@ enough to allow it. Proving a highlight is visible word-matches every listed result before the first -paint — measured at 130,298 ``word_matches`` calls on a real corpus — and each +paint (measured at 130,298 ``word_matches`` calls on a real corpus) and each one reached the Snowball stemmer. Re-measured in-process on that path, cache live against ``__wrapped__``: 85.5ms -> 10.1ms over 551 hit texts, at 23,156 hits against 1,896 misses. @@ -62,7 +62,7 @@ def test_caching_does_not_change_glob_translation() -> None: def test_the_stem_cache_is_actually_used() -> None: """The point of the exercise: a repeated word must not re-enter the stemmer. - Asserted on the cache itself rather than on elapsed time — a timing + Asserted on the cache itself rather than on elapsed time: a timing threshold would be flaky on a loaded machine and would not say WHY it got slower. """ diff --git a/tests/test_md_line_tracking.py b/tests/test_md_line_tracking.py index 91cb90b4..3db5088b 100644 --- a/tests/test_md_line_tracking.py +++ b/tests/test_md_line_tracking.py @@ -66,10 +66,10 @@ def test_hit_line_round_trips_through_index(tmp_path: Path) -> None: def test_hit_line_zero_for_kinds_without_line_tracking(tmp_path: Path) -> None: - """PDF / DOCX / PPTX chunks set ``line=0`` — they have their own + """PDF / DOCX / PPTX chunks set ``line=0``: they have their own locators (page, slide, heading_path) and CLI deep-links for those formats don't accept a line anyway.""" - # Use the existing fixture PDF — easiest non-MD non-TXT corpus. + # Use the existing fixture PDF: easiest non-MD non-TXT corpus. fixtures = Path("tests/fixtures") if not fixtures.is_dir(): return diff --git a/tests/test_meta_blob.py b/tests/test_meta_blob.py index 22d94854..0486b593 100644 --- a/tests/test_meta_blob.py +++ b/tests/test_meta_blob.py @@ -34,7 +34,7 @@ def test_list_roundtrip() -> None: def test_date_roundtrip() -> None: """Dates must round-trip as ``datetime.date`` so the DSL evaluator's - ordered comparisons (`<=`, `>=`) work — strings can't be compared + ordered comparisons (`<=`, `>=`) work: strings can't be compared against dates and silently fail closed.""" fm = {"due": dt.date(2026, 6, 1)} out = decode(encode(fm)) diff --git a/tests/test_migrate.py b/tests/test_migrate.py index d767d914..d5a0a7fc 100644 --- a/tests/test_migrate.py +++ b/tests/test_migrate.py @@ -1,4 +1,4 @@ -"""Schema-migration helpers — detection + rebuild prompt.""" +"""Schema-migration helpers: detection + rebuild prompt.""" from __future__ import annotations diff --git a/tests/test_number_synonyms.py b/tests/test_number_synonyms.py index ca96ebf7..c2eea584 100644 --- a/tests/test_number_synonyms.py +++ b/tests/test_number_synonyms.py @@ -1,6 +1,6 @@ """Number<->word synonym groups (cardinals + ordinals). -`4` matches `four`, `1st` matches `first`, bidirectionally — but a number +`4` matches `four`, `1st` matches `first`, bidirectionally, but a number inside a user's quoted phrase stays literal (the exact-match request wins). """ diff --git a/tests/test_open_actions_and_footer_labels.py b/tests/test_open_actions_and_footer_labels.py index 4f060de1..14991009 100644 --- a/tests/test_open_actions_and_footer_labels.py +++ b/tests/test_open_actions_and_footer_labels.py @@ -231,7 +231,7 @@ async def test_chunk_widgets_rebuild_when_focus_moves_to_different_file( ) assert app._preview.parent_id is not None assert app._preview.parent_id != first_pid, "preview did not rebuild for file 2" - # Stage 1c: a single shared LineBufferPreview is reused across files — + # Stage 1c: a single shared LineBufferPreview is reused across files, # what swaps is the installed RenderedDocument (and therefore file_view). second_buf = app._flat.active_buffer assert second_buf is not None @@ -245,7 +245,7 @@ async def test_chunk_widgets_rebuild_when_focus_moves_to_different_file( def test_footer_labels_are_unique() -> None: - """No two visible footer entries share a label — fixes the original + """No two visible footer entries share a label: fixes the original 'two toggles, two opens' duplication.""" labels = [a.footer_label for a in REGISTRY if a.show_in_footer and a.footer_label] assert len(labels) == len(set(labels)), f"duplicate footer labels: {labels}" diff --git a/tests/test_open_with_screen.py b/tests/test_open_with_screen.py index 1a3d0e6a..69f4719d 100644 --- a/tests/test_open_with_screen.py +++ b/tests/test_open_with_screen.py @@ -48,7 +48,7 @@ def test_eligible_apps_for_md_lists_md_capable() -> None: "system": True, "obsidian": True, "vscode": True, - "skim": True, # not eligible — handles only pdf + "skim": True, # not eligible: handles only pdf }, ) ids = [r.id for r in rows] @@ -78,7 +78,7 @@ def test_letter_shortcuts_unique_first_letters() -> None: keys = letter_shortcuts(rows) assert keys["skim"] == "s" assert keys["preview"] == "p" - # System Default — 's' taken by skim, walk to next letter. + # System Default: 's' taken by skim, walk to next letter. assert keys["system"] != "s" assert keys["system"] != "p" @@ -262,7 +262,7 @@ async def on_mount(self) -> None: assert len(captured) == 1 argv = captured[0] - # Whichever app sits below skim must have fired — NOT skim. + # Whichever app sits below skim must have fired, NOT skim. assert not (argv[0] == "open" and argv[1].startswith("skim://")), ( f"expected ↓+Enter to fire a non-skim app; got {argv}" ) diff --git a/tests/test_opener.py b/tests/test_opener.py index 59a25152..76fc328a 100644 --- a/tests/test_opener.py +++ b/tests/test_opener.py @@ -11,7 +11,7 @@ # Skim is a macOS-only PDF viewer and its skim:/// deep-link is only ever built # on Darwin (open_smart gates the promotion behind sys.platform == "darwin"). -# The URL is an absolute-path scheme, so its shape is platform-specific — pin +# The URL is an absolute-path scheme, so its shape is platform-specific: pin # these to macOS rather than reason about Windows drive-letter encoding. mac_only = pytest.mark.skipif(sys.platform != "darwin", reason="Skim is macOS-only") @@ -122,7 +122,7 @@ def test_open_smart_falls_through_to_system_when_no_skim_no_ax( from fnd import apps as apps_mod apps_mod._reset_ax_cache() - # macOS no-Skim/no-AX path — pin the platform so a Linux/Windows box with a + # macOS no-Skim/no-AX path: pin the platform so a Linux/Windows box with a # page-jump viewer installed doesn't auto-promote it and bypass the seam. monkeypatch.setattr("sys.platform", "darwin") monkeypatch.setattr(opener, "_has_skim", lambda: False) diff --git a/tests/test_opener_injection.py b/tests/test_opener_injection.py index a8bea9a0..0047ff3b 100644 --- a/tests/test_opener_injection.py +++ b/tests/test_opener_injection.py @@ -1,6 +1,6 @@ """Filename-injection regression tests for the Skim open path. -The previous AppleScript opener escaped only ``\\`` and ``"`` — a filename +The previous AppleScript opener escaped only ``\\`` and ``"``: a filename containing a newline could break out of the AppleScript string literal and inject arbitrary ``osascript`` commands. The URL form goes through ``urllib.parse.quote`` and reaches ``open`` as an argv element, so every @@ -20,7 +20,7 @@ # Skim is macOS-only; its skim:/// deep-link (and the NUL/adversarial-filename # hardening around it) is only built on Darwin. Windows also differs at the path -# layer — resolve() does not reject an embedded NUL the way POSIX does. +# layer: resolve() does not reject an embedded NUL the way POSIX does. mac_only = pytest.mark.skipif(sys.platform != "darwin", reason="Skim is macOS-only") @@ -41,7 +41,7 @@ ], ) def test_skim_url_neutralises_adversarial_filenames(tmp_path: Path, evil_name: str) -> None: - # No `.touch()` — we don't need the file to exist on disk. `resolve()` + # No `.touch()`: we don't need the file to exist on disk. `resolve()` # works on absent paths and the security claim is purely about # encoding, not filesystem state. (pytest's tmp-dir slugifier mangles # the parametrized name for the dir, so creating a file with the @@ -49,7 +49,7 @@ def test_skim_url_neutralises_adversarial_filenames(tmp_path: Path, evil_name: s f = tmp_path / evil_name url = opener.skim_url(f, page=1) - # No raw control characters survive into the URL — anything that could + # No raw control characters survive into the URL: anything that could # break out of an AppleScript string literal or a shell argv element # is percent-encoded. for forbidden in ("\n", "\r", "\t", '"', "\\"): @@ -66,7 +66,7 @@ def test_skim_url_neutralises_adversarial_filenames(tmp_path: Path, evil_name: s @mac_only def test_skim_url_rejects_nul_byte_via_path_resolution(tmp_path: Path) -> None: - """NUL in a Path raises before ``skim_url`` ever runs — Python's path + """NUL in a Path raises before ``skim_url`` ever runs: Python's path layer refuses to embed it, which is the right outcome (NUL would truncate an argv element on the C side of ``open(2)``).""" with pytest.raises(ValueError, match=r"null byte|embedded null"): @@ -87,7 +87,7 @@ def test_open_smart_dispatches_to_url_for_adversarial_filename( # path runs on any CI runner. monkeypatch.setattr("sys.platform", "darwin") monkeypatch.setattr(opener, "_has_skim", lambda: True) - # Isolate from the user's real config — without this the test routes + # Isolate from the user's real config, without this the test routes # through whichever ``[app_defaults]`` the developer happens to have set. from fnd.config import Config diff --git a/tests/test_os_labels.py b/tests/test_os_labels.py index 640a186a..2fbcf2f1 100644 --- a/tests/test_os_labels.py +++ b/tests/test_os_labels.py @@ -74,7 +74,7 @@ def test_is_macos_only_true_on_darwin(monkeypatch: pytest.MonkeyPatch) -> None: def test_alt_modifier_is_the_option_glyph_on_macos(monkeypatch: pytest.MonkeyPatch) -> None: - """macOS keyboards label the key ⌥, not 'Alt' — the keybindings page and + """macOS keyboards label the key ⌥, not 'Alt': the keybindings page and the footer both render it, so the vocabulary lives in one place.""" _as(monkeypatch, "Darwin") assert os_labels.modifier_label("alt") == "⌥" @@ -93,7 +93,7 @@ def test_alt_word_is_option_on_macos(monkeypatch: pytest.MonkeyPatch) -> None: def test_shared_modifiers_are_stable_across_platforms(monkeypatch: pytest.MonkeyPatch) -> None: - """Ctrl and Shift are spelled the same everywhere — only ⌥/⌘ differ, so a + """Ctrl and Shift are spelled the same everywhere: only ⌥/⌘ differ, so a platform switch must not churn the rest of the key column.""" for system in ("Darwin", "Windows", "Linux"): _as(monkeypatch, system) @@ -119,7 +119,7 @@ def test_modifier_label_returns_none_for_non_modifiers() -> None: def test_no_caching_so_the_platform_seam_stays_patchable( monkeypatch: pytest.MonkeyPatch, ) -> None: - """Unlike ``launcher.get_launcher``, these are uncached pure functions — + """Unlike ``launcher.get_launcher``, these are uncached pure functions: a test (or a same-process platform patch) sees the change with no cache_clear() bookkeeping.""" _as(monkeypatch, "Darwin") diff --git a/tests/test_paths.py b/tests/test_paths.py index a0475cc4..a1d42faf 100644 --- a/tests/test_paths.py +++ b/tests/test_paths.py @@ -17,7 +17,7 @@ def test_app_dirs_pass_appauthor_false(monkeypatch: pytest.MonkeyPatch) -> None: - """Both roots must call platformdirs with ``appauthor=False`` — the + """Both roots must call platformdirs with ``appauthor=False``: the single fact that keeps Windows app data from splitting.""" calls: dict[str, tuple[str, object]] = {} @@ -86,7 +86,7 @@ def test_config_reexports_the_same_data_root() -> None: def test_uv_tool_root_falls_back_when_uv_absent(monkeypatch: pytest.MonkeyPatch) -> None: - """When ``uv tool dir`` can't run, fall back to the platform default — + """When ``uv tool dir`` can't run, fall back to the platform default, which ends in ``uv/tools`` on every OS (POSIX XDG / Windows APPDATA).""" def boom(*_a: object, **_k: object) -> object: @@ -118,7 +118,7 @@ def boom(*_a: object, **_k: object) -> object: def test_helpers_create_nothing() -> None: - """Pure path computation — no directory is created as a side effect.""" + """Pure path computation: no directory is created as a side effect.""" assert not paths.reindex_state_dir().exists() or paths.reindex_state_dir().is_dir() # Calling a helper must not create its parent. p = paths.dismissed_dir() diff --git a/tests/test_pdf_docling_daemon.py b/tests/test_pdf_docling_daemon.py index 495ced91..a96ef1d2 100644 --- a/tests/test_pdf_docling_daemon.py +++ b/tests/test_pdf_docling_daemon.py @@ -50,7 +50,7 @@ def test_daemon_singleton_returns_none_when_docling_missing( def test_daemon_singleton_reuses_one_instance( monkeypatch: pytest.MonkeyPatch, ) -> None: - """F10: repeated .get() calls return the same singleton — model + """F10: repeated .get() calls return the same singleton, model loads exactly once per process.""" from fnd.extract import _docling_daemon @@ -93,7 +93,7 @@ def test_fallback_returns_empty_when_daemon_unavailable( monkeypatch: pytest.MonkeyPatch, ) -> None: """NF4: when DoclingDaemon.get() returns None (docling missing), - _try_docling_fallback returns "" — caller keeps pymupdf4llm output.""" + _try_docling_fallback returns "": caller keeps pymupdf4llm output.""" from fnd.extract import _docling_daemon, pdf monkeypatch.setattr(_docling_daemon, "_docling_python", lambda: None) @@ -138,7 +138,7 @@ def test_fallback_returns_daemon_output_on_success( def test_needs_docling_fallback_triggers_on_table_label_proximity() -> None: """The HBR p99 case: small (sub-15%) picture region but a TABLE - label nearby — should trigger fallback.""" + label nearby: should trigger fallback.""" from fnd.extract import pdf class FakePage: diff --git a/tests/test_pdf_extract_structured.py b/tests/test_pdf_extract_structured.py index eb7145e6..858dab6d 100644 --- a/tests/test_pdf_extract_structured.py +++ b/tests/test_pdf_extract_structured.py @@ -3,7 +3,7 @@ Requirements covered: - F5: With extras installed, PDF chunks have populated body_md. -These tests are skipped when pymupdf4llm isn't available — i.e., they +These tests are skipped when pymupdf4llm isn't available: i.e., they run only after `uv sync --extra pdf-structure` (or the future `fnd extras install pdf-structure`). CI without the extra still passes because the tests are gated by pytest.importorskip. @@ -18,11 +18,11 @@ _pymupdf4llm = pytest.importorskip("pymupdf4llm") # Partial uninstalls leave pymupdf4llm importable but missing -# ``to_markdown`` — the extra is effectively broken in that state. +# ``to_markdown``: the extra is effectively broken in that state. # Skip rather than fail; full reinstall fixes it. if not hasattr(_pymupdf4llm, "to_markdown"): pytest.skip( - "pymupdf4llm is importable but has no `to_markdown` — " + "pymupdf4llm is importable but has no `to_markdown`: " "looks like a partial install. Run `fnd extras install " "pdf-structure` to restore.", allow_module_level=True, @@ -36,7 +36,7 @@ def test_body_md_populated_when_extra_present() -> None: from fnd.extract import pdf assert pdf._HAS_PYMUPDF4LLM, ( - "extras present but _HAS_PYMUPDF4LLM is False — module detection bug" + "extras present but _HAS_PYMUPDF4LLM is False: module detection bug" ) chunks = list(pdf.extract(FIXTURE)) assert chunks, "extract() must yield chunks" @@ -57,7 +57,7 @@ def test_body_md_is_markdown_with_headings() -> None: def test_body_struct_still_populated_flat() -> None: """F1 invariant: even with extras installed, body_struct keeps its - plain-text Block shape — the snippet pipeline reads from there and + plain-text Block shape: the snippet pipeline reads from there and must not see Markdown markers.""" from fnd.extract import pdf @@ -66,7 +66,7 @@ def test_body_struct_still_populated_flat() -> None: for c in chunks: assert c.body_struct, "body_struct should still be populated" for block in c.body_struct: - # No Markdown markers in body_struct — those go in body_md. + # No Markdown markers in body_struct: those go in body_md. assert "**" not in block.text, ( f"bold marker leaked into body_struct: {block.text[:80]!r}" ) diff --git a/tests/test_pdf_extras_optional.py b/tests/test_pdf_extras_optional.py index 0cb7e14e..ea4c32cc 100644 --- a/tests/test_pdf_extras_optional.py +++ b/tests/test_pdf_extras_optional.py @@ -81,5 +81,5 @@ def test_optional_extractors_not_imported_eagerly(modname: str) -> None: sys.modules.pop(k, None) importlib.import_module("fnd.extract.pdf") assert modname not in sys.modules, ( - f"{modname} was imported at fnd.extract.pdf import time — should be lazy" + f"{modname} was imported at fnd.extract.pdf import time: should be lazy" ) diff --git a/tests/test_pdf_page_label.py b/tests/test_pdf_page_label.py index d1de9a65..032bda12 100644 --- a/tests/test_pdf_page_label.py +++ b/tests/test_pdf_page_label.py @@ -3,7 +3,7 @@ Books with prefatory pages (TOC, preface) typically label them in roman numerals so the displayed locator matches what's actually printed on the page, while ``page`` (PDF index) is what Skim needs -for deep-linking. The two diverge — that's the bug this guards +for deep-linking. The two diverge: that's the bug this guards against. """ @@ -36,7 +36,7 @@ def _build_labelled_pdf(path: Path) -> None: def _build_unlabelled_pdf(path: Path) -> None: - """Plain 3-page PDF without page labels — extractor should leave + """Plain 3-page PDF without page labels: extractor should leave ``page_label`` empty so display falls back to the PDF index.""" doc = pymupdf.open() for n in range(3): @@ -97,7 +97,7 @@ class _C: def _build_pdf_with_printed_numbers(path: Path, *, body_count: int, body_start: int) -> None: """A PDF whose body pages print ``body_start..body_start+N-1`` in - the bottom margin — the typical typeset-book layout — and whose + the bottom margin (the typical typeset-book layout) and whose front matter has no margin number at all (mimicking unlabeled front matter you'd find in most real books). @@ -172,7 +172,7 @@ def test_cross_page_sequence_picks_page_number_over_chapter_number(tmp_path: Pat """Real-book layout: a header that combines chapter number with the running page number. Per-page rules can't tell which integer is which, but the cross-page resolver picks the one that ticks - up by 1 across consecutive pages — that's the page number, not + up by 1 across consecutive pages: that's the page number, not the (constant) chapter number.""" pdf = tmp_path / "with-headers.pdf" doc = pymupdf.open() @@ -196,7 +196,7 @@ def test_cross_page_sequence_picks_page_number_over_chapter_number(tmp_path: Pat def test_cross_page_sequence_rejects_short_runs(tmp_path: Path) -> None: - """Two pages with sequential margin numbers isn't enough — could + """Two pages with sequential margin numbers isn't enough: could be coincidence (e.g. cross-references of ``page 5 / page 6``). Require at least 3 consecutive pages to commit.""" pdf = tmp_path / "short-run.pdf" @@ -217,7 +217,7 @@ def test_cross_page_sequence_rejects_short_runs(tmp_path: Path) -> None: def test_page_label_survives_layered_search_pipeline(tmp_path: Path, tmp_index_dir: Path) -> None: """Regression: every Hit-reconstruction site (rerank, fusion, cascade) - must propagate ``page_label`` — otherwise the result-tree locator + must propagate ``page_label``, otherwise the result-tree locator falls back to the PDF index even though the index has the right label stored. This used to silently drop the field at four sites.""" from fnd.index import build_index @@ -229,26 +229,26 @@ def test_page_label_survives_layered_search_pipeline(tmp_path: Path, tmp_index_d build_index(roots=[tmp_path], index_dir=tmp_index_dir, collection="t") s = Searcher(index_dir=tmp_index_dir) - # The fixture has bodies "body 0".."body 4" — query for the unique + # The fixture has bodies "body 0".."body 4": query for the unique # body text so we get a stable hit. groups, _ = search_layered( s, query="body", limit=5, sections_per_file=5, collection="t", with_trace=True ) assert groups, "expected at least one matching group" labels = [h.page_label for g in groups for h in g.hits] - # Fixture pages: i, ii, 1, 2, 3 — none should be empty after the + # Fixture pages: i, ii, 1, 2, 3, none should be empty after the # layered pipeline reconstructs Hits. assert all(lbl != "" for lbl in labels), f"page_label dropped somewhere: {labels}" def test_meta_label_takes_precedence_over_margin_scan(tmp_path: Path) -> None: - """If the PDF declares an explicit page label, trust it — that's + """If the PDF declares an explicit page label, trust it: that's the authoritative source. Margin scan only runs as a fallback.""" pdf = tmp_path / "explicit.pdf" doc = pymupdf.open() for i in range(3): page = doc.new_page(width=612, height=792) - # Margin shows a *different* number — make sure metadata wins. + # Margin shows a *different* number: make sure metadata wins. page.insert_text((300, 770), str(900 + i), fontsize=10, fontname="helv") page.insert_text((72, 200), "body", fontsize=12, fontname="helv") doc.set_page_labels([{"startpage": 0, "prefix": "", "style": "D", "firstpagenum": 7}]) @@ -256,5 +256,5 @@ def test_meta_label_takes_precedence_over_margin_scan(tmp_path: Path) -> None: doc.close() chunks = list(extract(pdf)) - # Meta says 7,8,9 — that's what we should see, not 900,901,902. + # Meta says 7,8,9: that's what we should see, not 900,901,902. assert [c.page_label for c in chunks] == ["7", "8", "9"] diff --git a/tests/test_pdf_splice.py b/tests/test_pdf_splice.py index 8bd39042..7036b50d 100644 --- a/tests/test_pdf_splice.py +++ b/tests/test_pdf_splice.py @@ -1,7 +1,7 @@ """Splice docling's recovered tables into pymupdf4llm's formatted page. The fallback used to replace the whole page with docling output, losing -pymupdf4llm's bold/italic/headings — and on figure pages (no table) +pymupdf4llm's bold/italic/headings, and on figure pages (no table) gaining nothing. The splice keeps the formatted page and drops tables in at the marker site, with three behaviours measured on the real corpus: no-table keeps pymupdf4llm, clean 1:1 splices, ambiguous count falls @@ -48,7 +48,7 @@ def test_splice_keeps_formatting_and_inserts_table() -> None: def test_splice_keeps_pymupdf_when_docling_finds_no_table() -> None: - """The marker is a genuine figure/chart — docling recovers no table. + """The marker is a genuine figure/chart: docling recovers no table. Keep pymupdf4llm verbatim (formatting preserved); never replace with table-less docling output for no gain.""" pymupdf_md = ( @@ -64,7 +64,7 @@ def test_splice_keeps_pymupdf_when_docling_finds_no_table() -> None: def test_splice_keeps_pymupdf_when_it_already_has_a_table() -> None: """pymupdf4llm already rendered a vector table on the page but also emitted a redundant picture marker (the same table as an image). - docling recovers no MISSED table here — splicing its copy would + docling recovers no MISSED table here: splicing its copy would duplicate. Keep pymupdf4llm (table + formatting), no duplicate.""" pymupdf_md = ( "## **Acids**\n\n" diff --git a/tests/test_perms.py b/tests/test_perms.py index 4c4e9f8e..b1b25dcf 100644 --- a/tests/test_perms.py +++ b/tests/test_perms.py @@ -18,7 +18,7 @@ # The 0o700/0o600 hardening is POSIX-only; os.chmod merely toggles the read-only # bit on Windows (the user-profile data dir is already ACL-scoped there). So we -# skip only the mode-bit assertions on Windows — the functional behaviour +# skip only the mode-bit assertions on Windows: the functional behaviour # (creation, atomic replace, idempotence) still runs and is asserted, giving # _perms real Windows coverage. See fnd/_perms.py for the platform note. _POSIX_MODES = sys.platform != "win32" diff --git a/tests/test_phrase_matching.py b/tests/test_phrase_matching.py index 9307c137..0151e726 100644 --- a/tests/test_phrase_matching.py +++ b/tests/test_phrase_matching.py @@ -1,7 +1,7 @@ """Quoted phrases highlight as a contiguous span, not word-by-word. A quoted query like ``"defence in depth"`` must: -* highlight only the contiguous phrase occurrence(s) — so a stopword in +* highlight only the contiguous phrase occurrence(s), so a stopword in the phrase (``in``) is NOT lit up everywhere in the document; * still highlight loose (unquoted) terms word-by-word. """ @@ -26,7 +26,7 @@ def test_pure_phrase_excludes_words_from_doc_wide_highlight() -> None: def test_quoted_underscore_identifier_stays_a_phrase_not_loose_terms() -> None: # The index analyser's split (DOC_WORD_RE) makes a quoted underscore identifier a # multi-token phrase. Its parts must NOT leak into the loose (doc-wide) term - # set — otherwise quoting "recursive_directory_iterator" would light up + # set, otherwise quoting "recursive_directory_iterator" would light up # 'recursive'/'directory'/'iterator' everywhere, breaking the phrase contract. spec = MatchSpec.from_query('"recursive_directory_iterator"', auto_fuzzy=False) assert spec.phrases # recorded as a contiguous phrase diff --git a/tests/test_pilot_wait_helpers.py b/tests/test_pilot_wait_helpers.py index dcc305a6..f82d3e40 100644 --- a/tests/test_pilot_wait_helpers.py +++ b/tests/test_pilot_wait_helpers.py @@ -1,6 +1,6 @@ """The wait helpers every timing test rests on, tested without an app. -Driven by a stand-in Pilot whose ``pause`` is free — the shape ``safe_pause`` +Driven by a stand-in Pilot whose ``pause`` is free: the shape ``safe_pause`` degrades to when Textual's internal screen wait times out on a loaded runner. """ diff --git a/tests/test_pizza_highlight.py b/tests/test_pizza_highlight.py index 782461fd..3d21afc0 100644 --- a/tests/test_pizza_highlight.py +++ b/tests/test_pizza_highlight.py @@ -29,10 +29,10 @@ def test_case_difference_alone_is_all_yellow() -> None: def test_substitution_paints_substituted_char_orange() -> None: - """Query 'pizza' on doc 'pizzs' — last 'a' replaced with 's'.""" + """Query 'pizza' on doc 'pizzs': last 'a' replaced with 's'.""" spec = MatchSpec.from_query("pizza", auto_fuzzy=True) # Aligning "pizzs" vs "pizza": - # p-p, i-i, z-z, z-z, then 's' vs 'a' (substitution) — orange. + # p-p, i-i, z-z, z-z, then 's' vs 'a' (substitution): orange. runs = word_highlight_runs("pizzs", spec) assert runs == [ (0, 4, HIGHLIGHT_STYLE), diff --git a/tests/test_plain_chunk_fnd_text_type.py b/tests/test_plain_chunk_fnd_text_type.py index 6ef73b7e..b3ee63b2 100644 --- a/tests/test_plain_chunk_fnd_text_type.py +++ b/tests/test_plain_chunk_fnd_text_type.py @@ -4,7 +4,7 @@ and stashed that same ``Text`` on ``line.fnd_text``. Match counting (``MatchNavigator._count_stops``) and stop-region scanning (``enumerate_stop_rows``) then feed ``fnd_text`` to ``text_has_any_match``, -which runs ``DOC_WORD_RE.finditer`` over it — ``re`` needs a ``str``, so a +which runs ``DOC_WORD_RE.finditer`` over it: ``re`` needs a ``str``, so a rich ``Text`` raised ``TypeError: expected string or bytes-like object, got 'Text'`` and crashed the query (e.g. a PDF's "SPAIN" line while searching ``bastardo``). diff --git a/tests/test_pr_review_fixes.py b/tests/test_pr_review_fixes.py index 76d1cf08..10c1fad0 100644 --- a/tests/test_pr_review_fixes.py +++ b/tests/test_pr_review_fixes.py @@ -4,7 +4,7 @@ back without breaking a guard: * (#1) Obsidian ``file_in_vault`` is computed relative to the actual - vault root, not the source root — fixes deep-link breakage when a + vault root, not the source root: fixes deep-link breakage when a source is configured as a subdirectory of the vault. * (#4) KIND_ACTION rows with empty ``action_id`` (widget-only doc rows in the keybindings cheat sheet) don't dismiss the settings @@ -37,7 +37,7 @@ ) from tests._pilot_wait import settings_ready -# ── #1 — vault-relative file_in_vault ────────────────────────────────────── +# ── #1: vault-relative file_in_vault ────────────────────────────────────── def test_detect_obsidian_vault_path_returns_root_path(tmp_path: Path) -> None: @@ -68,7 +68,7 @@ def test_open_smart_computes_file_in_vault_relative_to_vault_not_source( """The PR #4 bug: when source.path is a subdirectory of the vault (e.g. an Obsidian collection that indexes only one project's notes folder), ``file_in_vault`` was computed relative - to source.path — Advanced URI would then look for the file at + to source.path: Advanced URI would then look for the file at the wrong location in the vault.""" from types import SimpleNamespace @@ -95,7 +95,7 @@ def fake_handler(req: OpenRequest) -> int: return 0 # Stub the obsidian handler to capture the OpenRequest that - # ``open_smart`` builds — we're testing OpenRequest assembly here, + # ``open_smart`` builds: we're testing OpenRequest assembly here, # not URL rendering. monkeypatch.setitem( BUILTIN_APPS, @@ -127,7 +127,7 @@ def fake_handler(req: OpenRequest) -> int: assert req.file_in_vault == "Notes/Budget/ch1.md", req.file_in_vault -# ── #4 — KIND_ACTION + empty action_id is a no-op ────────────────────────── +# ── #4: KIND_ACTION + empty action_id is a no-op ────────────────────────── @pytest.mark.asyncio @@ -161,13 +161,13 @@ async def test_enter_on_widget_only_row_does_not_close_settings_stack() -> None: lst.focus() await pilot.press("enter") await pilot.pause() - # Stack must still have Keybindings on top — user is reading + # Stack must still have Keybindings on top: user is reading # help, hitting Enter on a doc row shouldn't dismiss anything. assert isinstance(app.screen, SettingsScreen) assert app.screen._breadcrumb == ("Keybindings",) -# ── #5 — per-source App picker filters unavailable ──────────────────────── +# ── #5, per-source App picker filters unavailable ──────────────────────── @pytest.mark.asyncio @@ -176,7 +176,7 @@ async def test_per_source_app_picker_filters_unavailable_apps( ) -> None: """Make Skim 'unavailable' and confirm the per-source App picker doesn't list it (matches the Preferences picker behaviour). System - stays — it's always available.""" + stays: it's always available.""" from fnd.config import CollectionConfig, Config, SourceConfig from fnd.tui import FNDApp from fnd.tui.settings_screen import SourceFormScreen @@ -196,7 +196,7 @@ async def test_per_source_app_picker_filters_unavailable_apps( assert "system" in ids -# ── #6 — isinstance check for SourceFormScreen ──────────────────────────── +# ── #6: isinstance check for SourceFormScreen ──────────────────────────── def test_keybindings_context_hint_uses_isinstance_not_string_check() -> None: @@ -214,7 +214,7 @@ def test_keybindings_context_hint_uses_isinstance_not_string_check() -> None: assert '"SourceFormScreen" in type(' not in src -# ── #7 — keybinding row ids are unique ──────────────────────────────────── +# ── #7: keybinding row ids are unique ──────────────────────────────────── def test_keybinding_row_ids_are_unique() -> None: @@ -234,13 +234,13 @@ def test_keybinding_row_ids_are_unique() -> None: assert not dupes, dupes -# ── #3 — user template validation at load ───────────────────────────────── +# ── #3: user template validation at load ───────────────────────────────── def test_load_user_apps_rejects_unknown_placeholder_in_argv() -> None: """A typo'd placeholder in an argv template (e.g. {ptha}) must surface at config load as a clean ValueError naming the bad - placeholder — not at open time as an opaque KeyError.""" + placeholder, not at open time as an opaque KeyError.""" cfg = { "myapp": { "display_name": "MyApp", diff --git a/tests/test_present_kinds_scope.py b/tests/test_present_kinds_scope.py index ec9c0e5b..300beb3c 100644 --- a/tests/test_present_kinds_scope.py +++ b/tests/test_present_kinds_scope.py @@ -1,5 +1,5 @@ -"""present_kinds must scope by the full active scope — full collections AND the -active sources of partially-selected collections — so the file-type filter never +"""present_kinds must scope by the full active scope: full collections AND the +active sources of partially-selected collections, so the file-type filter never reveals kinds from unselected sources of the same collection.""" from __future__ import annotations diff --git a/tests/test_preview_cache_eviction.py b/tests/test_preview_cache_eviction.py index 45fde348..c9b46739 100644 --- a/tests/test_preview_cache_eviction.py +++ b/tests/test_preview_cache_eviction.py @@ -4,7 +4,7 @@ strand (data-reproduced under overshoot-and-return navigation at ~0.25s cadence): With ``PREVIEW_CACHE_MAX_FILES == 1`` the cache holds one container. A mount that -a rapid nav CANCELS runs its ``finally`` LATE — after a newer navigation has +a rapid nav CANCELS runs its ``finally`` LATE: after a newer navigation has already re-activated a *different* container. That late ``finally`` called ``put(its_container, protect=its_container)``, which evicted the now-active container out of the DOM while ``self.active`` still pointed at it: a detached, diff --git a/tests/test_preview_condemned_container.py b/tests/test_preview_condemned_container.py index 1b273f54..59880828 100644 --- a/tests/test_preview_condemned_container.py +++ b/tests/test_preview_condemned_container.py @@ -2,7 +2,7 @@ ``Widget.remove()`` is asynchronous. ``App._prune`` posts a ``Prune`` message and flags ``_pruning``; the widget keeps a live ``.parent`` and still turns up in -``app.query()`` until that message is processed — and when it is, ``on_prune`` +``app.query()`` until that message is processed, and when it is, ``on_prune`` closes the widget's message loop and detaches it. So between the two there is a window where a container looks perfectly healthy but is doomed. @@ -10,7 +10,7 @@ detached"), which is blind to that window. The consequence, reproduced by ``dev/tools/preview_blank_fuzz.py``: ``dispatch_mount`` sweeps stranded containers with ``.remove()`` and then, a few lines later in the same -synchronous block, re-scans the DOM for an adoptable container — and adopts the +synchronous block, re-scans the DOM for an adoptable container, and adopts the very one it just condemned. Because that container still reports a parent, the mount skips its ``pane.mount()`` and simply builds into it and makes it ``self.active``; the queued ``Prune`` then detaches it. Result: ``active`` @@ -73,12 +73,12 @@ async def _quiesce_pane(app: FNDApp, pilot: object) -> None: async def test_dispatch_never_adopts_a_container_it_just_condemned(built_index: Path) -> None: """The stranded-container sweep and the DOM-scan adopt run in the same synchronous block. The sweep must not be able to hand the adopt a widget it - has already condemned — the mount would build into a doomed container and + has already condemned: the mount would build into a doomed container and strand ``active`` on a detached widget (permanent blank pane).""" app = FNDApp(index_dir=built_index, initial_query="apple") async with app.run_test() as pilot: await safe_pause(pilot) - assert app._search.groups, "setup — query produced no results" + assert app._search.groups, "setup: query produced no results" preview = app._preview g, chunks = _structural_group(app) query_sig = app._search.query_signature() @@ -97,8 +97,8 @@ async def test_dispatch_never_adopts_a_container_it_just_condemned(built_index: preview.active = None preview.chunk_cache[g.parent_id] = chunks await safe_pause(pilot) - assert stray.parent is not None, "setup — stray is mounted" - assert not stray._pruning, "setup — stray is not condemned" + assert stray.parent is not None, "setup: stray is mounted" + assert not stray._pruning, "setup: stray is not condemned" mounted_on: list[PreviewContainer] = [] orig = preview._mount_chunks_async @@ -116,7 +116,7 @@ def _spy(parent_id, focus, chks, container, **kw): # type: ignore[no-untyped-de adopted = mounted_on[0] if mounted_on else None assert adopted is not None, "dispatch should have started a mount" assert not adopted._pruning, ( - "dispatch adopted a container whose Prune is already queued — the " + "dispatch adopted a container whose Prune is already queued: the " "mount will build into a widget that is about to be torn out of the DOM" ) @@ -150,8 +150,8 @@ async def test_condemned_cache_entry_is_a_miss(built_index: Path) -> None: await app.query_one("#preview_pane").mount(doomed) await safe_pause(pilot) doomed.remove() # queued, not yet processed - assert doomed.parent is not None, "setup — still attached" - assert doomed._pruning, "setup — removal is queued (condemned)" + assert doomed.parent is not None, "setup: still attached" + assert doomed._pruning, "setup: removal is queued (condemned)" preview.active = None preview.chunk_cache[g.parent_id] = chunks @@ -193,8 +193,8 @@ async def test_already_active_fast_path_skipped_when_active_is_condemned( await app.query_one("#preview_pane").mount(doomed) await safe_pause(pilot) doomed.remove() - assert doomed.parent is not None, "setup — still attached" - assert doomed._pruning, "setup — removal is queued (condemned)" + assert doomed.parent is not None, "setup: still attached" + assert doomed._pruning, "setup: removal is queued (condemned)" preview.active = doomed preview.chunk_cache[g.parent_id] = chunks @@ -216,7 +216,7 @@ async def _spy(container: object, parent_id: str, focus_chunk_seq: int) -> None: @pytest.mark.asyncio async def test_mount_finally_does_not_cache_a_condemned_container(built_index: Path) -> None: """The mount's ``finally`` refuses to cache a *detached* container. A - condemned one is equally dead — caching it hands the next visit a hit on a + condemned one is equally dead: caching it hands the next visit a hit on a widget that is being torn down.""" import asyncio import contextlib @@ -257,13 +257,13 @@ async def _blocking_cancel_task_on(_c: object) -> None: ) preview.mount_task = task await safe_pause(pilot) - assert container.parent is not None, "setup — parked mount has the container mounted" + assert container.parent is not None, "setup: parked mount has the container mounted" # Condemn it mid-flight, then release + cancel so the finally runs while # the Prune is still only queued. container.remove() - assert container.parent is not None, "setup — still attached in flight" - assert container._pruning, "setup — condemned in flight" + assert container.parent is not None, "setup: still attached in flight" + assert container._pruning, "setup: condemned in flight" task.cancel() gate.set() with contextlib.suppress(asyncio.CancelledError): diff --git a/tests/test_preview_container_reclaim.py b/tests/test_preview_container_reclaim.py index d2fe773b..f4370347 100644 --- a/tests/test_preview_container_reclaim.py +++ b/tests/test_preview_container_reclaim.py @@ -1,8 +1,8 @@ """Navigating inside ONE file must not accumulate preview containers. The same-file out-of-window navigation path builds a FRESH ``PreviewContainer`` -and atomic-swaps to it. It used to return before reaching the sweep — which -lives on the cross-file path — on the assumption that the old container would be +and atomic-swaps to it. It used to return before reaching the sweep, which +lives on the cross-file path, on the assumption that the old container would be "swept on the next navigation". Inside a single file that next navigation takes the same early return, so nothing was ever reclaimed. @@ -50,7 +50,7 @@ async def test_in_file_navigation_does_not_accumulate_containers( assert searcher is not None chunks = searcher.get_file_chunks(group.parent_id) assert len(chunks) > tuning.FULLMOUNT_CHUNK_BUDGET, ( - f"fixture has {len(chunks)} chunks — under the full-mount budget it is " + f"fixture has {len(chunks)} chunks: under the full-mount budget it is " "mounted whole and every jump scrolls in place, so this never exercises " "the rebuild path that leaked" ) @@ -66,7 +66,7 @@ async def test_in_file_navigation_does_not_accumulate_containers( # navigations must not leave eight containers. assert len(containers) <= 3, ( f"{len(containers)} containers after 8 in-file navigations " - f"(parents={[c.parent_doc_id[:6] for c in containers]}) — " + f"(parents={[c.parent_doc_id[:6] for c in containers]}), " "the same-file rebuild path is not reclaiming" ) @@ -110,7 +110,7 @@ async def test_reclaim_runs_after_an_in_window_landing( a container stranded earlier survived for as long as the user stayed inside one file, where every jump is an in-window scroll and neither path runs. Measured on a real file: twelve in-window navigations later, one strand still - held 6 live chunk trees and 169 widgets — more DOM than the 92 frozen chunks + held 6 live chunk trees and 169 widgets, more DOM than the 92 frozen chunks of the file actually being read. Asserts the sweep is REACHED from the in-window landing rather than that a @@ -150,6 +150,6 @@ def counting(self, **kwargs): # type: ignore[no-untyped-def] await settle(pilot, ticks=12) assert calls, ( - "no reclamation ran for an in-window navigation — a container " + "no reclamation ran for an in-window navigation: a container " "stranded earlier would survive for the whole session" ) diff --git a/tests/test_preview_coverage.py b/tests/test_preview_coverage.py index bbdc91c7..3b005a82 100644 --- a/tests/test_preview_coverage.py +++ b/tests/test_preview_coverage.py @@ -1,14 +1,14 @@ """Coverage captures the matches ahead, and a jump then mounts instead of builds. A file past ``FULLMOUNT_CHUNK_BUDGET`` is never background-filled, so before this -every jump outside the ±7 window built a fresh container from markdown source — +every jump outside the ±7 window built a fresh container from markdown source, including to a chunk visited moments earlier, because the rebuild discarded the one that held it. Measured on a real corpus at ~1743ms against ~243ms for a jump inside the window. What is asserted here is the mechanism that closes that gap: that hit chunks get captured, and that a jump to one MOUNTS the capture rather than building a -markdown widget. Latency itself is not asserted — the suite pins preview debounce +markdown widget. Latency itself is not asserted: the suite pins preview debounce and prefetch to zero, so it cannot reproduce the timings that motivate any of it. """ @@ -113,14 +113,14 @@ async def test_a_far_jump_mounts_a_capture_instead_of_building( assert searcher is not None chunks = searcher.get_file_chunks(group.parent_id) assert len(chunks) > tuning.FULLMOUNT_CHUNK_BUDGET, ( - f"fixture has {len(chunks)} chunks — inside the budget it is filled whole " + f"fixture has {len(chunks)} chunks: inside the budget it is filled whole " "and there is no far jump left to serve" ) target_seq = seqs[-1] store = app._preview.capture_store # The width captures are FILED under, which is the width chunks lay out - # at — not the pane's content width, which does not shrink by the + # at, not the pane's content width, which does not shrink by the # scrollbar and so misses every capture once one is showing. pane_width = app._preview.capture_width(app.query_one("#preview_pane", VerticalScroll)) @@ -141,7 +141,7 @@ async def test_a_far_jump_mounts_a_capture_instead_of_building( assert active is not None widget = active.chunk_widgets.get(target_seq) assert isinstance(widget, FrozenChunkView), ( - f"jumped to chunk {target_seq} and got {type(widget).__name__} — the capture " + f"jumped to chunk {target_seq} and got {type(widget).__name__}: the capture " "was not served, so the jump paid for a markdown build it did not need" ) assert not isinstance(widget, FNDMarkdown) @@ -187,14 +187,14 @@ def compose(self) -> ComposeResult: @pytest.mark.asyncio async def test_an_off_screen_capture_matches_the_on_screen_one() -> None: """Coverage builds on a screen the user never sees, and what it captures is - served in place of the real widget — so any divergence between the two is a + served in place of the real widget, so any divergence between the two is a preview that renders differently depending on which path produced it. This caught tables coming back as an empty box: the off-screen screen is not current, so Textual will not lay it out on its own, and a DataTable sizes itself in response to its own posted refresh. Without a yield to the message pump between layout passes the capture holds the border and none of the - cells — measured rows=3, size=0, virtual=0. + cells: measured rows=3, size=0, virtual=0. """ app = _CaptureHost() async with app.run_test(size=(90, 30)) as pilot: @@ -226,7 +226,7 @@ async def test_an_off_screen_capture_matches_the_on_screen_one() -> None: assert captured is not None, "the warm host refused a chunk the pane captures fine" assert [s.text for s in captured.strips] == [s.text for s in on_screen.strips], ( - "an off-screen capture rendered differently from the on-screen one — " + "an off-screen capture rendered differently from the on-screen one: " "it is served in place of the widget, so this is a visible difference" ) @@ -239,7 +239,7 @@ async def test_coverage_does_not_hold_the_mount_in_flight( ``lazy_mount.check`` bails while ``user_mount_in_flight()``, so awaiting coverage from the mount task means scrolling upward stops working for as - long as coverage runs — tens of seconds on a large file. The mount is over + long as coverage runs: tens of seconds on a large file. The mount is over when the preview is on screen; capturing ahead is separate work. """ index = wide_doc(tmp_path, tmp_index_dir) @@ -260,7 +260,7 @@ async def test_coverage_does_not_hold_the_mount_in_flight( ), timeout=30.0, message=( - "coverage never ran with the mount already finished — it is being " + "coverage never ran with the mount already finished: it is being " "awaited by the mount task, which blocks lazy mount for its whole run" ), ) @@ -272,11 +272,11 @@ async def test_coverage_stands_down_while_lazy_mount_runs( ) -> None: """Both are background mount work, and they must not overlap. - Lazy mount's above-path awaits a SETTLED message pump before it can measure - how far its prepend moved the anchor. Coverage feeds that pump continuously - — a widget mounted and removed per capture — so running both at once left an - upward scroll mounting nothing at all, which is a wall the user hits when - scrolling back up through a file. + Lazy mount's above-path awaits a SETTLED message pump before it can measure + how far its prepend moved the anchor. Coverage feeds that pump continuously ( + a widget mounted and removed per capture) so running both at once left an + upward scroll mounting nothing at all, which is a wall the user hits when + scrolling back up through a file. """ index = wide_doc(tmp_path, tmp_index_dir) app = FNDApp(index_dir=index, initial_query="quartzfin") @@ -294,14 +294,14 @@ async def test_coverage_stands_down_while_lazy_mount_runs( chunks = searcher.get_file_chunks(group.parent_id) pane = app.query_one("#preview_pane", VerticalScroll) # The width captures are filed under, which is the width chunks lay out - # at — the count below has to read the same key the capture writes. + # at: the count below has to read the same key the capture writes. width = presenter.capture_width(pane) sig = app._search.query_signature() spec = app._effective_match_spec # Stop the presenter's OWN coverage run first. It may already be inside # a capture, and that capture completing would store a row this test - # would read as the gate having failed — the background task is not what + # would read as the gate having failed: the background task is not what # is under test here. presenter.stop_background_work() for _ in range(6): @@ -326,7 +326,7 @@ async def test_coverage_stands_down_while_lazy_mount_runs( for _ in range(12): await pilot.pause() assert presenter.capture_store.count(group.parent_id, sig, width) == before, ( - "coverage captured while a lazy-mount batch was in flight — the two " + "coverage captured while a lazy-mount batch was in flight: the two " "compete for the same message pump" ) finally: @@ -334,8 +334,8 @@ async def test_coverage_stands_down_while_lazy_mount_runs( app._lazy.task = None # The positive control, and the whole reason this test means anything: # with lazy mount finished, the SAME call must capture. Without it, any - # early return — a stale anchor, a changed query, an unrenderable chunk - # — would satisfy the assertion above while proving nothing. + # early return (a stale anchor, a changed query, an unrenderable chunk) + # would satisfy the assertion above while proving nothing. assert await job > 0, ( "the same targets captured nothing once lazy mount finished, so the " "assertion above proved only that some other guard fired" @@ -349,7 +349,7 @@ async def test_the_freeze_sweep_yields_between_chunks( """The cold-to-warm swap must not hold the loop for the whole file. The sweep replaces every out-of-window chunk's widget tree with its capture. - Done in one synchronous loop it is a hard stall of however long that takes — + Done in one synchronous loop it is a hard stall of however long that takes: measured 424ms of worst-case loop block on 237 synthetic six-line chunks, and real chunks cost far more each. Sliced, the same run sits at parity with freezing switched off entirely (156ms against 191ms). @@ -402,7 +402,7 @@ def slow_freeze(chunk, chunk_seq): # type: ignore[no-untyped-def] # The LONGEST gap between turns, not a count: the sweep awaits in its # prologue too, so counting turns passes whether or not the chunk loop - # itself ever yields — which is exactly how the first version of this + # itself ever yields, which is exactly how the first version of this # test managed to be vacuous. gaps: list[float] = [] @@ -434,13 +434,13 @@ async def ticker() -> None: # own during startup, so by the time this runs the chunks can already be # stand-ins that the sweep skips. assert len(swept) >= 8, ( - f"the sweep only froze {len(swept)} chunks — too few to tell a blocking " + f"the sweep only froze {len(swept)} chunks: too few to tell a blocking " "sweep from a yielding one, so this would pass either way" ) worst_ms = max(gaps) * 1000 if gaps else 0.0 # Turns the ticker got BETWEEN the first chunk and the last, which is the # only window the chunk loop controls. Counting across the whole sweep is - # vacuous — the prologue awaits regardless — and the single worst gap cannot + # vacuous (the prologue awaits regardless) and the single worst gap cannot # tell a blocking sweep from one OS deschedule: it read 483ms against a # 249ms sweep on a Windows runner that was slicing correctly, a gap longer # than the sweep it was meant to describe. @@ -452,7 +452,7 @@ async def ticker() -> None: # boundary has no chunk after it. `// 4` passed on two yields in eighty. owed = len(turns_at_freeze) - 2 assert turns_in_loop >= owed, ( - "the freeze sweep held the loop through the whole swap — the " + "the freeze sweep held the loop through the whole swap: the " f"cold-to-warm transition is one uninterruptible block ({turns_in_loop} yields " f"against {owed} owed, {len(turns_at_freeze)} chunks, {sweep_ms:.0f}ms sweep, " f"worst gap {worst_ms:.0f}ms)" @@ -467,7 +467,7 @@ async def test_lazy_mount_is_only_walled_off_until_first_paint( The mount task stays alive past the reveal to fill and freeze, and treating that tail as "a mount is happening" walls scroll-driven lazy mount off for - seconds — scrolling up mounts nothing until it finishes. Slicing the sweep + seconds: scrolling up mounts nothing until it finishes. Slicing the sweep lengthened the tail and made it plain. The sweep is held open deliberately, because a fast tail would let this pass @@ -504,7 +504,7 @@ async def slow_sweep(self, container, chunks, win_start, win_end): # type: igno timeout=10.0, message=( "the container never registered as painted while the backfill " - "ran — lazy mount stays walled off for the whole of it" + "ran: lazy mount stays walled off for the whole of it" ), ) assert presenter.user_mount_in_flight(), "the sweep should still be held" @@ -516,7 +516,7 @@ async def test_coverage_skips_chunks_too_expensive_to_build( ) -> None: """A capture runs on the UI's event loop, so an expensive one IS a freeze. - The duty cycle between captures cannot help — it has no way into a single + The duty cycle between captures cannot help: it has no way into a single build. Measured on a real PDF, one 120,123-character chunk took 4.4s to build and produced an 8.4s freeze in a live session, against a 5.3ms median. So the outliers must be refused BEFORE building, which is the only point at @@ -568,7 +568,7 @@ async def test_coverage_skips_chunks_too_expensive_to_build( # Now make it oversized and confirm coverage refuses it. FileChunk is # frozen, so this is a copy standing in the same position. # A FIXED size, matching the 120K-character chunk measured on the real - # PDF — deliberately not derived from the threshold, because a body + # PDF: deliberately not derived from the threshold, because a body # sized as a multiple of the limit scales with it and no change to the # limit could ever fail this test. chunks[victim_index] = dataclasses.replace(chunks[victim_index], body_md="x " * 60_000) @@ -595,14 +595,14 @@ async def test_a_capture_is_cut_at_the_width_it_will_be_served_into( Children lay out inside the pane's SCROLLABLE content region, which the vertical scrollbar shrinks by a column; `size` and `content_size` are equal to each other and neither moves with the bar. Building at those meant that - for any document long enough to need a scrollbar — which is every document - this feature exists for — the capture was a column wider than the slot it + for any document long enough to need a scrollbar, which is every document + this feature exists for, the capture was a column wider than the slot it was served into, so the last cell of every row was cropped and the strips were wrapped for a width never displayed. Heights, `first_match_row` and the stop rows were all off by the difference. The earlier version of this test monkeypatched the width function and then - asserted the writer and the sweep agreed — which they did by construction, + asserted the writer and the sweep agreed, which they did by construction, whatever the function returned. It passed with the bug present. """ index = wide_doc(tmp_path, tmp_index_dir) @@ -635,7 +635,7 @@ async def test_a_capture_is_cut_at_the_width_it_will_be_served_into( assert live is not None, "no mounted chunk to measure against" assert presenter.capture_width(pane) == live.size.width, ( f"captures would be cut at {presenter.capture_width(pane)} while chunks " - f"lay out at {live.size.width} — every served row loses its last cells" + f"lay out at {live.size.width}: every served row loses its last cells" ) @@ -647,7 +647,7 @@ async def test_a_tall_chunk_is_captured_at_the_width_it_was_asked_for( `WarmHost` lays its screen out `_LAYOUT_HEIGHT` rows tall. A chunk taller than that overflows the container, which then grows a vertical scrollbar, and - the chunk lays out one column narrower than the width asked for — so the + the chunk lays out one column narrower than the width asked for, so the capture is cut for a width it is not filed under, and every row it is served into is a column short. Measured before the fix, the boundary was exactly the layout box: 300 rendered rows captured at 76, 420 rows at 75. @@ -701,13 +701,13 @@ async def test_a_resize_does_not_leave_frozen_chunks_painting_cropped_text( ) -> None: """Strips are width-locked, and `render_line` crops them to the widget. - So a narrower pane does not re-wrap a frozen chunk — it removes the + So a narrower pane does not re-wrap a frozen chunk: it removes the right-hand cells of every row, and the text is simply gone until something rebuilds the file. Measured before the fix on a 120-section document, shrinking 100 to 80 columns left 87 chunks rendering 20 columns short. The store sweep cannot help: those strips are on screen, not in the store. - This asserts the on-screen half — that no frozen chunk is left painting + This asserts the on-screen half: that no frozen chunk is left painting strips cut for a width it is no longer laid out at. """ index = wide_doc(tmp_path, tmp_index_dir) @@ -729,8 +729,8 @@ def frozen_views() -> list[FrozenChunkView]: # Freeze one mounted chunk by hand rather than waiting for the # background sweep: the sweep's timing is not what is under test, and - # depending on it makes this a race. This is exactly what the sweep does - # — capture the widget, mount the capture in its place. + # depending on it makes this a race. This is exactly what the sweep does: + # capture the widget, mount the capture in its place. container = presenter.active assert container is not None # Ask `freeze` itself which chunk is ready, rather than predicting its @@ -738,7 +738,7 @@ def frozen_views() -> list[FrozenChunkView]: # declines a hidden ancestor, an ancestor still at `-pre-reveal` opacity, # and an unlaid table. Selecting on `size > 0` alone picked chunks it # then refused, and the assertion below failed on the slowest CI runner - # while passing on the other two — the container had not been revealed + # while passing on the other two: the container had not been revealed # yet, which the "preview is active" wait above does not cover. picked: list[tuple[int, FNDMarkdown, FrozenChunk]] = [] @@ -775,7 +775,7 @@ def repaired_in_place() -> bool: # The emptiness check is part of the CONDITION, not an assert: # `wait_until` swallows a raising predicate, so an assert here would # be invisible and the timeout would report the wrong diagnosis. An - # empty list is not "nothing stale" — if the repair ever removed the + # empty list is not "nothing stale", if the repair ever removed the # view instead of re-cutting it, `any()` over nothing is False and # this would pass for the wrong reason. return bool(views) and not any( @@ -787,9 +787,9 @@ def repaired_in_place() -> bool: await wait_until( pilot, lambda: presenter.active is not None and repaired_in_place(), - # Generous on purpose. The repair is deliberately unhurried — it + # Generous on purpose. The repair is deliberately unhurried: it # debounces the gesture, waits out any mount in flight, and yields - # between captures — so under full-suite load it legitimately takes + # between captures, so under full-suite load it legitimately takes # far longer than it does alone. The assertion is that it happens, # not that it happens fast. timeout=90.0, @@ -812,7 +812,7 @@ def repaired_in_place() -> bool: ] assert not stale, ( f"{len(stale)} frozen chunks are cut for a width they are not laid out " - f"at {stale[:3]} — every row of those loses its right-hand cells" + f"at {stale[:3]}, every row of those loses its right-hand cells" ) @@ -823,7 +823,7 @@ async def test_the_repair_re_arms_but_cannot_chain_forever( """Two halves of one guarantee, and a capture that always fails proves both. A pass abandons whenever the width moves again mid-drag, and reports arriving - while a pass runs are dropped — so the pass has to re-arm itself or the tail + while a pass runs are dropped, so the pass has to re-arm itself or the tail stays cropped until the next navigation. Gating that re-arm on having repaired something looks safer but loses exactly the case it is needed for: a pass that abandons BEFORE its first success. @@ -912,7 +912,7 @@ async def counting(attempt: int = 0) -> None: await pilot.pause() assert passes > 1, ( - "the repair ran once and stopped even though it had repaired nothing — " + "the repair ran once and stopped even though it had repaired nothing: " "a pass that abandons before its first success must still re-arm" ) assert passes <= tuning.STALE_STRIP_MAX_PASSES, ( @@ -929,7 +929,7 @@ async def test_a_failed_mount_does_not_disable_warming_for_the_session() -> None So a first call that installs the screen and then fails to mount into it leaves that name taken. Re-installing it on the retry raises on the duplicate, the raise is swallowed, and `ensure` returns `None` for the rest - of the session — every capture missing, nothing logged, and the only symptom + of the session: every capture missing, nothing logged, and the only symptom a preview that is quietly slow forever. """ app = _CaptureHost() @@ -948,12 +948,12 @@ async def failing_mount(*args: object, **kwargs: object) -> None: with pytest.MonkeyPatch.context() as mp: mp.setattr(textual.screen.Screen, "mount", failing_mount) assert await host.ensure() is None, "the failing mount should yield no container" - # The context restored the real mount on exit — so this second call is + # The context restored the real mount on exit, so this second call is # the retry, and it is the whole test. assert calls["n"] == 1, "the mount failed more than the once this models" assert await host.ensure() is not None, ( - "warming stayed dead after one failed mount — the screen name was " + "warming stayed dead after one failed mount: the screen name was " "still taken and every retry raised on the duplicate" ) @@ -962,7 +962,7 @@ async def failing_mount(*args: object, **kwargs: object) -> None: # # What a pass DOES with the cursor is only visible in the order files are # worked, and that order is decided in `_run_coverage` from the anchor, the -# navigation order and the store — none of which need a real corpus, a real +# navigation order and the store: none of which need a real corpus, a real # capture, or a real widget. Driving the real method against stubbed edges # gives the trace directly; a full app test would have to infer the order from # store contents after the fact, and could not tell "covered late" from @@ -974,7 +974,7 @@ class _StubChunk: chunk_seq: int # Enough for `uses_markdown_renderer` to accept it. Coverage skips any # chunk the off-screen builder cannot build, so a stub without these is - # uncapturable and every file in the plan gets silently skipped — the + # uncapturable and every file in the plan gets silently skipped: the # tests below would then assert about an ordering that never happened. kind: str = "md" body_md: str = "stub body" @@ -992,7 +992,7 @@ def __init__(self) -> None: self.closed = False self.abandoned = False self.reports: list[tuple[float, float]] = [] - #: Reports that arrived after retirement — the real session silently + #: Reports that arrived after retirement; the real session silently #: drops these, so nothing else could show they were being sent. self.reports_after_close: list[tuple[float, float]] = [] @@ -1015,7 +1015,7 @@ class _StubProgress: """The facility's single AMBIENT slot, which the warm must not steal. ``begin`` takes the slot and retires whoever held it, as the real facility - does — a stub that merely handed back a session could not model the + does: a stub that merely handed back a session could not model the collision these tests exist to pin. """ @@ -1134,7 +1134,7 @@ async def _record( await asyncio.sleep(0.005) done += 1 if on_capture is not None: - # (walked, captured) — the stub captures everything it walks. + # (walked, captured): the stub captures everything it walks. on_capture(done, done) if self.pause_after == (parent_id, done): # Hold this file mid-capture so the test can move the cursor at @@ -1175,7 +1175,7 @@ async def test_moving_within_a_file_keeps_warming_that_file( """The file the user is IN must not be shunted behind its neighbours. Tier 1's order is nearest-first around the cursor, so a move inside the file - does invalidate the ORDER — but not the file, which is still the only one + does invalidate the ORDER, but not the file, which is still the only one that can serve the next keypress. Abandoning the pass on that basis handed the serial host to the neighbours for the rest of it: the file was dropped part-covered, both neighbours were covered whole, and it was only picked up @@ -1197,7 +1197,7 @@ async def test_moving_within_a_file_keeps_warming_that_file( # fixture's speed rather than about the marker. assert p.coverage_parent == "A", ( "the file the cursor is in stopped being warmed the moment the cursor " - "moved inside it — the results arrow paints this as cold" + "moved inside it: the results arrow paints this as cold" ) trace.resume.set() await _drain(trace) @@ -1223,9 +1223,9 @@ async def test_leaving_a_file_stands_its_coverage_down( replaces, because the pass planned around the new current file then queues behind a whole pass for a file nobody is reading. - A property guard, not a red-then-green test: two things enforce this — the + A property guard, not a red-then-green test: two things enforce this; the per-round predicate is bound to the file, and the loop ends when the cursor - stops moving within it — so removing either one alone still passes. What it + stops moving within it, so removing either one alone still passes. What it pins is the outcome, which is what a later change to either would break. """ monkeypatch.setattr(tuning, "PREVIEW_WARM_DELAY", 0.0) @@ -1260,7 +1260,7 @@ async def test_the_seed_files_are_actually_covered( as the plan it is judged against. Judging seed files by the neighbour window alone made the whole tier inert: a seed file outside that window is never "still wanted", so every one of them paid a full off-loop chunk decode and - was then abandoned before its first capture — with a stationary cursor as + was then abandoned before its first capture, with a stationary cursor as much as a moving one. """ monkeypatch.setattr(tuning, "PREVIEW_WARM_DELAY", 0.0) @@ -1284,7 +1284,7 @@ async def test_context_work_yields_to_a_file_the_cursor_moved_towards( """Margins are context; a hit is what a jump lands on. A pass is only re-planned when it ENDS, and the plan is the only place a - newly neighbouring file can appear — so a margin walk that runs to + newly neighbouring file can appear, so a margin walk that runs to completion holds off every landing the cursor has just moved towards. The walk could not be stood down either: its test was plan membership, and the seed is in every plan. Measured on the real index, the file two below the @@ -1322,7 +1322,7 @@ async def test_a_request_arriving_late_in_a_pass_is_still_served( """A pass checks for a request once, before its margin walk. So a request that lands DURING that walk has already been missed, and the - cursor has not moved — the loop would call itself finished and the file + cursor has not moved: the loop would call itself finished and the file would never warm. The outstanding request is what keeps it going. """ monkeypatch.setattr(tuning, "PREVIEW_WARM_DELAY", 0.0) @@ -1373,7 +1373,7 @@ async def test_cancelling_only_stops_the_file_it_names( """The key toggles per FILE, and the caller needs to know which it did. Unnamed, pressing it on a second file stopped the warm on the first and - started nothing — the one press that could not do what it looked like. + started nothing: the one press that could not do what it looked like. """ monkeypatch.setattr(tuning, "PREVIEW_WARM_DELAY", 0.0) trace = _CoverageTrace(["A", "B"]) @@ -1411,7 +1411,7 @@ async def test_a_warm_never_takes_the_line_from_an_ambient_operation( """The facility has ONE ambient slot and ``begin`` retires whatever holds it. Indexing is the other ambient operation and can run for minutes, so a warm - that opened unconditionally left the machine indexing with no indication — + that opened unconditionally left the machine indexing with no indication: the exact regression the index line exists to prevent. """ monkeypatch.setattr(tuning, "PREVIEW_WARM_DELAY", 0.0) @@ -1447,7 +1447,7 @@ async def test_warming_a_second_file_does_not_inherit_the_first_ones_line( p.request_full_warm("B", 0) # Checked at the moment of the switch, with no await in between. Later, the - # same session object is closed when B's warm finishes — so a test that + # same session object is closed when B's warm finishes, so a test that # looked afterwards could not tell reuse from retirement. assert all(s.closed for s in first), ( "the second file's warm inherited the first file's progress line" @@ -1491,8 +1491,8 @@ async def test_stopping_a_warm_does_not_claim_it_finished( ) -> None: """Closing paints the bar full and files the run for calibration. - Three of the four retirement paths are abandonments — cancel, query reset - and switching file — and a cancelled warm filled its bar to 100% beside a + Three of the four retirement paths are abandonments (cancel, query reset + and switching file) and a cancelled warm filled its bar to 100% beside a toast saying it had been stopped. """ monkeypatch.setattr(tuning, "PREVIEW_WARM_DELAY", 0.0) @@ -1564,7 +1564,7 @@ async def test_the_freeze_sweep_asks_the_markers_to_re_measure( tmp_path: Path, tmp_index_dir: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """The sweep rewrites ``chunk_widgets`` / ``match_targets``, which is where - the ▲▼ markers get their stops — so the counts it invalidates must be + the ▲▼ markers get their stops, so the counts it invalidates must be re-derived, not left describing the tree it just removed.""" # The app sweeps on its own during startup, and under load it wins: this # sweep then has nothing to freeze, owes no notification, and the test diff --git a/tests/test_preview_detached_container_guard.py b/tests/test_preview_detached_container_guard.py index 771deb15..90d85184 100644 --- a/tests/test_preview_detached_container_guard.py +++ b/tests/test_preview_detached_container_guard.py @@ -29,7 +29,7 @@ @pytest.fixture def built_index(tmp_path: Path, tmp_index_dir: Path) -> Path: # A small MARKDOWN corpus so results render on the structural (per-chunk - # widget) path the detached-container guards protect — the flat PDF/TXT path + # widget) path the detached-container guards protect: the flat PDF/TXT path # has no widget cache and no fast path. root = tmp_path / "corpus" root.mkdir() @@ -46,7 +46,7 @@ def built_index(tmp_path: Path, tmp_index_dir: Path) -> Path: def _structural_group(app: FNDApp) -> tuple[FileGroup, list[FileChunk]]: """First result whose chunks render on the STRUCTURAL (per-chunk widget) - path — the flat PDF/TXT path has no widget cache and no fast path, so the + path: the flat PDF/TXT path has no widget cache and no fast path, so the detached-container guards under test don't apply there.""" assert app._search.searcher is not None for g in app._search.groups: @@ -64,13 +64,13 @@ async def test_dispatch_purges_a_detached_cache_entry(built_index: Path) -> None app = FNDApp(index_dir=built_index, initial_query="apple") async with app.run_test() as pilot: await pilot.pause() - assert app._search.groups, "setup — query produced no results" + assert app._search.groups, "setup: query produced no results" preview = app._preview g, chunks = _structural_group(app) query_sig = app._search.query_signature() - # Poison the cache with a DETACHED container for this file+query — the - # exact divergence the eviction/sweep race produces — and make the + # Poison the cache with a DETACHED container for this file+query: the + # exact divergence the eviction/sweep race produces, and make the # dispatch take the cache-resolution path (no same-file active shortcut). ghost = PreviewContainer( parent_doc_id=g.parent_id, @@ -80,14 +80,14 @@ async def test_dispatch_purges_a_detached_cache_entry(built_index: Path) -> None preview.active = None preview.chunk_cache[g.parent_id] = chunks preview.preview_cache.put(ghost) - assert ghost.parent is None, "setup — the cached container is detached" + assert ghost.parent is None, "setup: the cached container is detached" assert preview.preview_cache.get(g.parent_id, query_sig) is ghost preview.dispatch_mount(g.parent_id, chunks[0].chunk_seq, chunks) # Synchronous guard effect: the detached entry is gone and was never made # the active preview. (A fresh, attached container is built asynchronously - # in its place — see the harness runs for the end-to-end no-blank proof.) + # in its place: see the harness runs for the end-to-end no-blank proof.) assert preview.preview_cache.get(g.parent_id, query_sig) is not ghost, ( "the detached cache entry must be purged, not served as a hit" ) @@ -108,7 +108,7 @@ async def test_dispatch_skips_the_already_active_fast_path_when_active_is_detach ) -> None: """The 'same file already active' fast path scrolls ``self.active`` in place. If a race detached that active container, taking the fast path would scroll a - zero-region ghost and strand a blank pane — so dispatch must NOT enter it. + zero-region ghost and strand a blank pane, so dispatch must NOT enter it. A spy on the instant-scroll entry point proves the fast path is skipped.""" app = FNDApp(index_dir=built_index, initial_query="apple") async with app.run_test() as pilot: @@ -118,7 +118,7 @@ async def test_dispatch_skips_the_already_active_fast_path_when_active_is_detach query_sig = app._search.query_signature() # A "complete" active container for this file so the fast path's own - # gate (is_complete) would pass — then detach it out from under active. + # gate (is_complete) would pass, then detach it out from under active. detached = PreviewContainer( parent_doc_id=g.parent_id, query_signature=query_sig, @@ -127,8 +127,8 @@ async def test_dispatch_skips_the_already_active_fast_path_when_active_is_detach detached.mounted_indices = set(range(len(chunks))) # is_complete == True preview.active = detached preview.chunk_cache[g.parent_id] = chunks - assert detached.parent is None, "setup — active container is detached" - assert detached.is_complete, "setup — active container would pass the fast-path gate" + assert detached.parent is None, "setup: active container is detached" + assert detached.is_complete, "setup: active container would pass the fast-path gate" scrolled: list[object] = [] orig = preview._settled_instant_scroll @@ -142,7 +142,7 @@ async def _spy(container: object, parent_id: str, focus_chunk_seq: int) -> None: preview.dispatch_mount(g.parent_id, chunks[0].chunk_seq, chunks) assert detached not in scrolled, ( - "a detached active container must not be scrolled in place — the " + "a detached active container must not be scrolled in place: the " "already-active fast path must fall through to a fresh rebuild" ) assert preview.active is not detached, "the detached container must be dropped as active" diff --git a/tests/test_preview_dispatcher.py b/tests/test_preview_dispatcher.py index c09a8a82..28806977 100644 --- a/tests/test_preview_dispatcher.py +++ b/tests/test_preview_dispatcher.py @@ -1,6 +1,6 @@ """``choose_preview_mode`` routes chunks to the right pipeline. -Pure-function tests on the dispatcher — no Textual pilot needed. The +Pure-function tests on the dispatcher: no Textual pilot needed. The host wire-in (``fnd/tui/app.py``) calls this once per file-load to decide between the flat buffer (PDF / TXT) and the structural Markdown renderer (MD / DOCX / PPTX). @@ -46,7 +46,7 @@ def test_pdf_with_body_md_takes_structural_path(monkeypatch: pytest.MonkeyPatch) def test_pdf_partially_structured_still_structural(monkeypatch: pytest.MonkeyPatch) -> None: """A PDF that was reindexed with extras present but had an individual page fail extraction (empty body_md) still routes - structural overall — the any-chunk rule wins.""" + structural overall: the any-chunk rule wins.""" monkeypatch.delenv("_FND_FORCE_FLAT", raising=False) chunks = [_chunk("pdf", body_md=""), _chunk("pdf", body_md="## p2")] assert choose_preview_mode(chunks) == "structural" @@ -69,7 +69,7 @@ def test_pptx_with_body_md_takes_structural_path() -> None: def test_markdown_kind_without_body_md_falls_through_to_flat() -> None: - """Stale-index defence — a markdown-kinded chunk with empty + """Stale-index defence: a markdown-kinded chunk with empty ``body_md`` shouldn't mount through the structural renderer (which would render nothing). The flat path renders the legacy ``blocks`` text safely instead.""" @@ -92,7 +92,7 @@ def test_empty_chunks_takes_flat_path() -> None: def test_uses_markdown_renderer_pdf_with_body_md() -> None: - """Per-chunk routing must agree with ``choose_preview_mode`` — PDF + """Per-chunk routing must agree with ``choose_preview_mode``: PDF chunks with ``body_md`` route through the markdown renderer at mount time. Drift between this helper and ``_MARKDOWN_RENDERED_KINDS`` caused PDFs to render flat despite the dispatcher selecting @@ -106,7 +106,7 @@ def test_uses_markdown_renderer_pdf_with_body_md() -> None: def test_an_oversized_chunk_is_routed_away_from_the_markdown_renderer( monkeypatch: pytest.MonkeyPatch, ) -> None: - """The size cap had no test at all — neither the routing nor the fallback. + """The size cap had no test at all, neither the routing nor the fallback. A chunk over the cap must stop using the structural renderer and route to the flat one instead. @@ -120,7 +120,7 @@ def test_an_oversized_chunk_is_routed_away_from_the_markdown_renderer( small = _chunk(kind="md", body_md="| a | b |\n|---|---|\n| 1 | 2 |") assert uses_markdown_renderer(small) - # Both sides of the boundary — the cap is inclusive, and a body far under + # Both sides of the boundary: the cap is inclusive, and a body far under # and far over it cannot tell `<=` from `<`. assert uses_markdown_renderer(_chunk(kind="md", body_md="x" * MARKDOWN_MAX_CHARS)) assert not uses_markdown_renderer(_chunk(kind="md", body_md="x" * (MARKDOWN_MAX_CHARS + 1))) diff --git a/tests/test_preview_flat_cancels_structural.py b/tests/test_preview_flat_cancels_structural.py index fdd6b865..4e691770 100644 --- a/tests/test_preview_flat_cancels_structural.py +++ b/tests/test_preview_flat_cancels_structural.py @@ -5,7 +5,7 @@ *before* the ``cancel_mount_task()`` the structural branch does. So a structural mount already in flight for the previous file kept running; when it finished it called ``activate_container``, which hides every ``LineBufferPreview`` and shows -its own container — the preview ends up displaying the file the user navigated +its own container: the preview ends up displaying the file the user navigated AWAY from, while the cursor sits on the flat one. ``schedule_load``'s own navigate-away cancel does not cover it: that cancel is @@ -66,7 +66,7 @@ async def test_flat_navigation_cancels_an_inflight_structural_mount(mixed_index: app = FNDApp(index_dir=mixed_index, initial_query="apples") async with app.run_test() as pilot: await safe_pause(pilot) - assert app._search.groups, "setup — query produced no results" + assert app._search.groups, "setup: query produced no results" preview = app._preview # The app's own first preview must land first: the parked mount below # takes its `mount_task` slot, so a load still running there is never cancelled. @@ -103,13 +103,13 @@ async def _blocking_cancel_task_on(_c: object) -> None: ) preview.mount_task = task await safe_pause(pilot) - assert not task.done(), "setup — the structural mount should be parked in flight" + assert not task.done(), "setup: the structural mount should be parked in flight" # Navigate to the flat result. preview.dispatch_mount(flat_g.parent_id, flat_chunks[0].chunk_seq, flat_chunks) assert task.cancelled() or task.cancelling() > 0, ( - "a flat navigation must cancel the structural mount it replaces — " + "a flat navigation must cancel the structural mount it replaces: " "otherwise that mount activates its container and displays the wrong file" ) diff --git a/tests/test_preview_focus_class_toggle.py b/tests/test_preview_focus_class_toggle.py index e25c5ad7..cbd880f0 100644 --- a/tests/test_preview_focus_class_toggle.py +++ b/tests/test_preview_focus_class_toggle.py @@ -29,7 +29,7 @@ def built_index(fixtures_dir: Path, tmp_index_dir: Path) -> Path: @pytest.mark.asyncio async def test_preview_pane_has_no_focus_within_rule(built_index: Path) -> None: """The pane's applicable styles must NOT include any ``:focus-within`` - pseudo-class — otherwise Textual reapplies CSS across the entire + pseudo-class, otherwise Textual reapplies CSS across the entire preview subtree on every focus change. """ app = FNDApp(index_dir=built_index) @@ -50,17 +50,17 @@ async def test_preview_focused_class_toggles_with_focus(built_index: Path) -> No async with app.run_test() as pilot: await pilot.pause() pane = app.query_one("#preview_pane") - # Focus elsewhere — class should be absent. + # Focus elsewhere: class should be absent. app.query_one("#results_pane", Tree).focus() await pilot.pause() assert "-focused" not in pane.classes - # Focus the preview pane — class should appear. + # Focus the preview pane: class should appear. pane.focus() await pilot.pause() assert "-focused" in pane.classes - # Move focus back out — class should clear. + # Move focus back out: class should clear. app.query_one("#results_pane", Tree).focus() await pilot.pause() assert "-focused" not in pane.classes @@ -68,8 +68,8 @@ async def test_preview_focused_class_toggles_with_focus(built_index: Path) -> No @pytest.mark.asyncio async def test_focus_border_class_moves_between_panes(built_index: Path) -> None: - """The accent border (``-focused``) follows focus across every pane — - not just the preview — and only the focused pane wears it.""" + """The accent border (``-focused``) follows focus across every pane, + not just the preview, and only the focused pane wears it.""" app = FNDApp(index_dir=built_index, initial_query="blue penguin sandwich") async with app.run_test() as pilot: await pilot.pause() @@ -92,7 +92,7 @@ async def test_focus_border_survives_terminal_blur(built_index: Path) -> None: """The accent border must NOT drop when the terminal loses focus. Textual clears widget focus on ``AppBlur`` (``app_focus = False`` → - ``screen.set_focus(None)``) and only restores it on the next keypress — + ``screen.set_focus(None)``) and only restores it on the next keypress, which made the border vanish on tab-away and reappear a beat late. The persistent ``-focused`` class is driven by descendant *focus*, never cleared on blur, so it (and the border) stays put across the blur/refocus @@ -122,7 +122,7 @@ async def test_focus_change_does_not_walk_preview_subtree(built_index: Path) -> """The pane's ``watch_has_focus`` override must not walk the subtree. The stock ``Widget.watch_has_focus`` calls ``update_node_styles``, - which walks every descendant and reapplies CSS — the dominant cost + which walks every descendant and reapplies CSS: the dominant cost of focus transitions on large documents. The override on ``MatchAwareScroll`` applies CSS only to the pane itself. """ diff --git a/tests/test_preview_frozen_chunk.py b/tests/test_preview_frozen_chunk.py index bb8b6122..e516cb5e 100644 --- a/tests/test_preview_frozen_chunk.py +++ b/tests/test_preview_frozen_chunk.py @@ -4,8 +4,8 @@ linear in widget count, so mounted DOM is what makes a long reading session progressively slower. Freezing keeps the rendered result and drops the tree. -Fidelity is not re-implemented here — the strips are the real widget tree's own -output — so the tests that matter are that the capture is COMPLETE (the whole +Fidelity is not re-implemented here (the strips are the real widget tree's own +output) so the tests that matter are that the capture is COMPLETE (the whole chunk, not just the part on screen), that the positions navigation needs survive the widgets that produced them, and that the stand-in occupies exactly the same height so swapping it in moves nothing. @@ -44,7 +44,7 @@ def sample(): """ + "\n\n".join( f"Filler paragraph {i}, present so the chunk is comfortably taller than the " - f"test viewport — otherwise 'captures past the fold' proves nothing." + f"test viewport, otherwise 'captures past the fold' proves nothing." for i in range(12) ) @@ -60,7 +60,7 @@ async def _built(pilot) -> FNDMarkdown: # type: ignore[no-untyped-def] md.update(DOC) await md.build_done.wait() # Ask the operation, not a proxy for it: every geometry gate written here - # has been weaker than what `freeze` enforces — a DataTable can report a + # has been weaker than what `freeze` enforces: a DataTable can report a # height with `virtual_size` still zero, which freeze refuses. await wait_until( pilot, @@ -94,7 +94,7 @@ async def test_capture_holds_everything_the_tree_painted() -> None: assert "quartzfin in prose" in text.replace("\n", " ") # ONE line carrying both words. "quartzfin" alone is in the heading, # the prose, the fence and the list item, so asserting it document-wide - # passes even when the table captures as an empty box — the exact + # passes even when the table captures as an empty box: the exact # failure the table guards exist to catch. assert any("quartzfin" in line and "cell" in line for line in text.splitlines()), ( "the table row did not survive the capture" @@ -134,7 +134,7 @@ async def test_positions_survive_the_widgets_that_produced_them() -> None: ) assert frozen.stop_rows, "no match stops captured" assert all(0 <= r < frozen.outer_height for r in frozen.stop_rows) - # Table cells resolve to rows recorded while the DataTable still existed — + # Table cells resolve to rows recorded while the DataTable still existed: # the point being that a row cannot race, whereas a live cell region can. for coord, row in frozen.cell_rows.items(): assert 0 <= row < frozen.outer_height, f"cell {coord} row {row} outside the chunk" @@ -162,7 +162,7 @@ async def test_the_stand_in_is_one_widget_of_the_same_height() -> None: assert len(list(view.query("*"))) == 0, "the stand-in must hold no child widgets" assert view.size.height == frozen.outer_height, ( - f"stand-in is {view.size.height} rows, capture is {frozen.outer_height} — " + f"stand-in is {view.size.height} rows, capture is {frozen.outer_height}: " "a height mismatch would shift the page when it is swapped in" ) rendered = "\n".join(view.render_line(y).text for y in range(view.size.height)) @@ -171,7 +171,7 @@ async def test_the_stand_in_is_one_widget_of_the_same_height() -> None: class _PaddedHost(App[None]): - """A chunk as the preview actually mounts it — with the padding classes. + """A chunk as the preview actually mounts it, with the padding classes. ``_Host`` deliberately has none, so it cannot see this: ``freeze`` captures the CONTENT region, and a stand-in sized to that alone is a row shorter than @@ -200,7 +200,7 @@ async def test_freezing_above_the_viewport_does_not_move_the_page() -> None: If a stand-in is even one row shorter than the chunk it replaces, the content above the viewport shrinks while ``scroll_y`` stays put, and - everything the user is reading slides upward — a second or two after the + everything the user is reading slides upward: a second or two after the navigation landed, which is exactly when it reads as the page jumping on its own. Measured before the padding was carried across: -6 rows for 6 chunks. """ @@ -229,7 +229,7 @@ async def test_freezing_above_the_viewport_does_not_move_the_page() -> None: # both were wrong: `scroll_y == scroll_target_y` settles before the # compositor re-arranges and read a position 174 rows stale, and # `reading.region.y == pane.region.y` assumes the scroll lands exactly at - # the chunk top — it can be clamped short, and the wait then times out + # the chunk top; it can be clamped short, and the wait then times out # under load while passing on an idle machine. # # The assertion below needs only that the position has STOPPED moving. @@ -270,7 +270,7 @@ async def test_freezing_above_the_viewport_does_not_move_the_page() -> None: after = reading.region.y - pane.region.y assert after == before, ( f"the page moved {after - before:+d} rows when {len(above)} chunks above it " - "were frozen — the stand-ins are not the height of what they replaced" + "were frozen: the stand-ins are not the height of what they replaced" ) @@ -301,7 +301,7 @@ async def test_a_frozen_chunk_still_contributes_its_match_stops() -> None: """Freezing must not make matches unreachable. ``enumerate_stop_rows`` walks ``FNDMarkdown`` blocks, and a frozen chunk - is not one — so without explicit handling it contributes nothing and its + is not one, so without explicit handling it contributes nothing and its matches drop out of n/b navigation and the off-screen markers. Nothing raises; the matches simply stop existing, which is why this is pinned. """ @@ -332,14 +332,14 @@ async def test_a_frozen_chunk_still_contributes_its_match_stops() -> None: frozen_rows = _rows_below(pane, view, spec) assert frozen_rows == live_rows, ( - f"stops at rows {live_rows} live but {frozen_rows} once frozen — " + f"stops at rows {live_rows} live but {frozen_rows} once frozen: " "n/b and the markers move when a chunk is captured" ) @pytest.mark.asyncio async def test_a_table_that_has_not_laid_out_is_refused() -> None: - """An unlaid table captures as a BORDER with no contents — an empty box, and + """An unlaid table captures as a BORDER with no contents: an empty box, and downstream nothing can tell it from a table that is genuinely empty. The nested-scroll guard cannot see this: an unlaid table measures 0 both @@ -360,12 +360,12 @@ async def test_a_table_that_has_not_laid_out_is_refused() -> None: dt._size = Size(0, 0) # type: ignore[attr-defined] dt.virtual_size = Size(0, 0) assert freeze(md, chunk_seq=7) is None, ( - "a table with rows but no geometry was captured — it would be served " + "a table with rows but no geometry was captured: it would be served " "as an empty box where the table should be" ) -# One paragraph, one source line, wrapping to many rows with its match late — +# One paragraph, one source line, wrapping to many rows with its match late: # the shape a PDF contents page has, and the one a block-top row gets wrong. # TWO matches, both inside that one block: with one, a per-block capture and a # per-row one produce the same rows and the parity test below cannot tell them @@ -499,7 +499,7 @@ async def test_a_stand_in_paints_the_rows_the_tree_painted() -> None: ``Compositor.reflow`` lays a root's children out inside the size it is given MINUS that root's padding. Sized to the content region, a capture therefore spends its first row on the padding and drops the last content row, and a - stand-in that re-adds that padding paints everything one row low — which no + stand-in that re-adds that padding paints everything one row low, which no height check can see, and which showed up as the page stepping sideways the first time a file was frozen. """ @@ -555,7 +555,7 @@ async def test_a_stand_in_paints_the_rows_the_tree_painted() -> None: @pytest.mark.asyncio async def test_a_stand_in_swap_never_doubles_the_container() -> None: """Textual defers ``remove()``, so mounting the stand-in and then removing - the tree leaves BOTH in the layout until the removal lands — ten swaps + the tree leaves BOTH in the layout until the removal lands: ten swaps measured 559 duplicate rows in a container being read. ``display = False`` is what takes the tree out in the pass the stand-in joins. """ diff --git a/tests/test_preview_leak_probe.py b/tests/test_preview_leak_probe.py index 9be57eae..a74fe5c1 100644 --- a/tests/test_preview_leak_probe.py +++ b/tests/test_preview_leak_probe.py @@ -7,7 +7,7 @@ removed from the DOM. - Result: every file visit added a container to ``#preview_pane`` and ``_activate_preview_container`` paid an O(N) walk over the growing - stack — a real linear slowdown for sessions of any length. + stack; a real linear slowdown for sessions of any length. After the fix: - ``_PREVIEW_CACHE_MIN_CHUNKS = 1`` so every complete file is bounded @@ -29,7 +29,7 @@ @pytest.fixture def small_corpus(tmp_path: Path) -> Path: - """Short markdown files — enough to exceed _TEST_LRU_CAP and exercise + """Short markdown files: enough to exceed _TEST_LRU_CAP and exercise eviction.""" root = tmp_path / "corpus" root.mkdir() diff --git a/tests/test_preview_liveness_contract.py b/tests/test_preview_liveness_contract.py index 4619a64a..5dfe8965 100644 --- a/tests/test_preview_liveness_contract.py +++ b/tests/test_preview_liveness_contract.py @@ -2,7 +2,7 @@ ``fnd/tui/preview/liveness.py`` reads private Textual state (``_pruning`` / ``_closing`` / ``_closed``) via ``getattr(..., False)``. That default is -deliberate — it keeps a teardown race from raising — but it means a Textual +deliberate (it keeps a teardown race from raising) but it means a Textual upgrade that renames or drops those attributes would silently degrade ``is_live`` back to a plain attachment check, which is exactly the bug the module exists to prevent. A silent regression here is expensive: it took five @@ -28,7 +28,7 @@ @pytest.mark.asyncio async def test_remove_is_deferred_and_is_live_sees_through_it() -> None: """The precise behaviour the whole fix rests on: between ``remove()`` and - the ``Prune`` message being processed, the widget still reports a parent — + the ``Prune`` message being processed, the widget still reports a parent, and ``is_live`` must not be fooled by it.""" class _Probe(App[None]): @@ -39,25 +39,25 @@ def compose(self): # type: ignore[no-untyped-def] async with app.run_test() as pilot: await pilot.pause() victim = app.query_one("#victim", Static) - assert is_live(victim), "setup — a mounted widget is live" + assert is_live(victim), "setup: a mounted widget is live" assert not is_condemned(victim) victim.remove() # The load-bearing assertion: removal has NOT taken effect yet. assert victim.parent is not None, ( - "Textual's remove() is no longer deferred — the liveness guards in " + "Textual's remove() is no longer deferred: the liveness guards in " "fnd/tui/preview/liveness.py can be simplified, and this test should " "be updated deliberately rather than deleted" ) assert is_condemned(victim), ( - "a queued Prune must read as condemned — liveness.py detects it via " + "a queued Prune must read as condemned: liveness.py detects it via " "Textual's private _pruning / _closing / _closed, read with a False " "default, so a rename shows up HERE and nowhere else" ) assert not is_live(victim), ( "is_live must reject a condemned widget even while it still reports " - "a parent — this is the window the blank-preview strand lived in" + "a parent: this is the window the blank-preview strand lived in" ) # And once the Prune lands it is gone for real. @@ -70,7 +70,7 @@ def compose(self): # type: ignore[no-untyped-def] @pytest.mark.asyncio async def test_is_live_rejects_a_widget_whose_ancestor_left_the_tree() -> None: """``is_attached`` walks to the DOM root, so a child still parented to a - removed container is correctly dead — the second case ``parent is not None`` + removed container is correctly dead: the second case ``parent is not None`` missed.""" from textual.containers import Vertical diff --git a/tests/test_preview_load_debounce.py b/tests/test_preview_load_debounce.py index 3a7e9af6..a44fe927 100644 --- a/tests/test_preview_load_debounce.py +++ b/tests/test_preview_load_debounce.py @@ -49,7 +49,7 @@ def counted(parent_id: str, *, focus_chunk_seq: int) -> None: original(parent_id, focus_chunk_seq=focus_chunk_seq) app._preview.render_full_doc = counted # type: ignore[method-assign] - app._preview.cancel_pending_load() # settled — no pending window + app._preview.cancel_pending_load() # settled: no pending window app._preview.schedule_load("p1", 0) # Fires synchronously, before any time passes (no wait_until / sleep). assert render_calls == [("p1", 0)], "a settled move must load immediately" @@ -60,7 +60,7 @@ async def test_scan_move_never_mounts_until_cleared( built_index: Path, cfg_with_debounce: Config ) -> None: """Option/Alt + arrow scan: a scan move records the cursor but mounts - NOTHING — not even on a pause (no trailing timer is armed). The preview only + NOTHING, not even on a pause (no trailing timer is armed). The preview only loads once scan mode is cleared (a normal key) and a normal move dispatches. This is the "browse without mounting" behaviour; terminals can't report key release, so a normal key is the portable stand-in for releasing Option.""" @@ -80,7 +80,7 @@ def counted(parent_id: str, *, focus_chunk_seq: int) -> None: app._preview.schedule_load("p1", 0) assert render_calls == [], "a scan move must not load" # No timer is armed either, so a *pause* mid-scan can never mount. (This - # is the deterministic form of "even on a pause" — no sleep to flake on.) + # is the deterministic form of "even on a pause": no sleep to flake on.) assert app._preview.load_timer is None, "a scan move must not arm a load timer" # A normal key clears scan mode; the next move loads immediately (leading). app._preview._scan_move = False @@ -109,7 +109,7 @@ def counted(parent_id: str, *, focus_chunk_seq: int) -> None: app._preview.render_full_doc = counted # type: ignore[method-assign] app._preview.cancel_pending_load() # settled - # Five highlights back-to-back: leading fires p1 now; p2–p5 coalesce. + # Five highlights back-to-back: leading fires p1 now; p2, p5 coalesce. app._preview.schedule_load("p1", 0) app._preview.schedule_load("p2", 0) app._preview.schedule_load("p3", 0) @@ -131,7 +131,7 @@ def counted(parent_id: str, *, focus_chunk_seq: int) -> None: async def test_zero_delay_dispatches_synchronously( built_index: Path, ) -> None: - """When ``preview_load_debounce_ms`` is 0 the load fires inline — + """When ``preview_load_debounce_ms`` is 0 the load fires inline: the legacy / test-time behaviour.""" cfg = Config(defaults=Defaults(preview_load_debounce_ms=0)) app = FNDApp(index_dir=built_index, config=cfg, initial_query="results") @@ -157,7 +157,7 @@ async def test_return_to_cancelled_target_redispatches( Sweeping past a file whose mount is mid-flight cancels that mount; if the cursor then lands back on it, the coalescing latch must not suppress the - remount — the cancelled mount will never land it. Regression: the latch was + remount: the cancelled mount will never land it. Regression: the latch was cleared only on settle/new-query, so returning to a cancelled target was mistaken for "already in flight" and the only dispatch that would mount it was dedup-skipped, hanging the preview until an unrelated nav reset it. @@ -215,7 +215,7 @@ async def test_query_change_cancels_pending_load( app = FNDApp(index_dir=built_index, config=cfg_with_debounce, initial_query="results") async with app.run_test() as pilot: await safe_pause(pilot) - # Don't actually load the stale parents — they have no real chunk data. + # Don't actually load the stale parents: they have no real chunk data. app._preview.render_full_doc = ( # type: ignore[method-assign] lambda parent_id, *, focus_chunk_seq: None ) @@ -241,7 +241,7 @@ async def test_scan_start_cancels_prior_cooldown_timer( built_index: Path, cfg_with_debounce: Config ) -> None: """A scan move started during a prior normal nav's cooldown window cancels - that timer — otherwise the old timer fires and mounts the scanned row on a + that timer, otherwise the old timer fires and mounts the scanned row on a pause, defeating scan mode (review finding).""" app = FNDApp(index_dir=built_index, config=cfg_with_debounce, initial_query="results") async with app.run_test() as pilot: @@ -258,7 +258,7 @@ async def test_scan_start_cancels_prior_cooldown_timer( @pytest.mark.asyncio async def test_non_scan_key_clears_scan_mode(built_index: Path, cfg_with_debounce: Config) -> None: - """A non-Option key (End) ends scan mode so a later move loads — covers + """A non-Option key (End) ends scan mode so a later move loads: covers home/end/pageup/pagedown that the cursor-action overrides don't intercept (review finding). Without this a scan could leave the preview stuck.""" from fnd.tui.widgets.results_tree import ResultsTree diff --git a/tests/test_preview_markdown_widget.py b/tests/test_preview_markdown_widget.py index ac3b144c..52acc5c7 100644 --- a/tests/test_preview_markdown_widget.py +++ b/tests/test_preview_markdown_widget.py @@ -1,6 +1,6 @@ """Structural markdown rendering in the preview pane. -Replaces the legacy ``test_md_match_chunk_uses_per_line_layout`` — +Replaces the legacy ``test_md_match_chunk_uses_per_line_layout``: matched markdown chunks now mount through ``FNDMarkdown`` (Textual's Markdown widget with highlight-aware block subclasses) so tables, code fences, lists, and blockquotes render structurally even when they @@ -30,7 +30,7 @@ async def _settle(pilot) -> None: # type: ignore[no-untyped-def] - """Wait until every mounted FNDMarkdown has finished BUILDING — ``build_done``, + """Wait until every mounted FNDMarkdown has finished BUILDING: ``build_done``, not existence: the block tree these tests assert on is populated asynchronously, so the widget is queryable a tick before its paragraphs.""" @@ -103,7 +103,7 @@ def paragraph_index(tmp_path: Path, tmp_index_dir: Path) -> Path: @pytest.mark.asyncio async def test_preview_md_match_only_highlights_term(cfg: Config, paragraph_index: Path) -> None: """A query that matches one word in a paragraph applies a span over - exactly that word's character range — not the whole paragraph.""" + exactly that word's character range, not the whole paragraph.""" app = FNDApp( index_dir=paragraph_index, config=cfg, @@ -164,7 +164,7 @@ async def test_preview_md_renders_table_with_cell_highlight(cfg: Config, table_i MarkdownTH/MarkdownTD widgets' _content (with our spans already applied) into the MarkdownTableContent grid, so we read the spans off ``_headers`` / ``_rows`` directly rather than querying TH/TD - widgets — the latter aren't mounted as visible widgets, only + widgets: the latter aren't mounted as visible widgets, only referenced as Content sources.""" app = FNDApp( index_dir=table_index, @@ -233,7 +233,7 @@ def fence_index(tmp_path: Path, tmp_index_dir: Path) -> Path: @pytest.mark.asyncio async def test_preview_md_fence_highlights_inside_code(cfg: Config, fence_index: Path) -> None: """Query terms inside a fenced code block DO receive the highlight - overlay (``FNDMarkdownFence``), on top of the syntax colouring — so a + overlay (``FNDMarkdownFence``), on top of the syntax colouring, so a match in code is as findable as one in prose. """ app = FNDApp( @@ -339,7 +339,7 @@ def multi_para_index(tmp_path: Path, tmp_index_dir: Path) -> Path: @pytest.fixture def docx_corpus(tmp_path: Path, tmp_index_dir: Path) -> Path: - """A docx with a heading + bold + a table — exercises the docx + """A docx with a heading + bold + a table: exercises the docx extractor's body_md path end-to-end.""" from docx import Document @@ -363,7 +363,7 @@ def docx_corpus(tmp_path: Path, tmp_index_dir: Path) -> Path: @pytest.fixture def pptx_corpus(tmp_path: Path, tmp_index_dir: Path) -> Path: - """Single-slide deck with a title, bulleted body, and a table — + """Single-slide deck with a title, bulleted body, and a table: exercises the pptx extractor's body_md path end-to-end.""" from pptx import Presentation from pptx.util import Inches @@ -426,7 +426,7 @@ async def test_pptx_preview_routes_through_fnd_markdown(cfg: Config, pptx_corpus @pytest.mark.asyncio async def test_docx_preview_routes_through_fnd_markdown(cfg: Config, docx_corpus: Path) -> None: """A docx chunk's preview mounts an FNDMarkdown widget and - renders the embedded table — proves the renderer dispatch wires + renders the embedded table: proves the renderer dispatch wires the docx kind through the new path.""" from textual.widgets._markdown import MarkdownTable @@ -452,8 +452,8 @@ async def test_update_resets_per_render_match_state() -> None: document-scoped match state: clear ``build_done`` (so a waiter can't return on the previous render) and drop ``first_match_block`` (so match-nav doesn't stay anchored on the old document). Both resets - happen at the top of ``update()`` — synchronously, before the rebuild - completes — so the reset is observable the moment the call returns. + happen at the top of ``update()`` (synchronously, before the rebuild + completes) so the reset is observable the moment the call returns. """ from textual.app import App, ComposeResult @@ -484,7 +484,7 @@ def compose(self) -> ComposeResult: @pytest.mark.asyncio async def test_update_superseded_future_does_not_set_build_done() -> None: """A superseded update's future completing (or being cancelled) must - NOT set ``build_done`` — only the latest update's completion may, or a + NOT set ``build_done``: only the latest update's completion may, or a waiter wakes on a stale/incomplete render. ``AwaitComplete._future`` is a fresh ``gather`` per call and fires its done-callbacks on cancellation too, so a rapid re-update would otherwise let the old future's callback @@ -504,7 +504,7 @@ def compose(self) -> ComposeResult: aw_first = md.update("# A\n\nfirst render alpha.\n") md.update("# B\n\nsecond render beta.\n") assert not md.build_done.is_set() - # Fire the stale (first) update's done-callback directly — without + # Fire the stale (first) update's done-callback directly, without # pumping the loop, so the legitimate second render stays pending. # The guard must keep build_done clear (a newer render is current). stale_future = aw_first._future # type: ignore[attr-defined] diff --git a/tests/test_preview_markers.py b/tests/test_preview_markers.py index 81672dd9..0a7b371b 100644 --- a/tests/test_preview_markers.py +++ b/tests/test_preview_markers.py @@ -2,7 +2,7 @@ Regression for markers that were placed by chunk *ordinal* (a bool per chunk mapped uniformly across the bar), which put a match in a short -chunk following a long one near the top when it actually sat far down — +chunk following a long one near the top when it actually sat far down: off by up to half the bar. The fix weights each chunk by its rendered line count so a match's line fraction tracks its scroll-track fraction. """ @@ -95,7 +95,7 @@ def test_no_marker_when_the_match_is_only_in_unrendered_text() -> None: chunk should be silent. But the marker is a promise that a highlight is down there: scrolling to it showed the user nothing, and the scrollbar disagreed with the pane. The results row carries the ``◌`` flag for exactly - this chunk, so the match is still reported — just not by a marker that + this chunk, so the match is still reported: just not by a marker that can't lead anywhere. """ spec = MatchSpec.from_query("glimmer") diff --git a/tests/test_preview_mount_cancel_strand.py b/tests/test_preview_mount_cancel_strand.py index 28bbce16..37bd0e61 100644 --- a/tests/test_preview_mount_cancel_strand.py +++ b/tests/test_preview_mount_cancel_strand.py @@ -1,11 +1,11 @@ -"""Regression: a cold preview mount cancelled in its early-await phase — -before the detached finalise task is spawned — must not strand the in-flight +"""Regression: a cold preview mount cancelled in its early-await phase, +before the detached finalise task is spawned, must not strand the in-flight latch. The original symptom was "the loading bar gets stuck until I navigate to a different file and back": cancel_mount_task cancels the mount but hid nothing, the finally only hid on is_complete, and no finalise task existed -yet — so the bar stayed up forever AND the inflight latch kept a same-file +yet, so the bar stayed up forever AND the inflight latch kept a same-file re-load from re-dispatching. The bar half of that is now structurally impossible: the progress line is @@ -41,7 +41,7 @@ async def test_cancel_during_early_mount_does_not_strand_progress_bar( app = FNDApp(index_dir=built_index, initial_query="blue penguin sandwich") async with app.run_test() as pilot: await pilot.pause() - assert app._search.groups, "setup — query produced no results" + assert app._search.groups, "setup: query produced no results" preview = app._preview from fnd.tui.widgets.preview_container import PreviewContainer @@ -67,7 +67,7 @@ async def _blocking_cancel_task_on(_container: object) -> None: ) # Mirror the real pre-mount state: pane scroll-locked, latch set, mount - # task running — then drive the structural cold mount directly so + # task running, then drive the structural cold mount directly so # routing (flat vs structural, warm-cache) can't change the path. preview.show_progress_bar(total=len(chunks), phase="mounting…") preview.inflight_target = (g.parent_id, seq) @@ -81,16 +81,16 @@ async def _blocking_cancel_task_on(_container: object) -> None: # Let the task reach and park on the blocked early await. await safe_pause(pilot) assert getattr(container, "_finalise_task", None) is None, ( - "setup — mount must be parked BEFORE the finalise task is spawned" + "setup: mount must be parked BEFORE the finalise task is spawned" ) - # Cancel the mount while parked pre-finalise — the strand condition. + # Cancel the mount while parked pre-finalise: the strand condition. preview.cancel_mount_task() await safe_pause(pilot) await safe_pause(pilot) assert preview.inflight_target is None, ( - "inflight latch not released on cancel — a same-file re-load would " + "inflight latch not released on cancel: a same-file re-load would " "dedup out and never re-mount" ) @@ -102,7 +102,7 @@ async def test_exception_during_early_mount_does_not_strand_progress_bar( built_index: Path, ) -> None: """A mount that FAILS (not cancelled) in its early-await phase must also - clean up. cancel_mount_task nulls mount_task; an exception does not — so + clean up. cancel_mount_task nulls mount_task; an exception does not, so the finally checks `mount_task is current_task()` too, else the bar would strand on any non-cancellation failure (e.g. a MountError).""" import contextlib @@ -110,7 +110,7 @@ async def test_exception_during_early_mount_does_not_strand_progress_bar( app = FNDApp(index_dir=built_index, initial_query="blue penguin sandwich") async with app.run_test() as pilot: await pilot.pause() - assert app._search.groups, "setup — query produced no results" + assert app._search.groups, "setup: query produced no results" preview = app._preview from fnd.tui.widgets.preview_container import PreviewContainer @@ -136,7 +136,7 @@ async def _raising_cancel_task_on(_container: object) -> None: g.parent_id, seq, chunks, container, reset_generation=preview.reset_generation ) ) - preview.mount_task = task # NOT nulled — the mount fails, it isn't cancelled + preview.mount_task = task # NOT nulled: the mount fails, it isn't cancelled # Drain the task (it raises); the finally must still run. with contextlib.suppress(RuntimeError): @@ -152,13 +152,13 @@ async def test_early_cancel_does_not_clobber_successor_latch( ) -> None: """A mount cancelled in its early-await window must NOT clear a SUCCESSOR's latch. The uncached decode path cancels the old mount (nulling mount_task) - without reassigning it — so mount_task being None is NOT enough to prove + without reassigning it, so mount_task being None is NOT enough to prove ownership; the cleanup also checks the inflight latch still points at this target.""" app = FNDApp(index_dir=built_index, initial_query="blue penguin sandwich") async with app.run_test() as pilot: await pilot.pause() - assert app._search.groups, "setup — query produced no results" + assert app._search.groups, "setup: query produced no results" preview = app._preview from fnd.tui.widgets.preview_container import PreviewContainer @@ -187,10 +187,10 @@ async def _blocking_cancel_task_on(_container: object) -> None: ) preview.mount_task = task await safe_pause(pilot) - assert getattr(container, "_finalise_task", None) is None, "setup — must be pre-finalise" + assert getattr(container, "_finalise_task", None) is None, "setup: must be pre-finalise" # A successor (uncached) decode now owns the loading state: a DIFFERENT - # inflight target, and mount_task nulled — exactly what the decode path + # inflight target, and mount_task nulled: exactly what the decode path # leaves behind (cancel_mount_task + show_progress_bar, no mount_task). successor_target = ("successor-parent-id", 7) preview.inflight_target = successor_target diff --git a/tests/test_preview_new_query_strand.py b/tests/test_preview_new_query_strand.py index 58e6155f..87229e49 100644 --- a/tests/test_preview_new_query_strand.py +++ b/tests/test_preview_new_query_strand.py @@ -3,11 +3,11 @@ `search_controller.run()` (new query), `clear_results()` (scope clear) and `rerender_current()` (highlight toggle) all clear the chunk + preview caches, -drop the cached containers from the DOM, and cancel the in-flight mount — on +drop the cached containers from the DOM, and cancel the in-flight mount: on the assumption the cancel takes effect synchronously. It doesn't: the mount task's `finally` runs a tick LATER and unconditionally `preview_cache.put()`s its container back AND leaves it mounted in the pane. So the just-cleared -state is re-polluted with the previous query's half-built container — the +state is re-polluted with the previous query's half-built container: the "stuck mid-mount after a new query" symptom. """ @@ -38,7 +38,7 @@ async def test_new_query_during_inflight_mount_purges_stale_container( app = FNDApp(index_dir=built_index, initial_query="results") async with app.run_test() as pilot: await safe_pause(pilot) - assert app._search.groups, "setup — query produced no results" + assert app._search.groups, "setup: query produced no results" preview = app._preview # Gate the cold structural mount in its early-await window so it sits @@ -66,9 +66,9 @@ async def _blocking_cancel_task_on(_c: object) -> None: ) ) preview.mount_task = task - await safe_pause(pilot) # park on the gate — container now mounted, pre-finalise - assert stale.parent is not None, "setup — parked container should be mounted in DOM" - assert getattr(stale, "_finalise_task", None) is None, "setup — must be pre-finalise" + await safe_pause(pilot) # park on the gate: container now mounted, pre-finalise + assert stale.parent is not None, "setup: parked container should be mounted in DOM" + assert getattr(stale, "_finalise_task", None) is None, "setup: must be pre-finalise" # New query arrives while the mount is parked mid-flight; it clears the # caches + DOM and cancels the parked mount. @@ -102,13 +102,13 @@ async def test_superseded_mount_cancels_detached_finaliser(built_index: Path) -> The cold path spawns ``container._finalise_task`` (``_finalise_via_lock``), which on completion unconditionally hides the bar and clears ``inflight_target``. Cancelling the mount task does not cancel that detached - task — so without the generation-guarded cancel, a stale finaliser fires + task, so without the generation-guarded cancel, a stale finaliser fires after a new query and tears down the new query's loading state. """ app = FNDApp(index_dir=built_index, initial_query="results") async with app.run_test() as pilot: await safe_pause(pilot) - assert app._search.groups, "setup — query produced no results" + assert app._search.groups, "setup: query produced no results" preview = app._preview gate = asyncio.Event() @@ -128,7 +128,7 @@ async def _blocking_cancel_task_on(_c: object) -> None: ) # Stand-in for the detached _finalise_via_lock: parked on a gate; if it - # ever completes it hides the bar + clears the latch — the clobber the + # ever completes it hides the bar + clears the latch: the clobber the # fix must prevent. Attached to the container exactly as the cold path # attaches the real one. finalise_gate = asyncio.Event() diff --git a/tests/test_preview_paint_check.py b/tests/test_preview_paint_check.py index 277be385..a647aa56 100644 --- a/tests/test_preview_paint_check.py +++ b/tests/test_preview_paint_check.py @@ -3,7 +3,7 @@ Prevention at the consumption seams is the real fix for the blank-preview strand (see ``test_preview_condemned_container.py`` and ``fnd/tui/preview/liveness.py``). This is the backstop behind it: armed on every navigation, it verifies the -OUTCOME — is the pane actually showing the cursor's file? — rather than any one +OUTCOME, is the pane actually showing the cursor's file? rather than any one mechanism, so a strand introduced at some future seam costs the user one extra rebuild instead of a pane that stays blank until they navigate away and back. @@ -64,17 +64,17 @@ async def test_paint_check_rebuilds_a_stranded_blank_preview(built_index: Path) await _settled(pilot, preview) target = preview.cursor_target() - assert target is not None, "setup — the cursor should be on a result" + assert target is not None, "setup: the cursor should be on a result" # Strand it exactly as the race did: the active container leaves the DOM # while ``active`` keeps pointing at it. Nothing in the normal pipeline - # repaints this — the reveal watchdog only lifts ``-pre-reveal``. + # repaints this: the reveal watchdog only lifts ``-pre-reveal``. stranded = preview.active assert stranded is not None stranded.remove() for _ in range(6): await safe_pause(pilot) - assert not preview.is_painted(), "setup — the pane should now be blank" + assert not preview.is_painted(), "setup: the pane should now be blank" preview._verify_painted() await wait_until( @@ -111,7 +111,7 @@ def _spy(parent_id: str, *, focus_chunk_seq: int) -> None: @pytest.mark.asyncio async def test_paint_check_repairs_a_preview_showing_the_wrong_file(built_index: Path) -> None: - """The invariant is not merely "something is painted" — it is "the pane + """The invariant is not merely "something is painted": it is "the pane shows the file the cursor is on". A stale dispatch (a late cursor echo, a debounce timer firing for a row the user has already left) paints a different file; that must be corrected too.""" @@ -123,7 +123,7 @@ async def test_paint_check_repairs_a_preview_showing_the_wrong_file(built_index: target = preview.cursor_target() assert target is not None - # A perfectly healthy container — for the wrong file. + # A perfectly healthy container: for the wrong file. other = next( (g for g in app._search.groups if g.parent_id != target[0]), None, @@ -132,7 +132,7 @@ async def test_paint_check_repairs_a_preview_showing_the_wrong_file(built_index: assert preview.active is not None preview.active.parent_doc_id = other.parent_id - assert preview.is_painted(), "setup — the pane is painted, just with the wrong file" + assert preview.is_painted(), "setup: the pane is painted, just with the wrong file" assert preview.showing_parent() != target[0] rebuilt: list[tuple[str, int]] = [] @@ -181,12 +181,12 @@ async def test_paint_check_repairs_at_most_once_per_target(built_index: Path) -> assert target is not None rebuilt: list[str] = [] - # A rebuild that never paints — the pathological case the cap exists for. + # A rebuild that never paints: the pathological case the cap exists for. preview.render_full_doc = lambda parent_id, *, focus_chunk_seq: rebuilt.append( # type: ignore[assignment,method-assign] parent_id ) preview.active = None - assert not preview.is_painted(), "setup — pane is blank" + assert not preview.is_painted(), "setup: pane is blank" for _ in range(5): preview._verify_painted() @@ -213,13 +213,13 @@ async def test_paint_check_defers_while_the_pipeline_is_busy(built_index: Path) parent_id ) preview.active = None - assert not preview.is_painted(), "setup — pane is blank" + assert not preview.is_painted(), "setup: pane is blank" parked = asyncio.Event() task = asyncio.create_task(parked.wait()) preview.mount_task = task try: - assert preview.pipeline_busy(), "setup — a live mount task means busy" + assert preview.pipeline_busy(), "setup: a live mount task means busy" preview._verify_painted() assert not rebuilt, "must not repair while a mount is still in flight" assert preview._paint_check is not None, "the check must re-arm instead" diff --git a/tests/test_preview_paint_check_decode.py b/tests/test_preview_paint_check_decode.py index e420eb3b..8b079649 100644 --- a/tests/test_preview_paint_check_decode.py +++ b/tests/test_preview_paint_check_decode.py @@ -1,7 +1,7 @@ """The paint check must count a running decode as work in flight. ``render_full_doc`` on a chunk-cache miss cancels the mount task, cancels the -debounce timer, and hands the file to a background decode worker — so during a +debounce timer, and hands the file to a background decode worker, so during a cold load none of ``load_timer`` / ``mount_task`` / ``_finalise_task`` is set. ``pipeline_busy()`` looked only at those three, so it reported *idle* while the decode was still running. @@ -9,7 +9,7 @@ That is exactly when the pane legitimately shows the PREVIOUS file (the decode path deliberately keeps the old content up rather than blanking). So after ``PAINT_CHECK_MS`` the check would see "painted, but the wrong file", call it a -strand, and spend its single repair — and the repair re-enters +strand, and spend its single repair, and the repair re-enters ``render_full_doc``, whose worker group is ``exclusive=True``, restarting the very decode that was about to finish. On a slow file (a large PDF, or one evicted to iCloud, where decode is measured in minutes) that both delays the @@ -46,7 +46,7 @@ async def test_a_running_decode_counts_as_busy(built_index: Path) -> None: app = FNDApp(index_dir=built_index, initial_query="apple") async with app.run_test() as pilot: await safe_pause(pilot) - assert app._search.groups, "setup — query produced no results" + assert app._search.groups, "setup: query produced no results" preview = app._preview await wait_until(pilot, preview.is_painted, message="preview never painted") await wait_until( @@ -61,7 +61,7 @@ async def test_a_running_decode_counts_as_busy(built_index: Path) -> None: ) assert target_group is not None, "fixture needs a second file" - # Park the decode on a thread event — the worker runs off the loop. + # Park the decode on a thread event: the worker runs off the loop. gate = threading.Event() original = searcher.get_file_chunks @@ -79,7 +79,7 @@ def _blocking(parent_id: str, **kw: object): # type: ignore[no-untyped-def] await safe_pause(pilot) assert preview.pipeline_busy(), ( - "a running decode is work in flight — reporting idle lets the paint " + "a running decode is work in flight: reporting idle lets the paint " "check spend its one repair on a load that has not finished yet" ) diff --git a/tests/test_preview_pane_teardown_guard.py b/tests/test_preview_pane_teardown_guard.py index bfc05466..64b29dfa 100644 --- a/tests/test_preview_pane_teardown_guard.py +++ b/tests/test_preview_pane_teardown_guard.py @@ -9,8 +9,8 @@ The chain is ``run_worker(..., group="preview-load")`` → ``dispatch_mount`` → ``dispatch_flat_mount`` → ``FlatBufferView.ensure_shared_buffer``, which queried ``#preview_pane`` unguarded. A search dispatched just before the -screen goes away — app quitting, screen swapped, or a test exiting its -``run_test`` block — leaves the worker querying a pane that no longer +screen goes away (app quitting, screen swapped, or a test exiting its +``run_test`` block) leaves the worker querying a pane that no longer exists, and the raise surfaces as a worker crash rather than a no-op. Every sibling call site (``presenter`` line-wrap measurement, ``prefetch``, @@ -18,7 +18,7 @@ this one was simply missed. The fix follows ``_pane``'s convention of returning ``None`` rather than raising. -Timing is not exercised here — the crash is pinned deterministically by +Timing is not exercised here: the crash is pinned deterministically by removing the pane and driving the same code path directly. """ @@ -51,7 +51,7 @@ def flat_index(tmp_path: Path, tmp_index_dir: Path) -> Path: def _drop_pane(app: FNDApp) -> None: - """Remove ``#preview_pane`` outright — the end state of a teardown.""" + """Remove ``#preview_pane`` outright: the end state of a teardown.""" app.query_one("#preview_pane", VerticalScroll).remove() diff --git a/tests/test_preview_prefetch.py b/tests/test_preview_prefetch.py index 291870cc..a16b04ad 100644 --- a/tests/test_preview_prefetch.py +++ b/tests/test_preview_prefetch.py @@ -43,7 +43,7 @@ async def test_prefetch_populates_chunk_cache( await pilot.pause() await run_search(pilot, app, "test") # Wait (wall-clock, not a fixed iteration cap) for the prefetch worker - # to walk its targets — a fixed cap starves under full-suite CI load. + # to walk its targets: a fixed cap starves under full-suite CI load. await wait_until( pilot, lambda: ( @@ -157,7 +157,7 @@ def _flat_parents() -> set[str]: ) if not _flat_parents(): pytest.skip("no flat-path results in fixture corpus for this query") - # Read the signature only once results exist — prefetch keys its cache + # Read the signature only once results exist: prefetch keys its cache # on the signature of the query that produced them. sig = app._search.query_signature() @@ -208,7 +208,7 @@ async def test_prefetch_premounts_structural_container(multi_md_index: Path) -> app = FNDApp(index_dir=multi_md_index, config=cfg, collection="notes") # Prefetch pre-mounts up to the cache size; the shipped default caps the # cache at 1 (a larger cache adds mount/arrange overhead without speeding - # revisits — see PREVIEW_CACHE_MAX_FILES), with the decode still prefetched + # revisits: see PREVIEW_CACHE_MAX_FILES), with the decode still prefetched # for every target. Lift the cap here to exercise the multi-file pre-mount. app._preview.preview_cache.max_files = 8 async with app.run_test() as pilot: @@ -257,7 +257,7 @@ async def test_user_selection_of_prefetched_container_runs_to_completion( ) -> None: """Selecting a prefetched container completes mount up to the background-fill radius (regression for a prefetch/user-side mount - race that stalled at the visible window — narrower than the radius). + race that stalled at the visible window: narrower than the radius). With ``BACKGROUND_FILL_RADIUS = 10`` stages 2a/2b cap mount at ``focus +/- 10``; full-file completion would need a wider radius.""" diff --git a/tests/test_preview_prepend_anchor.py b/tests/test_preview_prepend_anchor.py index f8374c7b..4f742f36 100644 --- a/tests/test_preview_prepend_anchor.py +++ b/tests/test_preview_prepend_anchor.py @@ -1,15 +1,15 @@ """Absorbing a prepend without painting the document at the wrong offset. Mounting chunks ABOVE the viewport pushes everything below them down. Correcting -for that after a settled layout paints frames at the wrong offset first — +for that after a settled layout paints frames at the wrong offset first: measured at 19 of 21 frames during a fill; correcting before it clamps against a virtual size that has not grown and lands short. The pane compensates as the layout lands instead. Anchored on a widget rather than counting the height change: only content ABOVE the anchor moves it, so a background fill appending BELOW is correctly -ignored, and a prepend that arrives in instalments — built chunks resolve their -heights over successive layouts — is absorbed in full. The claim is signed: a +ignored, and a prepend that arrives in instalments, built chunks resolve their +heights over successive layouts, is absorbed in full. The claim is signed: a prune removing chunks above the reader moves them by exactly the same rule. """ @@ -210,7 +210,7 @@ async def test_a_chunk_claim_absorbs_a_prepend_in_its_container() -> None: await pilot.pause() assert pane.scroll_y == 160, ( - f"scroll_y {pane.scroll_y} — a 60-row prepend above the claimed chunk " + f"scroll_y {pane.scroll_y}: a 60-row prepend above the claimed chunk " f"was not absorbed, so the reader was pushed down by it" ) @@ -218,7 +218,7 @@ async def test_a_chunk_claim_absorbs_a_prepend_in_its_container() -> None: @pytest.mark.asyncio async def test_a_block_claim_cannot_see_that_prepend() -> None: """``virtual_region`` is measured against the immediate parent, so a claim on - a block inside the chunk is inert — the reason the swap does not use one.""" + a block inside the chunk is inert: the reason the swap does not use one.""" app = _NestedApp() async with app.run_test(size=(80, 24)) as pilot: pane, _chunk, block = await _nested(pilot, app) @@ -229,7 +229,7 @@ async def test_a_block_claim_cannot_see_that_prepend() -> None: await pilot.pause() assert pane.scroll_y == 100, ( - f"scroll_y {pane.scroll_y} — a block-level claim moved the pane, so the " + f"scroll_y {pane.scroll_y}: a block-level claim moved the pane, so the " f"coordinate space this test pins has changed" ) @@ -285,14 +285,14 @@ async def test_a_swap_leaves_the_claim_on_a_chunk_of_the_revealed_file( assert isinstance(container, PreviewContainer) assert claimed.parent is container, ( f"claim sits on {type(claimed).__name__} whose parent is " - f"{type(claimed.parent).__name__} — only a direct child of the container " + f"{type(claimed.parent).__name__}: only a direct child of the container " f"moves when a prepend lands, so this claim is inert" ) @pytest.mark.asyncio async def test_a_claim_on_content_that_left_the_layout_is_dropped_not_absorbed() -> None: - """``virtual_region`` reports ``Region()`` — y=0, no exception — for a widget + """``virtual_region`` reports ``Region()`` (y=0, no exception) for a widget whose ancestor is ``display: none``. Read as movement, that scrolls the pane backwards by the stored offset: measured 117-187 rows on a real swap.""" app = _NestedApp() @@ -312,7 +312,7 @@ async def test_a_claim_on_content_that_left_the_layout_is_dropped_not_absorbed() assert pane.absorb_anchor is None, "a claim on content out of the layout was kept" assert pane.scroll_y == before, ( - f"scroll_y {pane.scroll_y} (was {before}) — the pane moved for content " + f"scroll_y {pane.scroll_y} (was {before}): the pane moved for content " f"that is no longer laid out" ) @@ -352,7 +352,7 @@ def record(self: Compositor, *args: object, **kwargs: object) -> object: assert painted, "no frame was painted, so this proves nothing" assert set(painted) == {settled}, ( - f"the anchor painted at {sorted(set(painted))} — a 60-row prepend reached " + f"the anchor painted at {sorted(set(painted))}: a 60-row prepend reached " f"the screen before the scroll absorbing it, so the document moved under " f"the reader for a frame (it sits at {settled})" ) @@ -386,7 +386,7 @@ async def test_a_prepend_past_the_old_end_of_the_document_is_absorbed_in_full() await pilot.pause() assert pane.scroll_y == before + 100, ( - f"scroll_y {pane.scroll_y}, wanted {before + 100} — a 100-row prepend was " + f"scroll_y {pane.scroll_y}, wanted {before + 100}: a 100-row prepend was " f"clamped against the document size it was about to replace" ) @@ -396,7 +396,7 @@ async def test_a_claim_is_measured_against_its_container_not_the_pane() -> None: """``absorb_anchor`` is seeded from ``virtual_region.y``, which Textual measures against the immediate PARENT, so the row the absorb compares it against has to be parent-relative too. Measuring from the pane adds the - container's own offset to every delta — invisible while one container is + container's own offset to every delta: invisible while one container is mounted at y=0, and wrong for the whole staging window, which is the only time a second one exists. """ @@ -441,6 +441,6 @@ async def _container(pane: MatchAwareScroll, name: str) -> PreviewContainer: await pilot.pause() assert pane.scroll_y == before + 20, ( - f"scroll moved {pane.scroll_y - before} for a 20-row prepend — the " + f"scroll moved {pane.scroll_y - before} for a 20-row prepend: the " f"container's own offset ({incoming.virtual_region.y}) leaked into the delta" ) diff --git a/tests/test_preview_reveal_guard.py b/tests/test_preview_reveal_guard.py index cd5db037..b0da51cc 100644 --- a/tests/test_preview_reveal_guard.py +++ b/tests/test_preview_reveal_guard.py @@ -51,7 +51,7 @@ def test_reveal_preview_reveals_active_container_and_drops_outgoing() -> None: def test_reveal_preview_is_noop_for_superseded_container() -> None: # A finalise callback fires a tick late after a newer nav swapped in # ``current``. Revealing the stale ``superseded`` would surface the wrong - # file and clobber the new nav's outgoing reference — must be a no-op. + # file and clobber the new nav's outgoing reference: must be a no-op. superseded = _FakeContainer() superseded.add_class("-pre-reveal") current = _FakeContainer() diff --git a/tests/test_preview_reveal_watchdog.py b/tests/test_preview_reveal_watchdog.py index 8812382b..2cd2dac5 100644 --- a/tests/test_preview_reveal_watchdog.py +++ b/tests/test_preview_reveal_watchdog.py @@ -5,7 +5,7 @@ left its preview blank until the user navigated to a different result and back. Root cause: reveal is driven by one specific finalise task that rapid navigation can cancel before it reveals, or hang for seconds awaiting above-window chunks a -cancelled mount never mounted — either leaves the active container invisible; and +cancelled mount never mounted, either leaves the active container invisible; and the scroll-only resume path (``_settled_instant_scroll``) never revealed at all. The fix enforces the invariant "an active container becomes visible within a @@ -85,7 +85,7 @@ def test_reveal_active_reveals_pre_reveal_active_container() -> None: def test_reveal_active_cleans_up_bar_and_latch_when_already_visible() -> None: # Branch-A settle: the container is already visible, but a superseded mount - # may have left the shared bar open / latch set — reveal_active still clears. + # may have left the shared bar open / latch set: reveal_active still clears. c = _FakeContainer() # no -pre-reveal host = _Host(active=c) _reveal_active(host) @@ -110,7 +110,7 @@ async def test_watchdog_reveals_stranded_active_container( ) -> None: """A container activated invisibly with NO finalise to reveal it (the cut-short / hung-finalise case) must be revealed by the bounded-time - watchdog — otherwise it stays invisible forever ('blank until I pick a + watchdog, otherwise it stays invisible forever ('blank until I pick a different result').""" from fnd.tui.preview import tuning @@ -119,7 +119,7 @@ async def test_watchdog_reveals_stranded_active_container( app = FNDApp(index_dir=built_index, initial_query="blue penguin sandwich") async with app.run_test() as pilot: await pilot.pause() - assert app._search.groups, "setup — query produced no results" + assert app._search.groups, "setup: query produced no results" preview = app._preview g = app._search.groups[0] @@ -133,16 +133,16 @@ async def test_watchdog_reveals_stranded_active_container( await pane.mount(container) # Activate invisibly exactly as a cold mount does, but never spawn a - # finalise task — the strand condition (finalise cancelled/hung). + # finalise task: the strand condition (finalise cancelled/hung). preview.activate_container(container, pre_reveal=True) - assert container.has_class("-pre-reveal"), "setup — container should start invisible" + assert container.has_class("-pre-reveal"), "setup: container should start invisible" assert preview.active is container # Nothing else will reveal it. Within REVEAL_WATCHDOG_MS the watchdog must. await asyncio.sleep(0.2) await safe_pause(pilot) assert not container.has_class("-pre-reveal"), ( - "BUG: an active -pre-reveal container was never revealed — the " + "BUG: an active -pre-reveal container was never revealed; the " "reveal watchdog did not fire (preview stays blank until re-nav)" ) @@ -152,7 +152,7 @@ async def test_watchdog_rearms_and_does_not_reveal_during_active_nav( built_index: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """Re-arming: each pre-reveal activation resets the timer, so the watchdog - only fires once the user STOPS on a still-invisible container — it never + only fires once the user STOPS on a still-invisible container: it never interrupts an in-flight navigation by revealing a container prematurely.""" from fnd.tui.preview import tuning @@ -176,7 +176,7 @@ async def test_watchdog_rearms_and_does_not_reveal_during_active_nav( first_timer = preview._reveal_watchdog assert first_timer is not None, "activation must arm the watchdog" # Before the first watchdog would fire, a new nav activates a second - # container — this must re-arm the timer onto the new active one. + # container: this must re-arm the timer onto the new active one. await asyncio.sleep(0.05) second = PreviewContainer( parent_doc_id=g.parent_id, @@ -186,7 +186,7 @@ async def test_watchdog_rearms_and_does_not_reveal_during_active_nav( await pane.mount(second) preview.activate_container(second, pre_reveal=True) # Prove the timer was actually re-armed (a new timer object), not left as - # the stale first one — otherwise this test would pass even if re-arming + # the stale first one, otherwise this test would pass even if re-arming # were broken, since some other path could still reveal `second`. assert preview._reveal_watchdog is not None assert preview._reveal_watchdog is not first_timer, ( diff --git a/tests/test_preview_scroll_characterization.py b/tests/test_preview_scroll_characterization.py index 80b0edf5..ce169017 100644 --- a/tests/test_preview_scroll_characterization.py +++ b/tests/test_preview_scroll_characterization.py @@ -90,7 +90,7 @@ async def test_requery_same_flat_file_lands_on_new_match(built_index: Path) -> N async def test_md_match_in_tall_table_lands_on_matched_row( tmp_path: Path, tmp_index_dir: Path ) -> None: - """A match in a lower row of a tall table scrolls that row on-screen — + """A match in a lower row of a tall table scrolls that row on-screen, not the top of the table. ``scroll_y > 0`` is insufficient: the top-of-table bug satisfies it. Asserts both the recorded coordinate points at the matched cell AND that cell's screen-y is in the pane @@ -202,12 +202,12 @@ def _coldnav_file(label: str) -> str: early-middle section whose match is a prose line a few rows below its heading, preceded by varied content (tables, code) so chunk heights are non-trivial. The query term ``quartzfin`` is UNIQUE to that prose line and - appears in NO heading — so the scroll's match-block resolution is correct; + appears in NO heading, so the scroll's match-block resolution is correct; only the cold-render scroll *position* is at issue.""" lines: list[str] = [f"# {label} Notes", "", "Lead-in overview paragraph.", ""] # A few tall front sections so the match sits ~10-15% down the file by # line count, while staying within the background-fill radius (all - # above-chunks mounted — this is the under-shoot path, not lazy-mount). + # above-chunks mounted: this is the under-shoot path, not lazy-mount). for s in range(8): lines.append(f"## Section {s} overview") lines.append("") @@ -269,7 +269,7 @@ def _coldnav_match_region( app: FNDApp, parent_id: str, focus_seq: int ) -> Callable[[], Region | None]: """Return a probe for the region of the widget holding the unique query - text in the target file — the prose match the cold-nav scroll must land on. + text in the target file: the prose match the cold-nav scroll must land on. The probe returns None until that widget is laid out in the active preview. """ @@ -280,8 +280,8 @@ def _region() -> Region | None: chunk = ap.match_targets.get(focus_seq) or ap.chunk_widgets.get(focus_seq) if chunk is None: return None - # A frozen chunk has no child widgets to walk — that is what freezing - # is — so its match resolves from the row recorded at capture time, the + # A frozen chunk has no child widgets to walk: that is what freezing + # is, so its match resolves from the row recorded at capture time, the # way ``enumerate_stop_rows`` resolves one. Walking children only # made this probe answer None for good once the sweep reached the focus # chunk, and every wait gated on it then ran out its whole budget. @@ -362,7 +362,7 @@ async def test_cold_nav_to_prefetched_non_first_file_lands_on_screen( landed-signal here: the pane carries residual scroll from the previously previewed file AND the prefetched focus chunk is already mounted, so both ``scroll_y > 0`` and the match widget exist long before THIS navigation has - scrolled — asserting then reads the pre-landing position (the flake). The + scrolled, asserting then reads the pre-landing position (the flake). The controller exposes the real landed signal, ``is_settling`` (armed and the nav's scroll has not committed); gating on ``not is_settling`` runs the assert only once the match scroll has landed. The deterministic counterpart @@ -377,13 +377,13 @@ async def test_cold_nav_to_prefetched_non_first_file_lands_on_screen( focus_seq = target_group.hits[0].chunk_seq match_region = _coldnav_match_region(app, target_group.parent_id, focus_seq) - # Navigate the user to the (collapsed, non-first) target file node — - # closest to the real user action — and drive the cold load. + # Navigate the user to the (collapsed, non-first) target file node: + # closest to the real user action, and drive the cold load. rtree.focus() await safe_pause(pilot) rtree.move_cursor(rtree.root.children[1]) - # Gate on the real landed signal — NOT ``scroll_y > 0`` (true from the + # Gate on the real landed signal, NOT ``scroll_y > 0`` (true from the # prior file's residual scroll before this nav moves at all). Once the # controller is no longer settling, the match scroll has committed. await wait_until( @@ -401,12 +401,12 @@ async def test_cold_nav_to_prefetched_non_first_file_lands_on_screen( region = match_region() assert region is not None, "content match widget never laid out" - # The prose match must be inside the pane viewport — scroll_y > 0 alone + # The prose match must be inside the pane viewport: scroll_y > 0 alone # is not enough; the under-shoot scrolls but stops short of the match. top, bottom = pane.region.y, pane.region.y + pane.region.height assert top <= region.y < bottom, ( f"content match at screen y={region.y} is outside the preview " - f"viewport [{top}, {bottom}) — cold-render scroll under-shot, leaving " + f"viewport [{top}, {bottom}): cold-render scroll under-shot, leaving " f"the match below the fold (pane.scroll_y={pane.scroll_y})" ) @@ -422,18 +422,18 @@ async def test_cold_nav_delayed_landing_waits_for_real_settle( The original flake was the test asserting on a fixed-tick settle that expired before this late scroll committed (``scroll_y > 0`` + a mounted - match widget are both true well before THIS nav scrolls — see the sibling + match widget are both true well before THIS nav scrolls: see the sibling test's docstring). Here the finalise settle is delayed so the correcting scroll is guaranteed late; gating on ``not is_settling`` waits it out. We assert only the post-settle invariant (match in viewport): the pre-landing intermediate position is a transient, and sampling it in-suite would itself - be racy — that the weak proxy trips pre-landing is shown by the dev harness + be racy; that the weak proxy trips pre-landing is shown by the dev harness in ``dev/tools/coldnav_timeline.py``. """ app = _build_coldnav_app(tmp_path, tmp_index_dir) # Delay finalise's pre-scroll settle so the correcting match scroll commits - # well after the focus chunk has mounted — forcing the lagging-landing path + # well after the focus chunk has mounted: forcing the lagging-landing path # deterministically instead of relying on load timing. ``await_match_settled`` # is the settle the cold finalise awaits before its single match scroll. # Scoped to the TARGET nav only (enabled after prefetch): a global delay also @@ -478,7 +478,7 @@ def _latching_arm(anchor: object) -> None: monkeypatch.setattr(ctrl, "arm", _latching_arm) - # Arm the delay now — only the target navigation's landing lags. + # Arm the delay now: only the target navigation's landing lags. delay_target_landing = True rtree.move_cursor(rtree.root.children[1]) @@ -509,7 +509,7 @@ def _latching_arm(anchor: object) -> None: # is_settling clears when the scroll is ISSUED, not when it lands: the # scroll may still be animating, and the reveal + the lazy-mount gate it # unblocks both move the viewport afterwards. Gate on the offset holding - # still — a fixed settle here is what let the assertion read mid-glide. + # still: a fixed settle here is what let the assertion read mid-glide. await wait_stable( pilot, lambda: pane.scroll_offset.y, @@ -522,7 +522,7 @@ def _latching_arm(anchor: object) -> None: assert top <= region.y < bottom, ( f"with a delayed finalise, the match at y={region.y} is outside " f"viewport [{top}, {bottom}) after the controller settled " - f"(pane.scroll_y={pane.scroll_y}) — gating on is_settling did not " + f"(pane.scroll_y={pane.scroll_y}): gating on is_settling did not " f"wait for the real landing" ) @@ -535,7 +535,7 @@ async def test_the_landing_probe_survives_the_focus_chunk_freezing( Their waits end in ``match_region() is not None``. A frozen chunk has no child widgets, so a probe that walks children answers None for good the - moment the sweep reaches the focus chunk — and the wait then burns its whole + moment the sweep reaches the focus chunk, and the wait then burns its whole budget on a preview that is correct. """ app = _build_coldnav_app(tmp_path, tmp_index_dir) @@ -567,7 +567,7 @@ async def test_the_landing_probe_survives_the_focus_chunk_freezing( frozen = sum(1 for w in container.chunk_widgets.values() if isinstance(w, FrozenChunkView)) assert frozen, "the sweep froze nothing, so this proves nothing" # The stand-in has to lay out before it can resolve a row, and one pause - # is a wait only while the machine is idle — the defect this whole file + # is a wait only while the machine is idle: the defect this whole file # is about. await wait_until( pilot, @@ -630,7 +630,7 @@ async def _match_parked(pilot: Pilot[None], app: FNDApp, pane: Widget, match_seq landing finishes, and ``is_settling`` clears when the scroll is ISSUED, so neither says the match is on screen yet. Toggling Reading View before it is makes ``locate()`` capture a mid-landing position, and the restore then - faithfully reproduces a position that was never right — which is the whole + faithfully reproduces a position that was never right, which is the whole failure, not a reflow bug.""" await wait_until( pilot, @@ -653,7 +653,7 @@ async def _reading_reflow_landed( controller re-applies its restore across an unbounded number of refreshes. ``since`` is ``restores_completed`` read BEFORE the toggle. Waiting on - ``not is_restoring`` instead would be vacuous — the toggle only schedules + ``not is_restoring`` instead would be vacuous: the toggle only schedules the restore via ``call_after_refresh``, so the flag is still False when the wait first looks and it returns having proved nothing.""" await wait_until( @@ -675,7 +675,7 @@ async def test_reading_view_preserves_match_position(tmp_path: Path, tmp_index_d """Toggling Reading View (full-width reflow) keeps the match on screen when parked on it. The structural reflow re-wraps asynchronously, so the exact top row can drift a chunk; the guarantee is that the match chunk stays in - the viewport (the flat path is exact — see the scrolled-position test).""" + the viewport (the flat path is exact: see the scrolled-position test).""" index = _reading_doc(tmp_path, tmp_index_dir) app = FNDApp(index_dir=index, initial_query="quartzfin-anchor") async with app.run_test(size=(120, 40)) as pilot: @@ -771,7 +771,7 @@ async def test_reading_view_preserves_scrolled_position( tmp_path: Path, tmp_index_dir: Path ) -> None: """When the user has scrolled away from the match, Reading View preserves - THEIR position — not the match.""" + THEIR position, not the match.""" index = _reading_doc(tmp_path, tmp_index_dir) app = FNDApp(index_dir=index, initial_query="quartzfin-anchor") async with app.run_test(size=(120, 40)) as pilot: @@ -790,7 +790,7 @@ async def test_reading_view_preserves_scrolled_position( app._preview_scroll.release() pane.scroll_to(y=max(0, pane.scroll_y // 2), animate=False, immediate=True) # ``before`` is a moving target until the scroll lands, and a tick count - # can flush nothing under load — wait for the geometry itself. + # can flush nothing under load: wait for the geometry itself. await wait_stable( pilot, lambda: (pane.scroll_offset.y, pane.virtual_size.height), diff --git a/tests/test_preview_scroll_controller.py b/tests/test_preview_scroll_controller.py index 439d27fe..3996dd96 100644 --- a/tests/test_preview_scroll_controller.py +++ b/tests/test_preview_scroll_controller.py @@ -131,7 +131,7 @@ def test_reconcile_idempotent_same_target() -> None: c.reconcile() c.reconcile() c.reconcile() - assert strat.calls == [a, a, a] # always the same target — order-independent + assert strat.calls == [a, a, a] # always the same target: order-independent def test_reconcile_threads_on_settled_to_strategy() -> None: @@ -145,7 +145,7 @@ def test_reconcile_threads_on_settled_to_strategy() -> None: def test_reconcile_fires_on_settled_even_when_released() -> None: - # Reveal callbacks ride on on_settled — a released (or strategy-less) + # Reveal callbacks ride on on_settled: a released (or strategy-less) # reconcile must still fire it, or the container is stranded hidden. c = PreviewScrollController(select_strategy=lambda: None) c.release() @@ -297,7 +297,7 @@ def test_structural_strategy_drops_match_a_quarter_down_the_viewport() -> None: def test_a_last_match_landing_takes_a_plain_chunks_final_matching_line() -> None: """A pdf/txt chunk mounts one Static per line and records only its FIRST matching line as the match target, so the backward entry has to find the - last itself — the lines are flat siblings, bounded by the next chunk-first.""" + last itself: the lines are flat siblings, bounded by the next chunk-first.""" lines = [_FakeWidget(Region(0, 100 + i * 2, 80, 2)) for i in range(5)] for w in lines: w.add_class("chunk-line") @@ -575,7 +575,7 @@ def test_controller_is_restoring_until_the_strategy_reports_done() -> None: class _SlowStrategy(FakeStrategy): def scroll_to_location(self, location: ViewportLocation, on_done: object = None) -> None: self.restored.append(location) - held.append(on_done) # not called — the reflow is still running + held.append(on_done) # not called: the reflow is still running strat = _SlowStrategy() c = PreviewScrollController(select_strategy=lambda: strat) @@ -589,7 +589,7 @@ def scroll_to_location(self, location: ViewportLocation, on_done: object = None) def test_controller_is_restoring_clears_when_the_strategy_raises() -> None: - # A raising strategy must not strand the flag — nothing would ever clear it. + # A raising strategy must not strand the flag: nothing would ever clear it. c = PreviewScrollController(select_strategy=lambda: _RaisingStrategy()) c.scroll_to_location(ViewportLocation("flat", line=3)) assert not c.is_restoring @@ -664,7 +664,7 @@ def scroll_to_location(self, location: ViewportLocation, on_done: object = None) def test_reconcile_fires_on_settled_when_strategy_raises_then_reraises() -> None: - # The reveal/swap rides on_settled — it must fire even if the strategy + # The reveal/swap rides on_settled: it must fire even if the strategy # raises before settling, and the (unexpected) error is still surfaced. c = PreviewScrollController(select_strategy=lambda: _RaisingStrategy()) c.arm(ScrollAnchor(parent_id="p", focus_chunk_seq=1)) @@ -690,7 +690,7 @@ def test_scroll_to_location_swallows_strategy_error() -> None: def test_structural_do_scroll_fires_on_done_even_if_scroll_raises() -> None: - # An exception in the scroll body must not drop on_done — otherwise the + # An exception in the scroll body must not drop on_done, otherwise the # pre-reveal container would stay hidden (blank, stuck) on a cold mount. class _BoomPane(_FakePane): def scroll_to_region( @@ -713,7 +713,7 @@ def scroll_to_region( class _CallsThenRaisesStrategy: - """Strategy that calls on_settled itself, THEN raises — the flat-path + """Strategy that calls on_settled itself, THEN raises: the flat-path shape where the synchronous on_settled() is the last thing before any error can escape. The controller's error path must not call it a second time (fire-once contract).""" @@ -750,7 +750,7 @@ def test_scroll_waits_while_content_above_the_match_is_still_arriving() -> None: """The match's screen position is decided by how much content sits above it, and that content arrives late: chunk widgets mount at ~0 height and grow as their markdown lays out. Committing before it settles lands correctly and - then slides — measured at 17 rows on a real book page, leaving the match two + then slides: measured at 17 rows on a real book page, leaving the match two thirds down the pane instead of a quarter. ``build_done`` is NOT the signal to wait on: on that page the chunks above @@ -785,7 +785,7 @@ def test_a_growing_region_above_defers_the_commit() -> None: def test_the_wait_is_bounded_so_a_restless_layout_still_lands() -> None: - """A page whose layout never quite settles must not hold the scroll open — + """A page whose layout never quite settles must not hold the scroll open: landing late is worse than landing a few rows off.""" pane = _FakePane() above = _FakeWidget(Region(0, 0, 80, 27)) @@ -803,7 +803,7 @@ def test_an_unlaid_out_chunk_above_is_not_mistaken_for_settled() -> None: A preceding chunk mounts at height 0 and grows on a later refresh. Reading three zeroes as two stable ticks commits the scroll, and the chunk then lays - out and pushes the match down — the same class of drift the settle gate + out and pushes the match down: the same class of drift the settle gate exists to prevent, entered from the other side. """ pane = _FakePane() @@ -856,7 +856,7 @@ def test_a_frozen_chunk_does_not_get_the_focus_band() -> None: strat.reconcile(ScrollAnchor(parent_id="p", focus_chunk_seq=3)) assert not header.has_class("chunk-section-focused"), ( - "a frozen chunk was given the focus band — it can only paint the padding " + "a frozen chunk was given the focus band: it can only paint the padding " "row the capture does not cover, which is the stray bar above the text" ) @@ -891,7 +891,7 @@ def test_a_frozen_chunk_lands_its_match_a_quarter_down_not_on_the_top_line() -> def test_a_chunk_with_no_captured_row_keeps_the_chunk_top_landing() -> None: - """No recorded row means no match in the chunk — no margin, no context.""" + """No recorded row means no match in the chunk: no margin, no context.""" landing = _frozen_landing(first_match_row=None) assert landing is not None diff --git a/tests/test_preview_scroll_generation.py b/tests/test_preview_scroll_generation.py index 0d4aaa5f..3554e826 100644 --- a/tests/test_preview_scroll_generation.py +++ b/tests/test_preview_scroll_generation.py @@ -2,7 +2,7 @@ The single-anchor controller stopped the *many inline scroll sites* race, but the strategy's ``_do_scroll_to_chunk`` retry chain reschedules itself across -refreshes — so rapid navigation spawns overlapping chains, each pinned to a +refreshes, so rapid navigation spawns overlapping chains, each pinned to a captured chunk seq, all ending in a ``scroll_to_region`` commit. They race; the loser wins. A monotonic generation token (bumped on ``arm``) lets a superseded chain die at its next tick AND immediately before the commit @@ -25,7 +25,7 @@ class _DeferStrategy: - """Captures ``on_settled`` without firing it — models a scroll commit that + """Captures ``on_settled`` without firing it: models a scroll commit that lands a tick later, so a newer ``arm`` can supersede it in between.""" def __init__(self) -> None: @@ -59,7 +59,7 @@ def test_stale_reveal_is_honoured_but_does_not_flip_is_settling() -> None: c.arm(ScrollAnchor("p", 2)) # generation 2 supersedes; gen2 nav in flight assert c.is_settling strat.held[0]() # gen1's deferred commit lands LATE - assert revealed == ["g1"] # reveal still honoured — no stranded hidden container + assert revealed == ["g1"] # reveal still honoured: no stranded hidden container assert c.is_settling # but the stale commit did NOT open gen2's settling gate @@ -74,7 +74,7 @@ def test_current_reveal_flips_is_settling() -> None: def test_stale_do_scroll_does_not_commit_or_reschedule() -> None: # height 0 would normally reschedule the retry chain; a superseded chain must - # instead die on this tick — no scroll, no reschedule, reveal floor honoured. + # instead die on this tick: no scroll, no reschedule, reveal floor honoured. target = _FakeWidget(Region(0, 0, 80, 0)) pane = _FakePane() host = _FakeHost(pane, chunk_widgets={3: target}, match_targets={3: target}) @@ -108,7 +108,7 @@ def test_current_do_scroll_commits() -> None: def test_pre_commit_guard_catches_supersession_after_resolution() -> None: # The target resolves (would commit), but the generation bumps between - # resolution and the commit — the re-check immediately before the side + # resolution and the commit: the re-check immediately before the side # effect must prevent the scroll (the cooperative-cancel window). inner = _FakeWidget(Region(0, 100, 80, 2)) target = _FakeWidget(Region(0, 100, 80, 40), first_match_block=inner) diff --git a/tests/test_preview_scroll_span_anchor.py b/tests/test_preview_scroll_span_anchor.py index 63fd9ec6..73a5cb71 100644 --- a/tests/test_preview_scroll_span_anchor.py +++ b/tests/test_preview_scroll_span_anchor.py @@ -79,7 +79,7 @@ def test_no_offset_when_nothing_matches() -> None: def test_wrapped_prose_anchors_on_the_row_it_wraps_onto() -> None: - """One source line, many rendered rows — the case a newline count misses.""" + """One source line, many rendered rows: the case a newline count misses.""" words = [f"word{i:03d}" for i in range(300)] words[120] = "glimmer" block = cast(Widget, _Block(" ".join(words), 100, width=30)) diff --git a/tests/test_preview_scrolls_to_match.py b/tests/test_preview_scrolls_to_match.py index f0f0c21e..c84fc399 100644 --- a/tests/test_preview_scrolls_to_match.py +++ b/tests/test_preview_scrolls_to_match.py @@ -1,4 +1,4 @@ -"""Preview must scroll to the first match — flat (pdf/txt) and structural (md).""" +"""Preview must scroll to the first match: flat (pdf/txt) and structural (md).""" from __future__ import annotations @@ -223,7 +223,7 @@ async def test_md_preview_scrolls_to_matched_row_inside_tall_table( tmp_path: Path, tmp_index_dir: Path ) -> None: """Regression: a match in a LOWER row of a tall table must scroll the - matched row into view — not stop at the top of the table. + matched row into view, not stop at the top of the table. The W3 table renders every row in one full-height DataTable (a single Rich render, no per-cell widgets, no internal scroll), so scrolling to @@ -234,7 +234,7 @@ async def test_md_preview_scrolls_to_matched_row_inside_tall_table( Two distinct defects are guarded: 1. The matched coordinate must be the cell that actually contains the - query term — not the first cell carrying *any* Content span. Early + query term, not the first cell carrying *any* Content span. Early rows here use inline ``code`` / **bold** styling (as a real flashcards table does); the buggy coord logic picked the first styled cell (near the top) instead of the matched row below. @@ -250,7 +250,7 @@ async def test_md_preview_scrolls_to_matched_row_inside_tall_table( notes.mkdir() lines = ["# Notes", "", "Intro.", "", "| Term | Definition |", "| --- | --- |"] # Early rows carry markdown styling (inline code, bold) so their cell - # Content gains styling spans — the trap the old coord logic fell into. + # Content gains styling spans: the trap the old coord logic fell into. for i in range(40): lines.append(f"| Term{i} | Use `func{i}()` and **bold{i}** in definition {i}. |") lines.append("| Determinism | A Deterministic system always gives the same output. |") @@ -285,7 +285,7 @@ def matched_dt() -> DataTable[object] | None: cell_text = getattr(cell_value, "plain", str(cell_value)) assert "Deterministic" in cell_text, ( f"match coord {coord} points at {cell_text!r}, not the cell " - f"containing the query term — coord resolved to a merely-styled cell" + f"containing the query term: coord resolved to a merely-styled cell" ) # Defect 2: that cell is scrolled on-screen. @@ -294,14 +294,14 @@ def matched_dt() -> DataTable[object] | None: top, bottom = pane.region.y, pane.region.y + pane.region.height assert top <= csy < bottom, ( f"matched table cell at screen y={csy} is outside the preview " - f"viewport [{top}, {bottom}) — scrolled to the top of the table, " + f"viewport [{top}, {bottom}): scrolled to the top of the table, " f"not to the matched row (pane.scroll_y={pane.scroll_y})" ) @pytest.mark.asyncio async def test_md_scroll_with_varied_constructs(tmp_path: Path, tmp_index_dir: Path) -> None: - """Frontmatter, headings, lists, tables, code blocks, blockquotes — + """Frontmatter, headings, lists, tables, code blocks, blockquotes: the match must still find its block.""" notes = tmp_path / "notes" notes.mkdir() @@ -331,7 +331,7 @@ def safe(): | ----- | ----- | | x | y | -## Module 1 — Topic 3 Cybersecurity Attacks +## Module 1: Topic 3 Cybersecurity Attacks Some intro text. @@ -361,8 +361,8 @@ def safe(): @pytest.mark.asyncio async def test_flat_preview_no_jump_on_install(tmp_path: Path, tmp_index_dir: Path) -> None: - """Flat buffer must already be scrolled to the match before first paint - — no flash to file top + jump-to-match.""" + """Flat buffer must already be scrolled to the match before first paint: + no flash to file top + jump-to-match.""" notes = tmp_path / "notes" notes.mkdir() lines = [f"filler line {i}" for i in range(200)] @@ -390,8 +390,8 @@ async def test_flat_preview_no_jump_on_install(tmp_path: Path, tmp_index_dir: Pa async def test_flat_match_lands_a_quarter_down_not_at_top( tmp_path: Path, tmp_index_dir: Path ) -> None: - """Flat (PDF/TXT) match drops ~25% down the viewport — context above it, - consistent with the structural preview — not pinned to the top row. + """Flat (PDF/TXT) match drops ~25% down the viewport (context above it, + consistent with the structural preview) not pinned to the top row. ``scroll_y > 0`` alone can't catch a regression to top-anchoring; assert the match's on-screen row equals the context margin.""" notes = tmp_path / "notes" @@ -429,7 +429,7 @@ async def test_md_preview_scrolls_to_the_row_a_wrapped_match_paints_on( tmp_path: Path, tmp_index_dir: Path ) -> None: """Regression: one source line that WRAPS across the viewport several times - over, with its match late in it — a PDF contents page's shape. + over, with its match late in it: a PDF contents page's shape. ``scroll_y > 0`` is not a sufficient assertion: anchoring on the block's top row satisfies it while leaving the match below the fold, which is the defect. @@ -487,6 +487,6 @@ def painted_screen_y() -> int | None: top, bottom = pane.region.y, pane.region.y + pane.region.height assert top <= y < bottom, ( f"the match paints at screen y={y}, outside the preview viewport " - f"[{top}, {bottom}) — anchored on the block's top row, not the " + f"[{top}, {bottom}): anchored on the block's top row, not the " f"match's (pane.scroll_y={pane.scroll_y})" ) diff --git a/tests/test_preview_single_commit.py b/tests/test_preview_single_commit.py index 6861daca..cc37b976 100644 --- a/tests/test_preview_single_commit.py +++ b/tests/test_preview_single_commit.py @@ -3,7 +3,7 @@ A navigation reconciles more than once: the finalise commits the landing, then the background fill re-anchors after revealing the chunks it mounted above. ``_generation`` only cancels chains from an OLDER navigation, so both ran at the -same generation and neither cancelled the other — two chains, two committed +same generation and neither cancelled the other: two chains, two committed scrolls, and the retry budget spent twice (measured ~62 ticks and >1 commit on 22 of 30 navigations). @@ -74,7 +74,7 @@ def test_second_reconcile_supersedes_the_first_within_one_navigation() -> None: second_token, _ = strategy.calls[1] assert first_token != second_token, "each chain needs its own token to be cancellable" - # The superseded chain landing late must NOT reveal — the newer chain owns it. + # The superseded chain landing late must NOT reveal: the newer chain owns it. _, first_cb = strategy.calls[0] assert first_cb is not None first_cb() diff --git a/tests/test_preview_staged_scrollbar.py b/tests/test_preview_staged_scrollbar.py index 324838a9..10195d12 100644 --- a/tests/test_preview_staged_scrollbar.py +++ b/tests/test_preview_staged_scrollbar.py @@ -1,7 +1,7 @@ """A preview being staged must not move the scrollbar of the one on screen. -A container built behind ``-pre-reveal`` is in the layout — that is how the pane -scrolls to its match before it is revealed — so its rows land in the pane's +A container built behind ``-pre-reveal`` is in the layout (that is how the pane +scrolls to its match before it is revealed) so its rows land in the pane's ``virtual_size`` and, uncorrected, in the bar's. The thumb then shrinks for the two frames a swap takes and snaps back: a blink in the gutter on every navigation. @@ -90,7 +90,7 @@ def compose(self) -> ComposeResult: @pytest.mark.asyncio async def test_the_thumb_survives_a_swap_and_then_describes_the_revealed_file() -> None: """Revealing changes ``opacity`` only, and ``preview/visibility.py`` keeps - that free of layout on purpose — so a staged-row count taken during the + that free of layout on purpose, so a staged-row count taken during the arrange is never told what it counts has been revealed. On the swap's own ordering (hide the outgoing, then reveal) the stale count is the whole document, which subtracts the bar down to nothing: measured as the thumb @@ -129,7 +129,7 @@ async def test_the_thumb_survives_a_swap_and_then_describes_the_revealed_file() async def test_a_scroll_into_the_staged_preview_does_not_pin_the_thumb() -> None: """The swap scrolls into the staged container before revealing it. Correcting the bar's extent while the reader's position sits past the corrected end - leaves the ratio clamped at 1.0 — the thumb hard against the bottom of a + leaves the ratio clamped at 1.0: the thumb hard against the bottom of a track it is nowhere near the end of. """ app = _EmptyPaneApp() @@ -149,7 +149,7 @@ async def test_a_scroll_into_the_staged_preview_does_not_pin_the_thumb() -> None assert size > 0, "no thumb to place" assert top is not None assert top + size < _HEIGHT - 1, ( - f"thumb sits at {top}..{top + size} of {_HEIGHT} — pinned to the end of " + f"thumb sits at {top}..{top + size} of {_HEIGHT}: pinned to the end of " f"the track while the reader is at row 150 of 200" ) @@ -157,7 +157,7 @@ async def test_a_scroll_into_the_staged_preview_does_not_pin_the_thumb() -> None @pytest.mark.asyncio async def test_staging_does_not_give_a_short_preview_a_thumb() -> None: """A preview shorter than the viewport has nothing to scroll, so it has no - thumb — and staging the next file behind it must not conjure one. The + thumb, and staging the next file behind it must not conjure one. The correction has to treat "shorter than the window" as fitting; a predicate that only asks whether the position is below the corrected end quietly excludes every such preview, which is the same gutter blink on short files. @@ -184,7 +184,7 @@ async def test_staging_does_not_give_a_short_preview_a_thumb() -> None: @pytest.mark.asyncio async def test_a_reveal_reaches_the_bar_though_it_causes_no_layout() -> None: """Revealing changes ``opacity`` only, which ``preview/visibility.py`` keeps - free of layout on purpose — so nothing re-runs ``arrange``. With the outgoing + free of layout on purpose, so nothing re-runs ``arrange``. With the outgoing preview left on screen the reveal genuinely lengthens the document, and a staged-row count that waits for a layout keeps describing the shorter one. """ diff --git a/tests/test_preview_stale_highlight_echo.py b/tests/test_preview_stale_highlight_echo.py index 30368e0a..a3d39379 100644 --- a/tests/test_preview_stale_highlight_echo.py +++ b/tests/test_preview_stale_highlight_echo.py @@ -5,7 +5,7 @@ tree and each post highlight events; the earlier rebuild's events can still be in the queue when the later one's have already been handled. ``_load_result_node`` took whatever node the event carried, so a stale echo scheduled a preview load -for a row the user had already left — and because it arrived LAST, last writer +for a row the user had already left, and because it arrived LAST, last writer won: the pane settled showing a file the cursor is not on, permanently. Caught by ``dev/tools/preview_blank_fuzz.py`` (verdict ``wrong-file``), whose @@ -16,7 +16,7 @@ RENDER parent=a63e874a cursor=f9b9da1b/6 A highlight event whose node is no longer the tree's cursor is stale by -definition — the cursor moving to a node is what posts the event in the first +definition: the cursor moving to a node is what posts the event in the first place. """ @@ -50,12 +50,12 @@ async def test_stale_highlight_echo_does_not_schedule_a_load(built_index: Path) app = FNDApp(index_dir=built_index, initial_query="apple") async with app.run_test() as pilot: await safe_pause(pilot) - assert app._search.groups, "setup — query produced no results" + assert app._search.groups, "setup: query produced no results" tree = app.query_one("#results_pane", Tree) current = tree.cursor_node - assert current is not None, "setup — the cursor should be parked on a row" - assert isinstance(current.data, dict), "setup — the cursor row is a result row" + assert current is not None, "setup: the cursor should be parked on a row" + assert isinstance(current.data, dict), "setup: the cursor row is a result row" stale = next( ( n @@ -75,7 +75,7 @@ async def test_stale_highlight_echo_does_not_schedule_a_load(built_index: Path) # cursor any more. app._on_tree_highlight(Tree.NodeHighlighted(stale)) # type: ignore[arg-type] assert not scheduled, ( - "a highlight event for a node the cursor has already left is stale — " + "a highlight event for a node the cursor has already left is stale: " "loading it makes the preview show a file the user is not on" ) @@ -86,7 +86,7 @@ async def test_stale_highlight_echo_does_not_schedule_a_load(built_index: Path) @pytest.mark.asyncio async def test_enter_still_loads_the_selected_row(built_index: Path) -> None: - """``NodeSelected`` (Enter) is an explicit user action on the row it names — + """``NodeSelected`` (Enter) is an explicit user action on the row it names: it must keep loading even though it routes through the same helper.""" app = FNDApp(index_dir=built_index, initial_query="apple") async with app.run_test() as pilot: diff --git a/tests/test_preview_visibility.py b/tests/test_preview_visibility.py index 0af3dc6f..faa8d8de 100644 --- a/tests/test_preview_visibility.py +++ b/tests/test_preview_visibility.py @@ -2,7 +2,7 @@ `fnd.tui.preview.visibility` shows and hides preview containers without walking their contents to re-apply CSS. That is only sound while the two visibility -classes are never used as an ANCESTOR in a selector — the moment a rule like +classes are never used as an ANCESTOR in a selector: the moment a rule like ``.-hidden Foo`` exists, descendants really can match differently and skipping them would leave stale styling that no amount of staring at the CSS explains. @@ -82,7 +82,7 @@ async def test_setting_visibility_applies_the_style_to_the_node(tmp_index_dir: P set_preview_visibility(container, hidden=True) assert container.has_class("-hidden") assert container.styles.display == "none", ( - "the class was set but its style never applied — the node's own " + "the class was set but its style never applied: the node's own " "restyle was skipped along with its descendants" ) @@ -93,8 +93,8 @@ async def test_setting_visibility_applies_the_style_to_the_node(tmp_index_dir: P @pytest.mark.asyncio async def test_a_no_op_toggle_does_no_work(tmp_index_dir: Path) -> None: - """Called every activation over every container, so the common case — the - class is already right — must not reach the stylesheet at all.""" + """Called every activation over every container, so the common case: the + class is already right: must not reach the stylesheet at all.""" app = FNDApp(index_dir=tmp_index_dir, initial_query="anything") async with app.run_test(size=(80, 24)): container = PreviewContainer(parent_doc_id="x", query_signature="y", total_chunks=1) @@ -157,7 +157,7 @@ async def test_reveal_repaints_a_translucent_background_inside_the_container( # Properties a shortcut class may set. Anything outside this changes the geometry # descendants are laid out against, and the descendant walk is what re-arranges -# them — so skipping it would leave the subtree sized for the old layout. That is +# them, so skipping it would leave the subtree sized for the old layout. That is # not hypothetical: `is-loading` used to set `scrollbar-size-vertical`, and # shortcutting it left tables laid out at the pre-scrollbar width, so a match # inside a tall table could not be scrolled to and the off-screen indicator never @@ -169,7 +169,7 @@ async def test_reveal_repaints_a_translucent_background_inside_the_container( # # ANCESTOR_RULES are allowed only because the shortcut busts descendant style # caches when one of them moves. Spelling the set that way is what makes a new -# inherited rule — one the bust does not cover — fail here. +# inherited rule, one the bust does not cover, fail here. EXACT_ALLOWED = {"display", "visibility", *ANCESTOR_RULES} ALLOWED_PREFIXES = ("scrollbar_color", "scrollbar_background", "auto_scrollbar_") @@ -204,7 +204,7 @@ async def test_shortcut_classes_change_visibility_only(tmp_index_dir: Path) -> N extra = {name for name in declared if _is_geometric(name)} assert not extra, ( f"a shortcut class now sets {sorted(extra)}, which reaches past the " - f"node — either into the geometry descendants are laid out against, " + f"node, either into the geometry descendants are laid out against, " f"or into the styles they cache. fnd/tui/preview/visibility.py skips " f"the descendant walk, so they would keep the old value. Either move " f"the property elsewhere, or, if descendants only CACHE it, add it to " @@ -217,7 +217,7 @@ def test_the_shortcut_never_asks_for_a_subtree_restyle() -> None: """The invariant the whole module exists for, and the one nothing checked. Dropping ``update=False`` costs nothing visible: the class still lands, the - style still applies, the no-op still no-ops — every other test here passes. + style still applies, the no-op still no-ops, every other test here passes. What comes back is `App.update_styles` walking hundreds of descendants on every activation, which is the 108-of-110-samples cost this module removed. """ @@ -268,7 +268,7 @@ def test_the_shortcut_busts_descendant_caches_only_when_it_must() -> None: set_preview_visibility(node, hidden=True, pre_reveal=True) assert [d.notified for d in fake.descendants] == [1, 1], "opacity moved; nothing was busted" assert [d.refreshed for d in fake.descendants] == [1, 1], ( - "cleared the cache but left the strips it rendered — the repaint is what " + "cleared the cache but left the strips it rendered: the repaint is what " "puts the new colour on screen" ) diff --git a/tests/test_preview_worker.py b/tests/test_preview_worker.py index d038232a..005af01a 100644 --- a/tests/test_preview_worker.py +++ b/tests/test_preview_worker.py @@ -81,7 +81,7 @@ async def test_preview_load_dispatches_worker_on_cache_miss( # Cache hit path: no new worker, no progress, no spinner. before_workers = len(app.workers) app._preview.render_full_doc(big_group.parent_id, focus_chunk_seq=0) - # A cache hit dispatches nothing, so there is no outcome to wait for — + # A cache hit dispatches nothing, so there is no outcome to wait for: # one drain is the whole point, and the assertions are about absence. await pilot.pause() assert app._preview.load_progress is None @@ -137,7 +137,7 @@ async def test_progress_strip_runs_determinate_then_hides_on_complete( assert active_session.total >= 1 # The line now holds a completed fill for a minimum duration before it # clears (so short loads read as "done" rather than flashing), so a - # fixed number of pauses is no longer a sound way to wait for it — + # fixed number of pauses is no longer a sound way to wait for it: # under load it under-waits, and it says nothing about WHY it cleared. # Gate on the widget's own state instead. await wait_until( @@ -169,7 +169,7 @@ async def test_switching_files_mid_load_cancels_mount_task( first_task: Any = app._preview.mount_task # Switch to small.md before big's mount finishes. app._preview.render_full_doc(small_group.parent_id, focus_chunk_seq=0) - # task.cancel() only *requests* cancellation — the task settles to + # task.cancel() only *requests* cancellation: the task settles to # cancelled()/done() one loop tick later, so asserting those here # races that settle (flakes under load). cancelling() flips to >0 # synchronously on cancel and stays until the task finishes, so it @@ -190,10 +190,10 @@ async def test_switching_files_mid_load_cancels_mount_task( @pytest.mark.asyncio async def test_repeat_visit_uses_cached_widgets(cfg: Config, two_file_index: Path) -> None: """A previously-mounted file (with chunk count above the cache - threshold) should NOT remount on revisit — its PreviewContainer + threshold) should NOT remount on revisit: its PreviewContainer stays in the LRU and a return visit is an O(1) class flip.""" app = FNDApp(index_dir=two_file_index, config=cfg, collection="notes") - # The shipped cache caps at 1 (see _PREVIEW_CACHE_MAX_FILES) — leaving a file + # The shipped cache caps at 1 (see _PREVIEW_CACHE_MAX_FILES), leaving a file # then returning rebuilds rather than reusing its container, which measured # faster (a larger cache adds arrange overhead without a faster revisit). # Lift the cap here to exercise the LRU-reuse path this test guards. @@ -203,7 +203,7 @@ async def test_repeat_visit_uses_cached_widgets(cfg: Config, two_file_index: Pat await run_search(pilot, app, "target") big_group = next(g for g in app._search.groups if g.path.endswith("big.md")) small_group = next(g for g in app._search.groups if g.path.endswith("small.md")) - # First visit to big — fully mount. Gated on the cache entry appearing + # First visit to big: fully mount. Gated on the cache entry appearing # rather than on a tick count: a fixed number of pauses is a wait only # while the machine is idle, and degrades to a no-op under suite load, # which is exactly when this test used to fail. @@ -235,7 +235,7 @@ async def test_repeat_visit_uses_cached_widgets(cfg: Config, two_file_index: Pat timeout=30.0, message="never switched away to small.md", ) - # Return to big — the strip shows briefly during the cache-hit reveal + # Return to big: the strip shows briefly during the cache-hit reveal # cycle, then idles once _finalise_pre_reveal's on_done fires. Wait for # BOTH signals together: idling is what says the reveal finished, and # checking it on a tick count is what made this flaky. @@ -267,7 +267,7 @@ async def test_rapid_file_switching_does_not_raise_duplicate_ids( await run_search(pilot, app, "target") small_group = next(g for g in app._search.groups if g.path.endswith("small.md")) big_group = next(g for g in app._search.groups if g.path.endswith("big.md")) - # Toggle several times in quick succession — each call cancels + # Toggle several times in quick succession: each call cancels # the in-flight mount and starts a new one. None of these calls # should raise. for _ in range(5): @@ -284,7 +284,7 @@ async def test_rapid_file_switching_does_not_raise_duplicate_ids( async def test_preview_title_no_longer_carries_progress_text( cfg: Config, two_file_index: Path ) -> None: - """The border title stays clean — never says 'loading N/M chunks'. + """The border title stays clean: never says 'loading N/M chunks'. Progress lives on the ProgressBar widget instead.""" app = FNDApp(index_dir=two_file_index, config=cfg, collection="notes") async with app.run_test() as pilot: @@ -298,7 +298,7 @@ async def test_preview_title_no_longer_carries_progress_text( assert "chunks" not in title.lower() # After load the title shows the file basename. Gated on that, not on # three pauses: the load is a decode worker and a mount, and Windows - # finished neither in three ticks — four attempts, four failures, the + # finished neither in three ticks, four attempts, four failures, the # title still naming the cursor-parked file. await wait_until( pilot, diff --git a/tests/test_progress_calibration.py b/tests/test_progress_calibration.py index fe0c32d6..b43d9d33 100644 --- a/tests/test_progress_calibration.py +++ b/tests/test_progress_calibration.py @@ -3,7 +3,7 @@ The calibrator is what stops the timed phases being a guess: the seeds ship with the code, but after a few runs the bar is paced by what this machine actually did. These tests pin the summarising rules and the -failure behaviour — telemetry must never break a caller. +failure behaviour: telemetry must never break a caller. """ from __future__ import annotations @@ -40,7 +40,7 @@ def test_a_recorded_run_becomes_the_expectation() -> None: def test_the_median_rides_out_an_outlier() -> None: """A single cold-cache monster must not leave the bar crawling for - every normal navigation afterwards — which a mean would do.""" + every normal navigation afterwards, which a mean would do.""" for ms in (100.0, 110.0, 90.0, 105.0, 8000.0): calibration.record("op", {"a": ms}) assert calibration.expected_ms("op")["a"] == pytest.approx(105.0) @@ -116,7 +116,7 @@ def test_history_is_bounded(isolated_progress_calibration: Path) -> None: def test_a_non_finite_duration_is_ignored(isolated_progress_calibration: Path) -> None: """``json.loads`` accepts ``Infinity`` and ``NaN``, and an infinite value sails past a bare ``>=`` filter. It then reaches ``OperationPlan.weights``, - where ``inf / inf`` is ``nan`` and the bar stops advancing entirely — a + where ``inf / inf`` is ``nan`` and the bar stops advancing entirely: a corrupt line would freeze the line rather than be skipped. Written after the guard was added to ``record`` but silently not to the @@ -151,7 +151,7 @@ def test_a_record_missing_a_field_is_skipped_not_raised( isolated_progress_calibration: Path, ) -> None: """A line can be valid JSON and still lack a key, and KeyError is a - subclass of none of the other suppressed types — so it escaped ``_load``. + subclass of none of the other suppressed types, so it escaped ``_load``. That path runs from ``ProgressFacility.begin``, and ``SearchController.run`` does not guard that call: one corrupt history line broke the next search. diff --git a/tests/test_progress_index_tracker.py b/tests/test_progress_index_tracker.py index 2f5dfb1d..8e7ddf36 100644 --- a/tests/test_progress_index_tracker.py +++ b/tests/test_progress_index_tracker.py @@ -176,8 +176,8 @@ async def test_a_background_run_opens_an_ambient_session( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """Everything above tests the tracker against stand-ins, which cannot see - whether the service ever calls it. A background run — ``open_modal=False``, - which is what auto-resume starts on launch — is the case with no other + whether the service ever calls it. A background run (``open_modal=False``, + which is what auto-resume starts on launch) is the case with no other indication at all, so this pins the one line that connects them. The session must be AMBIENT: an interactive one is retired by the first @@ -201,7 +201,7 @@ async def test_a_background_run_opens_an_ambient_session( started = app._indexer.start( collection="default", config=cfg.collections["default"], open_modal=False ) - assert started, "setup — the indexer did not start" + assert started, "setup: the indexer did not start" session = app._progress.ambient assert session is not None, "a background index reported nothing on the line" assert session.operation_id == "index" diff --git a/tests/test_progress_line.py b/tests/test_progress_line.py index c0cd558a..dce6232b 100644 --- a/tests/test_progress_line.py +++ b/tests/test_progress_line.py @@ -45,8 +45,8 @@ def painted(width: int, fraction: float, label: str = "") -> str: def split(width: int, fraction: float, label: str = "") -> tuple[int, int]: """(filled cells, track cells). - Fill and track are drawn with the SAME glyph — they differ only in - colour — so the painted text cannot tell them apart. Classify by style + Fill and track are drawn with the SAME glyph (they differ only in + colour) so the painted text cannot tell them apart. Classify by style instead, which is what actually carries the distinction on screen. """ segments = progress_line_segments( @@ -87,7 +87,7 @@ def test_a_complete_line_is_all_fill() -> None: def test_any_progress_at_all_shows_at_least_one_cell() -> None: """1% of a 200-cell line rounds to 2 cells, but 1% of a 40-cell line - rounds to 0 — and a bar that reads as empty while work is happening is + rounds to 0, and a bar that reads as empty while work is happening is the complaint, not the fix.""" assert split(40, 0.001)[0] >= 1 @@ -299,7 +299,7 @@ def test_open_still_drives_a_determinate_bar() -> None: async def test_the_mounted_widget_renders_a_full_width_line( fixtures_dir: Path, tmp_index_dir: Path ) -> None: - """The pure function is tested above; this covers the part it cannot — + """The pure function is tested above; this covers the part it cannot: resolving the component styles from CSS and sizing to the real pane width. Asserts on the widget's own render output rather than the screen: a headless compositor paints blank regardless.""" @@ -441,7 +441,7 @@ def test_the_watchdog_is_disarmed_on_a_normal_clear() -> None: def test_an_active_session_never_paints_a_full_line() -> None: """A full line means finished. A session whose phases have all eased out - is not finished, so it must still show a gap — otherwise a stall there + is not finished, so it must still show a gap, otherwise a stall there looks identical to a stall in the clear.""" facility, bar, clock = make_facility() facility.begin(ONE_PHASE, sampler=lambda _s: True) @@ -485,7 +485,7 @@ def test_a_changing_label_counts_as_movement() -> None: def test_an_unchanged_frame_costs_nothing() -> None: """``fraction`` is a Textual reactive, so assigning it repaints the row. On a slow phase the eased value changes by a fraction of a percent per - tick — the same cells — so painting unconditionally spent 20 repaints a + tick (the same cells) so painting unconditionally spent 20 repaints a second drawing the identical thing, event-loop work during exactly the navigation this line exists to smooth. Measured on a real 3 s session: 61 repaints before this guard, 11 after.""" @@ -527,7 +527,7 @@ def test_the_stall_cap_retires_before_the_watchdog() -> None: """The tick loop is the normal retirement path; the watchdog only covers a tick loop that is gone. With the watchdog set below the stall cap it beat the loop to every retirement, so the stall branch was dead code and every - case went through _force_clear — a superseded retirement, which skips both + case went through _force_clear: a superseded retirement, which skips both the completion animation and the calibration sample.""" from fnd.tui.progress import facility as facility_mod @@ -547,7 +547,7 @@ def test_the_stall_cap_retires_before_the_watchdog() -> None: def test_the_watchdog_survives_a_widget_it_cannot_resolve() -> None: - """query_one fails whenever a modal screen is on top — which is exactly + """query_one fails whenever a modal screen is on top, which is exactly what a background index sits behind. Dropping the backstop there would retire it for the rest of the session, since only real progress re-arms it and a stalled session makes none.""" @@ -556,7 +556,7 @@ def test_the_watchdog_survives_a_widget_it_cannot_resolve() -> None: facility.begin(ONE_PHASE, sampler=lambda _s: True) def no_widget(_selector: object) -> object: - raise RuntimeError("NoMatches — a modal is on top") + raise RuntimeError("NoMatches: a modal is on top") app.query_one = no_widget # type: ignore[method-assign] before = len(app.watchdogs) @@ -608,7 +608,7 @@ def test_a_background_run_gets_the_line_back_after_a_navigation() -> None: With one slot, ``begin`` was last-writer-wins: every navigation retired the running index permanently. A reindex spans hundreds of navigations, so in practice the line showed it until the user's first keypress and - then never again — and at launch the initial query beat it to that. + then never again, and at launch the initial query beat it to that. """ facility, bar, clock = make_facility() index = facility.begin(AMBIENT_PLAN, label="Work · 3 of 40 files", sampler=lambda _s: True) @@ -625,7 +625,7 @@ def test_a_background_run_gets_the_line_back_after_a_navigation() -> None: def test_a_background_run_that_ended_unseen_does_not_come_back() -> None: """Sampled before it repaints, so a run that finished while the line was - busy elsewhere neither flashes a stale bar nor announces itself late — + busy elsewhere neither flashes a stale bar nor announces itself late: the same rule as one that ends while still suspended, which is what keeps the two indistinguishable to the user.""" facility, bar, clock = make_facility() @@ -656,7 +656,7 @@ def test_a_background_run_finishing_unseen_does_not_steal_the_completion() -> No def test_a_background_run_outlasts_a_burst_of_navigation() -> None: """The stall cap counts silence, and a suspended session is silent by - construction — so resuming has to forgive the gap it did not cause.""" + construction, so resuming has to forgive the gap it did not cause.""" facility, bar, clock = make_facility() index = facility.begin(AMBIENT_PLAN, label="Work", sampler=lambda _s: True) for _ in range(30): diff --git a/tests/test_progress_model.py b/tests/test_progress_model.py index 30ad6bf1..46fe8cd6 100644 --- a/tests/test_progress_model.py +++ b/tests/test_progress_model.py @@ -1,6 +1,6 @@ """Progress model: weights, easing, monotonicity, measurement. -Pure unit tests — no Textual, no app, no clock of their own. The model +Pure unit tests: no Textual, no app, no clock of their own. The model takes an injected clock so every timing assertion here is exact rather than a race against wall time. """ @@ -76,7 +76,7 @@ def test_recalibration_keeps_seeds_for_unmeasured_phases() -> None: def test_a_timed_phase_reads_nearly_full_when_it_runs_to_time() -> None: """The estimate coming true should look like it. An earlier curve read HALF the phase at its expected duration, so even a perfectly calibrated - operation finished with the bar around 50% — measured on the flat path, + operation finished with the bar around 50%: measured on the flat path, a median fill at completion of 0.167. That is the "pauses partway" complaint, caused by the curve rather than by the estimates.""" clock = FakeClock() @@ -94,7 +94,7 @@ def test_a_timed_phase_is_proportional_while_it_runs_to_time() -> None: def test_a_timed_phase_keeps_a_fat_tail() -> None: """The property the curve was chosen for. Doubling and quadrupling the - elapsed time must still move the bar by an amount a user can see — an + elapsed time must still move the bar by an amount a user can see: an exponential's tail is flat here, which froze the line for over a second on a phase that overran.""" clock = FakeClock() diff --git a/tests/test_progress_navigation_session.py b/tests/test_progress_navigation_session.py index 26f3ca67..dd73bf21 100644 --- a/tests/test_progress_navigation_session.py +++ b/tests/test_progress_navigation_session.py @@ -62,19 +62,19 @@ async def _search(pilot: Pilot[None], app: FNDApp) -> tuple[FileGroup, FileGroup @pytest.mark.asyncio async def test_a_navigation_opens_a_session_straight_away(two_file_index: Path) -> None: - """The line must belong to the keypress that caused it — not appear + """The line must belong to the keypress that caused it, not appear once some later stage happens to get round to showing it.""" app = FNDApp(index_dir=two_file_index) async with app.run_test() as pilot: await pilot.pause() _small, big = await _search(pilot, app) # A committed search parks the cursor, which dispatches a preview load - # of its own — so wait for that to land before testing a navigation. + # of its own, so wait for that to land before testing a navigation. await wait_until(pilot, lambda: app._progress.active is None) # Make the navigation cold, don't hope for it. That parked load starts # a coverage sweep which decodes and captures NEIGHBOURS, and both are - # inputs to the warm/cold plan — so whether this file is still cold when + # inputs to the warm/cold plan, so whether this file is still cold when # the test gets here is the runner's timing, not the test's setup. _make_cold(app, big.parent_id) @@ -108,7 +108,7 @@ async def test_a_jump_inside_the_open_file_is_a_warm_navigation( two_file_index: Path, ) -> None: """Cold and warm navigations differ by more than an order of magnitude, - so they calibrate separately — pooling them would make both estimates + so they calibrate separately: pooling them would make both estimates useless.""" app = FNDApp(index_dir=two_file_index) async with app.run_test() as pilot: @@ -162,7 +162,7 @@ async def test_a_background_index_survives_a_navigation_in_the_real_app( made the line useless for the one operation long enough to need it. Driven through the real app so the second visual channel is exercised - too — an unresolved component class raises at render time, and the + too: an unresolved component class raises at render time, and the stub bar cannot see that. """ app = FNDApp(index_dir=two_file_index) diff --git a/tests/test_progress_navigation_shape.py b/tests/test_progress_navigation_shape.py index ec2b910c..32fcee61 100644 --- a/tests/test_progress_navigation_shape.py +++ b/tests/test_progress_navigation_shape.py @@ -98,7 +98,7 @@ def is_painted(self) -> bool: return self._stage is None def file_warm_state(self, _parent_id: str) -> Any: - """No capture store here, so readiness is unknowable — which is what + """No capture store here, so readiness is unknowable, which is what the real presenter returns before the pane has a width. The plan then falls back to the chunk cache, as it did before coverage existed.""" return None @@ -128,7 +128,7 @@ def done() -> bool: class _Container: def __init__(self, parent_doc_id: str = "previous-file") -> None: - # The pane is showing a DIFFERENT file when the navigation starts — + # The pane is showing a DIFFERENT file when the navigation starts: # that is what makes this a cold navigation. Leaving it as "doc" had # the tracker pick the warm plan, whose seeds total 180 ms, and then # every phase of this 2.3 s schedule overran by an order of magnitude. @@ -264,21 +264,21 @@ def test_an_instant_navigation_still_shows_a_complete_line() -> None: # A file far slower than anything recorded before: the build phase runs five -# times its seed. This is the worst case for a timed phase — the first time +# times its seed. This is the worst case for a timed phase: the first time # this machine ever meets such a file, nothing has been calibrated. OVERRUN_SCHEDULE = [("decode", 0.30), ("mount", 0.25), ("build", 3.50), ("land", 0.55)] # What the curve can honestly guarantee on that first, uncalibrated encounter. # # Raised from 0.60s deliberately, and the trade is worth stating. The fill now # reads ~0.8 of a phase when it runs to its estimate rather than ~0.5, which is -# what stopped correctly-paced operations finishing with the bar partway — the +# what stopped correctly-paced operations finishing with the bar partway: the # reported bug. Paying for that leaves less headroom in the tail, so a phase # running FIVE TIMES its estimate, on a machine that has never seen that file # before, can hold the same cell for about a second. # # That is bounded, it only applies before calibration has a sample, and # test_the_second_visit_is_paced_by_the_first pins the improvement on the -# next encounter. The alternative — more tail range — makes every on-time +# next encounter. The alternative, more tail range, makes every on-time # completion read lower, which is the worse failure. MAX_DEAD_UNCALIBRATED_S = 1.10 diff --git a/tests/test_progress_preview_tracker.py b/tests/test_progress_preview_tracker.py index 56b64821..fc170a62 100644 --- a/tests/test_progress_preview_tracker.py +++ b/tests/test_progress_preview_tracker.py @@ -2,7 +2,7 @@ The tracker reads the pipeline rather than being called from inside it, so these tests drive it against stand-ins for the signals it samples. -The important one is ``test_mount_is_measured_against_the_window`` — +The important one is ``test_mount_is_measured_against_the_window``: dividing by the whole file's chunk count is exactly why the old bar topped out around 1% on a large PDF. """ @@ -63,12 +63,12 @@ def __init__(self) -> None: # The flat path (PDF/TXT) shows a file without setting ``active``. self.flat_parent: str | None = None self.busy = False - # Whether the pane is showing readable content — not a container still + # Whether the pane is showing readable content, not a container still # behind ``-pre-reveal``. The tracker ends a navigation on this. self.painted = False # Per-file readiness, as the presenter answers it. None means "cannot - # be answered yet" — before layout there is no capture width. + # be answered yet": before layout there is no capture width. self.warm: WarmState | None = None def is_painted(self) -> bool: @@ -193,7 +193,7 @@ def test_an_uncommitted_scroll_puts_us_in_land( def test_a_navigation_that_skips_the_decode_still_advances( app: StubApp, tracker: PreviewProgressTracker ) -> None: - """Cached chunks mean no decode at all — the phase must be retired, + """Cached chunks mean no decode at all: the phase must be retired, not waited on.""" session = tracker.begin("doc") app._preview.mount_task = StubTask(done=False) @@ -320,12 +320,12 @@ def test_a_scroll_that_never_commits_is_bounded_by_the_reveal_budget( ) -> None: """dispatch_mount has paths that cancel and rebuild without reconciling, so is_settling can stay true for good. The land phase is therefore bounded by - how long the app itself allows a reveal to take — past that the navigation + how long the app itself allows a reveal to take: past that the navigation is over whatever the scroll controller believes. Gating this on inflight_target instead (an earlier attempt) made the land phase UNREACHABLE, because reveal_active clears that latch before the - scroll commits — which capped every cold navigation near 20-40%. + scroll commits, which capped every cold navigation near 20-40%. """ from fnd.tui.preview import tuning @@ -359,7 +359,7 @@ def test_the_land_phase_is_reachable_at_all(app: StubApp, tracker: PreviewProgre def test_a_jump_inside_an_open_pdf_is_warm(app: StubApp, tracker: PreviewProgressTracker) -> None: """The flat path installs into one shared buffer and leaves ``active`` as - None, so reading ``active`` classified every PDF navigation as cold — + None, so reading ``active`` classified every PDF navigation as cold, including a jump inside the file already on screen. PDFs are the heavy case, so that mispriced the bar and fed warm samples into the cold calibration.""" @@ -396,14 +396,14 @@ def test_a_signal_from_the_other_path_does_not_retire_the_line( def test_a_navigation_that_ends_between_samples_still_retires_its_phases( app: StubApp, tracker: PreviewProgressTracker ) -> None: - """An observer only sees a phase if a tick falls inside it — and the event + """An observer only sees a phase if a tick falls inside it, and the event loop is saturated during exactly the work this line reports on, so ticks are dropped and the pipeline can pass through build and land between two samples. Measured on a real corpus: 7 of 31 navigations finished while the tracker still believed they were mounting. ``mount`` is followed by 53% of the - bar, so those sat at a median fill of 0.50 and then jumped to full — the + bar, so those sat at a median fill of 0.50 and then jumped to full: the "pauses halfway, then completes" report. The work being over means every phase of it is over, so the last sample retires them. """ @@ -440,7 +440,7 @@ def arrive(app: StubApp, parent_id: str) -> None: def test_a_navigation_ends_when_its_match_is_on_screen( app: StubApp, tracker: PreviewProgressTracker ) -> None: - """Not when the pipeline runs dry — those stopped being the same thing + """Not when the pipeline runs dry: those stopped being the same thing when the capture cache landed. The mount keeps filling below the fold long after the visible window has arrived, and waiting for it left the line up over a second after the match was readable: measured TRAIL p90 735 ms and @@ -452,7 +452,7 @@ def test_a_navigation_ends_when_its_match_is_on_screen( assert tracker.sample(session) is True arrive(app, "doc") - assert app._preview.pipeline_busy() is True, "setup — work must still be in flight" + assert app._preview.pipeline_busy() is True, "setup: work must still be in flight" assert tracker.sample(session) is False, "the line waited for work nobody is watching" @@ -460,7 +460,7 @@ def test_a_navigation_is_not_over_while_the_view_is_still_moving( app: StubApp, tracker: PreviewProgressTracker ) -> None: """Arrival must not short-circuit the scroll commit. Without this the line - cleared while the view was still moving — the opposite failure, and the + cleared while the view was still moving: the opposite failure, and the one that reads as the bar vanishing before it finished.""" session = tracker.begin("doc") arrive(app, "doc") @@ -487,7 +487,7 @@ def test_a_pane_showing_nothing_falls_back_to_the_pipeline( app: StubApp, tracker: PreviewProgressTracker ) -> None: """Arrival cannot be answered before anything is painted, so the pipeline - signals stay the authority there — and stay the thing that ends a + signals stay the authority there, and stay the thing that ends a navigation which never manages to paint at all.""" session = tracker.begin("doc") app._preview.painted = False diff --git a/tests/test_progress_tracker_contract.py b/tests/test_progress_tracker_contract.py index b3eca94b..8e01585e 100644 --- a/tests/test_progress_tracker_contract.py +++ b/tests/test_progress_tracker_contract.py @@ -4,20 +4,20 @@ inside them, which is what stops a stale teardown retiring someone else's bar (see ``fnd/tui/progress/operations.py``). The price of that choice is a set of implicit dependencies: the tracker reads attributes it does not -own, and renaming or removing one breaks the line **silently** — the +own, and renaming or removing one breaks the line **silently**; the sampler's catch-all treats the failure as "not busy", so the bar stops reflecting that stage rather than raising. These tests are the tripwire, and they run against a live app because every one of these is an instance attribute: nothing here is visible on -the class. They are deliberately shallow — existence, not behaviour. +the class. They are deliberately shallow: existence, not behaviour. Behaviour is covered by ``test_progress_preview_tracker`` and the real-app tests; what nothing else catches is a signal quietly going away. **If one of these fails after a preview-architecture change**, the fix is not to delete the assertion. Find the replacement signal, update ``PreviewProgressTracker``, and then re-run -``dev/tools/progress_phase_reachability.py`` — a phase that can no longer +``dev/tools/progress_phase_reachability.py``: a phase that can no longer be entered still owns its share of the bar, which caps the fill with nothing to show for it. That defect class has shipped four times. """ @@ -38,8 +38,8 @@ # Read by PreviewProgressTracker.sample or .plan_for. The comment is what it # is used for, so a replacement can be chosen rather than guessed at. PRESENTER_SIGNALS = { - "pipeline_busy": "anything in flight at all — the session's lifetime", - "showing_parent": "which file the pane is on — warm vs cold, and landing", + "pipeline_busy": "anything in flight at all: the session's lifetime", + "showing_parent": "which file the pane is on: warm vs cold, and landing", "decode_worker": "the decode phase", "mount_task": "the mount phase", "active": "the container carrying mounted_indices / _finalise_task", @@ -48,7 +48,7 @@ } # Read off ``presenter.active``. The mount fraction is measured against the -# WINDOW, not the file — dividing by the whole file is why the old bar topped +# WINDOW, not the file: dividing by the whole file is why the old bar topped # out near 1% on a large PDF. CONTAINER_SIGNALS = { "mounted_indices": "how much of the mount window has landed", @@ -92,12 +92,12 @@ async def test_the_preview_still_exposes_every_signal_the_line_reads( assert not missing, f"the progress line reads these and they are gone: {missing}" assert hasattr(app._preview_scroll, "is_settling"), ( - "is_settling is the whole of the `land` phase — the measured " + "is_settling is the whole of the `land` phase: the measured " "reconcile-to-scroll-commit window is 440-740 ms" ) container = app._preview.active - assert container is not None, "setup — the navigation did not produce a container" + assert container is not None, "setup: the navigation did not produce a container" gone = [ f"container.{name} ({why})" for name, why in CONTAINER_SIGNALS.items() diff --git a/tests/test_proximity_highlight.py b/tests/test_proximity_highlight.py index 2852f582..d3a797a5 100644 --- a/tests/test_proximity_highlight.py +++ b/tests/test_proximity_highlight.py @@ -223,7 +223,7 @@ def _span_styles_at(spans: list[Span], offset: int) -> set[str]: def test_live_markdown_baker_dims_lone_occurrence(): - # The stock FNDMarkdown widget bakes highlights via _build_match_spans — + # The stock FNDMarkdown widget bakes highlights via _build_match_spans: # this is the path the live preview actually uses, NOT apply_match_highlights. from fnd.render import DIM_MATCH_STYLES, MATCH_STYLES from fnd.tui.widgets.markdown import _build_match_spans @@ -240,7 +240,7 @@ def test_live_markdown_baker_dims_lone_occurrence(): # --- Auto-scroll target: prefer the co-occurrence, not a dimmed stray -------- -# The structural (FNDMarkdown) preview path — used for MD/DOCX/PPTX — picks the +# The structural (FNDMarkdown) preview path, used for MD/DOCX/PPTX, picks the # scroll target via _record_first_match (blocks) and _find_first_match_coord_in_table # (tables). #77 fixed the equivalent target in the flat (PDF/TXT) line_buffer path # but not these, so a proximity query landed on the first dimmed lone-term hit. @@ -357,7 +357,7 @@ def test_plain_query_multi_is_per_segment_unchanged(): def _painted(text: str, spec: MatchSpec) -> dict[str, bool]: """``{covered word: is_full}`` for ``text``. Keyed by the whole doc word, so a - wildcard hit split into literal/fill runs collapses back to one entry — + wildcard hit split into literal/fill runs collapses back to one entry, letting a test name the term it cares about instead of trusting that "some run was full" refers to the right one.""" from fnd.matching import DOC_WORD_RE @@ -374,7 +374,7 @@ def test_wildcard_member_can_qualify_a_window(): # DOC_WORD_RE dropped the glob in BOTH sinks: ``respons*`` reduced to the # stem ``respon`` (which no document token carries, so the window never # qualified) and left spec.wildcards empty (so the group's own term went - # unpainted). Assert on the wildcard term by name — asserting only that + # unpainted). Assert on the wildcard term by name: asserting only that # "some run is full" would pass on the sibling ``mobile`` alone. spec = MatchSpec.from_query("{6}respons* mobile", auto_fuzzy=False) assert spec.proximity_groups == ((("respons*", "mobil"), 6),) @@ -597,7 +597,7 @@ def test_unconstrained_wildcard_member_never_dims(): def test_unconstrained_member_still_qualifies_its_window(): - """Exemption removes a token from the dimmable set, not from the window — + """Exemption removes a token from the dimmable set, not from the window: the group's other member must still be able to reach full through it.""" spec = MatchSpec.from_query("{5}data replication OR replication", auto_fuzzy=False) assert _painted("Some data replication here.", spec) == {"data": True, "replication": True} @@ -605,7 +605,7 @@ def test_unconstrained_member_still_qualifies_its_window(): def test_many_arms_each_exempt_independently(): """The exemption is per term, not a single flag: across four groups only the - members that are also a free arm stay full. Every key carries signal — each + members that are also a free arm stay full. Every key carries signal: each word here is a group member, so a blanket exemption or none both fail.""" pairs = [("alpha", "bravo"), ("charlie", "delta"), ("echo", "foxtrot"), ("golf", "hotel")] freed = [a for a, _ in pairs] @@ -638,7 +638,7 @@ def test_several_groups_exempt_only_their_own_free_members(): def test_quoted_word_inside_the_operator_is_not_a_free_arm(): """``_RUN_TOKEN`` admits a quote inside a ``{N}`` run, so ``{5}"a" "b"`` is - the operator's own terms — not two free arms that would exempt the group.""" + the operator's own terms, not two free arms that would exempt the group.""" spec = MatchSpec.from_query('{5}"data" "replication"', auto_fuzzy=False) assert spec.proximity_groups == ((("data", "replic"), 5),) assert spec.unconstrained_terms == frozenset() @@ -655,7 +655,7 @@ def test_quoted_free_arm_still_exempts(): def test_single_word_slop_phrase_still_reaches_the_loose_terms(): - """``"x"~N`` is one word, so it is no group — it must survive the group-span + """``"x"~N`` is one word, so it is no group: it must survive the group-span blanking and still highlight.""" spec = MatchSpec.from_query('"replication"~5', auto_fuzzy=False) assert spec.proximity_groups == () @@ -699,7 +699,7 @@ def test_only_the_typed_free_arm_escapes_the_window(): def test_a_groups_own_synonyms_do_not_exempt_it(): - """Only the FREE run is synonym-expanded into the exemption set — expanding + """Only the FREE run is synonym-expanded into the exemption set: expanding the whole query would let every group exempt itself.""" from fnd.synonyms import SynonymTable diff --git a/tests/test_query_acceptance.py b/tests/test_query_acceptance.py index 91fd5104..6bc854b8 100644 --- a/tests/test_query_acceptance.py +++ b/tests/test_query_acceptance.py @@ -3,11 +3,11 @@ Encodes the desired end-state behaviour for every capability the README claims, verified against BOTH live search paths: - * Searcher.search — single-pass (CLI `fnd search`) - * search_layered — probe→fusion→cascade (TUI / --explain) + * Searcher.search, single-pass (CLI `fnd search`) + * search_layered, probe→fusion→cascade (TUI / --explain) Rows that pass today are regression guards. Rows that are broken today are marked -``xfail(strict=True)`` with the phase that will fix them — flip the marker to a plain +``xfail(strict=True)`` with the phase that will fix them: flip the marker to a plain test as each phase lands (a strict xfail that starts passing fails the suite, so we can't forget). @@ -236,7 +236,7 @@ def _layered(s: Searcher, q: str) -> set[str]: # (id, query, predicate, broken_paths). ``broken_paths`` lists the paths that -# are expected to FAIL today (strict xfail) — empty means "must pass on both". +# are expected to FAIL today (strict xfail): empty means "must pass on both". # As each phase lands, shrink the set; a strict xfail that starts passing fails # the suite, so a fixed capability can't silently stay marked broken. type _Case = tuple[str, str, Callable[[set[str]], bool], frozenset[str]] @@ -267,7 +267,7 @@ def _layered(s: Searcher, q: str) -> set[str]: ), ("path-tokens", "path_tokens:thesis", lambda r: "fld-path" in r, _OK), ("heading-path", "heading_path:proofs", lambda r: "fld-heading" in r, _OK), - # weighted-default ranking is order-sensitive — see test_weighted_default_ranking_*. + # weighted-default ranking is order-sensitive: see test_weighted_default_ranking_*. # --- P2: filter context + ranges (now live) --- ("filter-kind", "kind:docx diffusion", lambda r: r == {"kind-docx"}, _OK), ( @@ -394,7 +394,7 @@ def _layered(s: Searcher, q: str) -> set[str]: ), ("has-field", "has:author", lambda r: r == {"fld-author"}, _OK), # Field grouping composed with a boolean stays in content (filter extraction - # leaves it) — it must reach Tantivy's parser, not the AST (which would crash + # leaves it): it must reach Tantivy's parser, not the AST (which would crash # splitting ``title:`` from its group). ( "field-group-in-boolean", @@ -410,12 +410,12 @@ def _layered(s: Searcher, q: str) -> set[str]: lambda r: r == {"fld-title"}, _OK, ), - # Presence on a numeric field is "has a real (non-zero) value" — pages 5/15/25, + # Presence on a numeric field is "has a real (non-zero) value": pages 5/15/25, # not the page:0 default of every other doc. ("has-uint-presence", "has:page", lambda r: r == {"pg-5", "pg-15", "pg-25"}, _OK), # --- Operators composing INSIDE booleans/parens (boolean AST compiler) --- # The discriminator in each: the dropped ``*``/no-op ``~N`` of the old - # parse_query handoff would intersect on the *literal* stem and return {} — + # parse_query handoff would intersect on the *literal* stem and return {}, # only a real wildcard/fuzzy/regex leaf inside the AND yields the doc. ( "and-wildcard", @@ -582,7 +582,7 @@ def test_weighted_default_ranking_cli(searcher: Searcher) -> None: def test_unparsable_numeric_filter_does_not_crash() -> None: - """An invalid numeric/date bound must not blow up extraction — the clause + """An invalid numeric/date bound must not blow up extraction: the clause falls back to content (0 filters) instead of raising. Guards the `>N` / `[lo TO hi]` branches that previously let `ValueError` escape.""" from fnd.query_filters import extract_filters diff --git a/tests/test_query_boolean_ast.py b/tests/test_query_boolean_ast.py index 01753dce..7685996b 100644 --- a/tests/test_query_boolean_ast.py +++ b/tests/test_query_boolean_ast.py @@ -2,7 +2,7 @@ The end-to-end search semantics live in test_query_acceptance.py; this pins the *structure* the parser produces so operator precedence, grouping, prefix/boost -handling, and leaf classification can't drift. Pure Python — no index. +handling, and leaf classification can't drift. Pure Python: no index. """ from __future__ import annotations @@ -120,7 +120,7 @@ def test_empty_query_is_none() -> None: def test_single_child_groups_unwrap() -> None: - # A lone term in parens collapses to the term — no needless wrapper node. + # A lone term in parens collapses to the term: no needless wrapper node. assert parse_query_ast("(entropy)") == Term("entropy") diff --git a/tests/test_query_dsl.py b/tests/test_query_dsl.py index 2e1dea7f..30c4f550 100644 --- a/tests/test_query_dsl.py +++ b/tests/test_query_dsl.py @@ -80,7 +80,7 @@ def test_slide_less_or_equal() -> None: def test_page_range_passthrough() -> None: - """Bracket form is Tantivy-native — leave it alone.""" + """Bracket form is Tantivy-native: leave it alone.""" assert query_dsl.preprocess("page:[10 TO 50]") == "page:[10 TO 50]" @@ -154,9 +154,9 @@ def test_check_proximity_rejects_malformed(bad: str) -> None: @pytest.mark.parametrize( "ok", [ - '"foo bar"~5', # expanded proximity — no residual brace + '"foo bar"~5', # expanded proximity: no residual brace "page:[10 TO 50]", - "page:{10 TO 50}", # Tantivy exclusive range — not a proximity attempt + "page:{10 TO 50}", # Tantivy exclusive range, not a proximity attempt "plain words only", ], ) diff --git a/tests/test_query_dsl_split.py b/tests/test_query_dsl_split.py index 6c1d9717..67b7520e 100644 --- a/tests/test_query_dsl_split.py +++ b/tests/test_query_dsl_split.py @@ -45,7 +45,7 @@ def test_multiple_brackets_raises() -> None: def test_empty_brackets_returns_none() -> None: - """An empty [] is a no-op, not an empty filter — surface as None so + """An empty [] is a no-op, not an empty filter: surface as None so the search runs without metadata filtering.""" q, m = split_metadata_filter("foo []") assert q == "foo" diff --git a/tests/test_query_metadata_filter.py b/tests/test_query_metadata_filter.py index 64911189..b4846887 100644 --- a/tests/test_query_metadata_filter.py +++ b/tests/test_query_metadata_filter.py @@ -54,7 +54,7 @@ def test_meta_filter_narrows_to_matching_md(filter_corpus: Path) -> None: def test_meta_filter_empty_string_is_invalid(filter_corpus: Path) -> None: - """Defensive: an empty filter string must NOT compile — callers should + """Defensive: an empty filter string must NOT compile, callers should pass None for "no filter". An empty string is a bug on the caller side and we want a clear error.""" s = Searcher(index_dir=filter_corpus) @@ -116,7 +116,7 @@ def test_meta_filter_oversample_still_returns_limit_when_filter_strict( collection="notes", metadata_filter="Project == 'Website'", ) - # 5 of the 50 match the filter; we asked for limit=5 — must get all 5. + # 5 of the 50 match the filter; we asked for limit=5: must get all 5. assert len(hits) == 5 for h in hits: # Sanity: every returned hit's path corresponds to a 0/10/20/30/40 file. diff --git a/tests/test_reading_mode.py b/tests/test_reading_mode.py index 69cea705..219979c6 100644 --- a/tests/test_reading_mode.py +++ b/tests/test_reading_mode.py @@ -1,5 +1,5 @@ """Reading mode hides the sidebar so the preview fills the full terminal -width — a normal terminal text selection then covers only the preview +width: a normal terminal text selection then covers only the preview (clean copy for text-to-speech), and it reads distraction-free. Toggling again restores the sidebar. Reading mode also owns mouse capture: it hands the terminal back its mouse on entry (so native selection / TTS @@ -28,7 +28,7 @@ def _record_mouse_calls(app: FNDApp) -> Generator[list[str]]: (no delegation): the real hooks write mouse-tracking escape sequences and we don't want those side effects during tests. Patching just the two hooks (not swapping ``_driver`` wholesale) - keeps Textual's timer/bell code working — they read other driver + keeps Textual's timer/bell code working: they read other driver attrs like ``is_headless``.""" calls: list[str] = [] driver = app._driver # type: ignore[attr-defined] @@ -162,7 +162,7 @@ async def _step_after(pilot: Pilot[None], pane: VerticalScroll, press: Callable[ """Lines the pane advances for one scroll-key press, from a resting top. Waits for the deferred target update rather than assuming one ``pause()`` - covers it — under load it doesn't, and the delta then reads 0. + covers it: under load it doesn't, and the delta then reads 0. """ from tests._pilot_wait import wait_until @@ -182,7 +182,7 @@ async def _wait_preview_settled(app: FNDApp, pilot: Pilot[None], *, min_mounted: Not ``is_complete`` (``mounted >= total_chunks``): background fill does not reliably mount every chunk under headless test timing for large - docs, so a count-settled gate is the deterministic readiness signal — + docs, so a count-settled gate is the deterministic readiness signal, and waiting for the fill to settle keeps a later prune/toggle from racing an in-flight mount. """ @@ -252,7 +252,7 @@ def visible_marks() -> set[str]: await pilot.pause() mounted_after = len(active.mounted_indices) assert mounted_after < mounted_before, "reading view did not prune to a window" - # The same content is still on screen — prune compensated the scroll. + # The same content is still on screen: prune compensated the scroll. assert vis_before & visible_marks(), "visible content shifted/lost on prune" @@ -375,7 +375,7 @@ async def test_reading_view_scroll_step_is_larger(tmp_path: Path, tmp_index_dir: await _rest_at_top(pilot, pane) normal_step = await _step_after(pilot, pane, pane.action_scroll_down) assert normal_step == 1, f"normal step should be 1 line, got {normal_step}" - # Reading view from the top: larger step (not pruned — few chunks). + # Reading view from the top: larger step (not pruned, few chunks). app.action_toggle_reading_mode() await _rest_at_top(pilot, pane) reading_step = await _step_after(pilot, pane, pane.action_scroll_down) @@ -386,7 +386,7 @@ async def test_reading_view_scroll_step_is_larger(tmp_path: Path, tmp_index_dir: def test_apply_mouse_capture_is_safe_without_driver_hooks() -> None: """Helper must no-op when the driver lacks the private hooks (headless - test drivers, future driver changes) instead of raising — reading-mode + test drivers, future driver changes) instead of raising: reading-mode toggling stays usable even in such environments.""" app = FNDApp(config=Config()) app._driver = object() # type: ignore[assignment] diff --git a/tests/test_reading_view_table_reflow.py b/tests/test_reading_view_table_reflow.py index 37c99a4d..f2d623ab 100644 --- a/tests/test_reading_view_table_reflow.py +++ b/tests/test_reading_view_table_reflow.py @@ -4,7 +4,7 @@ The live preview renders markdown tables via ``FNDMarkdownTableDT`` → a ``DataTable`` whose column widths are computed from the pane width and recomputed by ``on_resize`` when the pane widens. The reflow's core is the -pure ``_compute_table_col_widths`` function — unit-tested here headlessly. +pure ``_compute_table_col_widths`` function: unit-tested here headlessly. The full-app integration runs under ``app.run_test``: the DataTable preview path is the live default (``FNDMarkdown.BLOCKS["table_open"]``) and DOES mount headless, verified 2026-06-13. (It was previously gated behind diff --git a/tests/test_rebuild_literal_wipe.py b/tests/test_rebuild_literal_wipe.py index ab1bb437..50754c9c 100644 --- a/tests/test_rebuild_literal_wipe.py +++ b/tests/test_rebuild_literal_wipe.py @@ -1,7 +1,7 @@ """A collection Rebuild is a LITERAL wipe: it deletes each file's saved texturing (cache entry + seen-marker) and re-extracts from scratch, so the cache holds no stale variants and the counts report every file as -newly indexed — genuine state, not an accumulated 'already' label. +newly indexed: genuine state, not an accumulated 'already' label. """ from __future__ import annotations diff --git a/tests/test_recovery_pipeline_golden.py b/tests/test_recovery_pipeline_golden.py index 8f930d30..f5f94740 100644 --- a/tests/test_recovery_pipeline_golden.py +++ b/tests/test_recovery_pipeline_golden.py @@ -2,7 +2,7 @@ The Bug-E fix routes structured extraction through a tiered ``PageRecoveryPipeline``. These tests pin born-digital output so the -refactor — and every later tier that gates on coverage — provably leaves +refactor, and every later tier that gates on coverage, provably leaves born-digital pages byte-for-byte unchanged. Born-digital coverage floors well above the fallback gate, so no tier may ever rewrite these pages. """ @@ -21,7 +21,7 @@ def _direct_page_md() -> dict[int, str]: """The pre-refactor production transform per page: ``_extract_page_md`` - followed by ``_strip_picture_markers`` (no docling — born-digital + followed by ``_strip_picture_markers`` (no docling: born-digital test.pdf emits no picture-omitted markers, so the splice never runs).""" from fnd.extract import pdf diff --git a/tests/test_reindex_prunes_removed_files.py b/tests/test_reindex_prunes_removed_files.py index 6bb5786d..ff1437e1 100644 --- a/tests/test_reindex_prunes_removed_files.py +++ b/tests/test_reindex_prunes_removed_files.py @@ -9,7 +9,7 @@ Pruning must stay narrow: it is scoped to one collection so a shared vault keeps its sibling's chunks, it leaves the content-addressed texture cache alone (``fnd cache prune-orphans`` owns that, across all collections), and it -is skipped whenever the walk was incomplete — a cancelled run or an offline +is skipped whenever the walk was incomplete; a cancelled run or an offline source root would otherwise read as "every file was deleted". """ @@ -209,10 +209,10 @@ def test_pruned_pdf_keeps_texture_cache_entry_when_another_collection_holds_it( beta = CollectionConfig(sources=[SourceConfig(path=src, includes=["**/*.pdf"])]) cfg = Config(collections={"alpha": alpha_narrowed, "beta": beta}) - # Dropped from alpha, still reachable via beta — so still live content. + # Dropped from alpha, still reachable via beta, so still live content. assert sha in live_content_shas(cfg) - # Gone from every collection — now genuinely an orphan. + # Gone from every collection: now genuinely an orphan. cfg_none = Config(collections={"alpha": alpha_narrowed}) assert sha not in live_content_shas(cfg_none) @@ -224,7 +224,7 @@ async def _run(cfg: CollectionConfig, collection: str, index_dir: Path, **kw: ob @pytest.mark.asyncio async def test_runner_prunes_file_deleted_from_disk(tmp_path: Path) -> None: - """The TUI indexer path prunes too — it is what most re-indexes go through.""" + """The TUI indexer path prunes too: it is what most re-indexes go through.""" notes = tmp_path / "notes" _touch(notes / "keep.md", "# Keep\npenguin sandwich\n") _touch(notes / "gone.md", "# Gone\npenguin sandwich\n") @@ -243,7 +243,7 @@ async def test_runner_prunes_file_deleted_from_disk(tmp_path: Path) -> None: @pytest.mark.asyncio async def test_runner_does_not_prune_when_cancelled(tmp_path: Path) -> None: """A cancelled run walked only part of the corpus, so its live set is - incomplete — pruning against it would delete files that still exist.""" + incomplete: pruning against it would delete files that still exist.""" notes = tmp_path / "notes" for i in range(6): _touch(notes / f"n{i}.md", f"# N{i}\npenguin sandwich {i}\n") diff --git a/tests/test_rerank.py b/tests/test_rerank.py index 565c7442..8f86738f 100644 --- a/tests/test_rerank.py +++ b/tests/test_rerank.py @@ -111,7 +111,7 @@ def test_filetype_boost_applies_multiplier() -> None: def test_phrase_proximity_single_term_is_neutral() -> None: - """Single-term queries can't have a 'cluster' — bonus is always 0.""" + """Single-term queries can't have a 'cluster': bonus is always 0.""" profile = RankingProfile(phrase_proximity=1.0) out = apply_phrase_proximity( score=1.0, body="quark gluon plasma", terms=["quark"], profile=profile @@ -205,7 +205,7 @@ def test_rerank_hits_scales_the_rank_score_not_the_display_score() -> None: @pytest.fixture def two_collection_index(fixtures_dir: Path, tmp_index_dir: Path, tmp_path: Path) -> Path: - """Two MD files with identical anchor phrases — used to verify a profile + """Two MD files with identical anchor phrases: used to verify a profile switch can flip ranking on tied content.""" md_dir = tmp_path / "md" md_dir.mkdir(parents=True, exist_ok=True) @@ -342,7 +342,7 @@ def test_load_config_with_ranking_profile(tmp_path: Path) -> None: def test_ranking_profile_unknown_name_is_neutral_default() -> None: """Calling Config.ranking_profile() with a missing name yields an - all-zero default — opt-in semantics. Proximity/exactness is handled by + all-zero default: opt-in semantics. Proximity/exactness is handled by fusion's exact-phrase pass, not this post-rank multiplier.""" from fnd.config import Config from fnd.rerank import profile_from_config @@ -358,7 +358,7 @@ def test_fnd_app_resolves_collection_specific_profile( two_collection_index: Path, ) -> None: """The TUI resolves a per-collection ranking profile when present and - applies it during search — no profile defined → neutral identity.""" + applies it during search: no profile defined → neutral identity.""" from fnd.config import CollectionConfig, Config, RankingProfileConfig from fnd.tui import FNDApp diff --git a/tests/test_results_collapse_behavior.py b/tests/test_results_collapse_behavior.py index d0bbd56c..39d5d3f1 100644 --- a/tests/test_results_collapse_behavior.py +++ b/tests/test_results_collapse_behavior.py @@ -1,7 +1,7 @@ """Results-pane collapse-to-header behaviour. Bug 3: collapsing the pane while a lower result is selected must keep the -*selected* file in the single visible strip row — not snap back to the top +*selected* file in the single visible strip row, not snap back to the top result (the strip drives the preview, so it must show the driving file). Bug 4: clicking the collapsed strip must reopen the pane (and expand the diff --git a/tests/test_results_never_filtered_by_highlighter.py b/tests/test_results_never_filtered_by_highlighter.py index f0e0f76d..efcb3a6f 100644 --- a/tests/test_results_never_filtered_by_highlighter.py +++ b/tests/test_results_never_filtered_by_highlighter.py @@ -1,7 +1,7 @@ """A broken highlighter must never subtract results. The engine's match is what makes a result a result. If paintability were allowed -to gate the results pane, a bug in the highlighter would remove rows silently — +to gate the results pane, a bug in the highlighter would remove rows silently: the user would never learn they had missed a match, and the bug would never be discovered. So an unpaintable match is *marked*, never withheld. @@ -23,7 +23,7 @@ from fnd.tui import FNDApp from fnd.tui.results_labels import _UNLOCATABLE_GLYPH -# Non-empty (so the evidence check engages) but present in no document — the +# Non-empty (so the evidence check engages) but present in no document: the # shape a broken highlighter would produce. An *empty* spec would short-circuit # to "nothing to locate" and prove nothing. BLIND_SPEC = MatchSpec.from_query("zzqqxx") @@ -71,7 +71,7 @@ def _rows(tree: Tree[object]) -> list[tuple[str, int]]: def _blind(app: FNDApp, monkeypatch: pytest.MonkeyPatch) -> None: - """Replace the app's specs with ones that match nothing — a highlighter + """Replace the app's specs with ones that match nothing: a highlighter that has stopped working. Both the painting spec and the evidence spec are blinded: they are built @@ -107,7 +107,7 @@ async def test_blind_highlighter_removes_no_results( async def test_blind_highlighter_marks_every_row( app_with_results: FNDApp, monkeypatch: pytest.MonkeyPatch ) -> None: - """Not merely retained — visibly flagged, so the regression is discoverable + """Not merely retained: visibly flagged, so the regression is discoverable rather than a silent oddity.""" async with app_with_results.run_test() as pilot: for _ in range(8): diff --git a/tests/test_results_preview_arrow_bridge.py b/tests/test_results_preview_arrow_bridge.py index c6991427..270a3e0e 100644 --- a/tests/test_results_preview_arrow_bridge.py +++ b/tests/test_results_preview_arrow_bridge.py @@ -33,7 +33,7 @@ async def test_right_on_results_leaf_focuses_preview(built_index: Path) -> None: assert top_file.is_expanded assert top_file.children # The default cursor placement after a query lands on the first - # hit child — a leaf. + # hit child: a leaf. assert tree.cursor_node is top_file.children[0] await pilot.press("right") await pilot.pause() diff --git a/tests/test_reveal_in_file_manager.py b/tests/test_reveal_in_file_manager.py index 1c0da72e..370d39ae 100644 --- a/tests/test_reveal_in_file_manager.py +++ b/tests/test_reveal_in_file_manager.py @@ -67,7 +67,7 @@ def test_reveal_action_is_offered_where_a_result_is_focused() -> None: def test_reveal_action_label_defers_the_file_manager_name_to_render_time() -> None: """The registry is a module constant, so it carries the placeholder rather - than a baked-in OS name — see ``tests/test_keybindings_os_vocabulary.py`` + than a baked-in OS name: see ``tests/test_keybindings_os_vocabulary.py`` for the per-platform rendering.""" action = next(a for a in REGISTRY if a.id == ACTION_ID) footer_label = action.footer_label @@ -127,7 +127,7 @@ async def test_reveal_key_press_reaches_the_action(built_index: Path, revealed: async def test_reveal_is_a_no_op_with_no_result_focused( built_index: Path, revealed: list[Path] ) -> None: - """A query with no hits leaves the cursor on nothing — reveal must not + """A query with no hits leaves the cursor on nothing: reveal must not raise or spawn a file manager on a bogus path.""" app = FNDApp(index_dir=built_index, initial_query="zzzznosuchtermzzzz") async with app.run_test() as pilot: @@ -156,7 +156,7 @@ def test_reveal_row_appears_in_the_picker_for_any_kind(kind: str) -> None: def test_reveal_sorts_last_so_it_never_pushes_openers_down() -> None: - """It's an escape hatch, not an opener — the apps that actually open the + """It's an escape hatch, not an opener: the apps that actually open the file stay at the top of the list.""" ids = [r.id for r in eligible_apps(kind="pdf", registry=apps.BUILTIN_APPS)] assert ids[-1] == "reveal", ids diff --git a/tests/test_rich_highlights_and_skim_search.py b/tests/test_rich_highlights_and_skim_search.py index 89ba2217..dc8071a9 100644 --- a/tests/test_rich_highlights_and_skim_search.py +++ b/tests/test_rich_highlights_and_skim_search.py @@ -105,7 +105,7 @@ def test_open_smart_routes_to_url_when_query_present( f.touch() monkeypatch.setattr("sys.platform", "darwin") # Skim auto-promote is macOS-only monkeypatch.setattr(opener, "_has_skim", lambda: True) - # Isolate from the developer's real config — without this the resolver + # Isolate from the developer's real config, without this the resolver # routes through whichever [app_defaults] they have set (eg. `preview`) # and the test fires a real `osascript`, opening Preview against the # empty tmp file. @@ -123,7 +123,7 @@ def test_open_smart_routes_to_url_when_query_present( def test_open_smart_uses_url_when_query_blank( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - """Query-less PDF opens still use the Skim URL form — the AppleScript + """Query-less PDF opens still use the Skim URL form: the AppleScript path was removed because filename control-char injection through the AppleScript string literal could execute arbitrary `osascript`.""" from fnd.config import Config @@ -248,7 +248,7 @@ async def test_chunk_widgets_mounted_per_pdf_page( The widget owns a FileView whose ``chunk_to_range`` covers every page; the focused chunk's first-match line is the scroll target. - Forces flat preview routing — when the pdf-structure extra is + Forces flat preview routing, when the pdf-structure extra is installed in the dev venv, PDFs carry body_md and would otherwise route structural. The flat-pipeline contract is what this test asserts; structural routing has its own coverage.""" diff --git a/tests/test_schema_meta_blob.py b/tests/test_schema_meta_blob.py index b981c173..7613250c 100644 --- a/tests/test_schema_meta_blob.py +++ b/tests/test_schema_meta_blob.py @@ -21,12 +21,12 @@ def test_meta_blob_field_constant_exists() -> None: def test_schema_accepts_meta_blob_bytes() -> None: - """The schema must accept ``meta_blob`` as a stored bytes field — the + """The schema must accept ``meta_blob`` as a stored bytes field: the indexer writes JSON-encoded frontmatter there, retrieved at query time by the post-filter.""" build_schema() doc = Document() - # Should not raise — the field is declared and accepts bytes. + # Should not raise: the field is declared and accepts bytes. doc.add_bytes(F_META_BLOB, b'{"Project": "Website"}') @@ -43,14 +43,14 @@ def test_old_index_sidecar_refuses_load(tmp_path: Path) -> None: def test_force_rebuild_wipes_stale_index_dir(tmp_path: Path) -> None: """``force=True`` is the rebuild path: it must clear leftover Tantivy files so a new index can be opened under the current schema. A stale - sidecar alone isn't enough — Tantivy stores the schema in + sidecar alone isn't enough: Tantivy stores the schema in ``meta.json`` too, and the constructor refuses to open a mismatched dir. Verifies that a stale dir with a non-matching ``meta.json`` doesn't cause Tantivy to raise; the rebuild path clears the dir then opens a fresh index. (Tantivy regenerates its own ``meta.json`` after the - wipe — that's expected.) + wipe: that's expected.) """ from fnd.index import _ensure_index @@ -67,7 +67,7 @@ def test_force_rebuild_wipes_stale_index_dir(tmp_path: Path) -> None: nested.mkdir() (nested / "x").write_text("y") - # Should not raise — the rebuild path wipes and reinitialises. + # Should not raise: the rebuild path wipes and reinitialises. _ensure_index(tmp_path, force=True) # Sidecar now matches current version. @@ -92,7 +92,7 @@ def _build_legacy_v1_index(index_dir: Path) -> None: schema = sb.build() register(Index(schema, path=str(index_dir))) # Pretend a prior force-rebuild bumped the sidecar but crashed before - # Tantivy got new segments — the exact stuck state in the field. + # Tantivy got new segments: the exact stuck state in the field. (index_dir / ".fnd-schema-version").write_text(str(SCHEMA_VERSION)) @@ -121,7 +121,7 @@ def test_open_without_force_on_inconsistent_dir_gives_clear_error( tmp_path: Path, ) -> None: """Without ``force=True``, the same inconsistent state surfaces as a - RuntimeError pointing the user at the rebuild command — not Tantivy's + RuntimeError pointing the user at the rebuild command, not Tantivy's cryptic ``Schema error``.""" from fnd.index import _ensure_index diff --git a/tests/test_scope_phantom_and_spaced_collection.py b/tests/test_scope_phantom_and_spaced_collection.py index 18d1900d..d3ec193e 100644 --- a/tests/test_scope_phantom_and_spaced_collection.py +++ b/tests/test_scope_phantom_and_spaced_collection.py @@ -5,7 +5,7 @@ Both stem from a collection name that contains a space/comma. The multi-collection filter built an unquoted ``c:`` value, so a spaced name -was split at the space — the tail became a stray bare term and the name +was split at the space; the tail became a stray bare term and the name was dropped. A phantom name with no config row could then pin every search while being invisible (untoggleable) in the sidebar. """ @@ -78,7 +78,7 @@ async def test_multi_collection_scope_hard_restricts( assert "b.md" in names, "spaced collection 'Spaced Coll' was dropped from scope" assert "a.md" in names, "'Plain' collection missing from scope" assert "c.md" not in names, ( - "'Third' collection leaked into a 2-collection scope — multi-" + "'Third' collection leaked into a 2-collection scope: multi-" "collection scoping is ranking softly instead of hard-filtering" ) @@ -101,7 +101,7 @@ async def test_phantom_collection_name_dropped_on_load( async with app.run_test() as pilot: await pilot.pause() assert app._scope.collections == ["Plain"], ( - "phantom 'Spaced Coll,Plain' should be dropped — it has no panel " + "phantom 'Spaced Coll,Plain' should be dropped: it has no panel " f"row yet drives scope; got {app._scope.collections}" ) diff --git a/tests/test_search_off_loop.py b/tests/test_search_off_loop.py index 40e3d2d9..a05965df 100644 --- a/tests/test_search_off_loop.py +++ b/tests/test_search_off_loop.py @@ -1,13 +1,13 @@ """Search runs off the event loop. -The point is a UI that stays alive while a query runs — but the change +The point is a UI that stays alive while a query runs, but the change has to be safe before it is useful, so most of what is pinned here is the staleness and teardown behaviour rather than the responsiveness. The property everything rests on: Textual *cancels* a superseded thread worker but cannot *interrupt* it. The old search runs to completion and arrives anyway. So the generation guard in ``_commit`` is not a -belt-and-braces extra — it is the only thing standing between a stale +belt-and-braces extra: it is the only thing standing between a stale result and the caches. """ @@ -100,7 +100,7 @@ def slow(request: Any) -> Any: @pytest.mark.asyncio async def test_a_superseded_search_never_reaches_the_caches(index: Path) -> None: """The staleness guard. A slow first query is overtaken by a second; when - the first finally finishes it must be discarded whole — not merged, not + the first finally finishes it must be discarded whole, not merged, not partially applied.""" app = FNDApp(index_dir=index) async with app.run_test() as pilot: @@ -181,7 +181,7 @@ def boom(_request: Any) -> Any: @pytest.mark.asyncio async def test_quitting_mid_search_is_not_an_error(index: Path) -> None: """A search in flight when the app goes away has no DOM to commit to. - That is an ordinary shutdown, not a crashed worker — which is exactly + That is an ordinary shutdown, not a crashed worker, which is exactly what it surfaced as before ``_marshal`` guarded it.""" app = FNDApp(index_dir=index) async with app.run_test() as pilot: @@ -233,7 +233,7 @@ async def test_a_malformed_query_supersedes_the_search_already_running(index: Pa ``_fail`` marks the current generation committed. Allocating the generation only after a successful parse meant a malformed query marked an - IN-FLIGHT search's generation as committed — that worker then passed the + IN-FLIGHT search's generation as committed: that worker then passed the guard in ``_commit``, restored its stale results, and wiped the error notice the user had just been shown. """ @@ -291,7 +291,7 @@ def recording(self: Any, request: Any, groups: Any, trace: Any, session: Any) -> finally: app._search.__class__._commit = original_enter # type: ignore[method-assign] - assert phases, "setup — the search never committed" + assert phases, "setup: the search never committed" assert phases[-1] == "results", "the commit stage never entered its own phase" assert {p.key for p in SEARCH.phases} == {"query", "results"} @@ -305,7 +305,7 @@ async def test_a_malformed_query_does_not_reload_the_index_under_a_worker( single search from two index generations. The guard used to derive "nothing is running" from the generation - counters — but ``_fail`` marks a generation committed WITHOUT waiting for + counters, but ``_fail`` marks a generation committed WITHOUT waiting for its worker, so a malformed query in between reported idle while a real search was still executing. """ @@ -334,7 +334,7 @@ def counting_reload(*a: Any, **k: Any) -> Any: app._search.run("target") await wait_until(pilot, started.is_set) - # The first query reloads legitimately — nothing was running. What + # The first query reloads legitimately: nothing was running. What # follows is the case under test. reloads.clear() @@ -355,7 +355,7 @@ async def test_a_cancelled_dispatch_does_not_disable_reload_forever(index: Path) Deriving that from a counter was wrong. ``exclusive=True`` makes Textual cancel the previous worker SYNCHRONOUSLY inside ``add_worker``, before the new one starts, so a worker cancelled before its coroutine ever ran never - reached either commit path — and a counter incremented at dispatch was + reached either commit path, and a counter incremented at dispatch was never decremented. Two searches in a single tick did it, which Enter auto-repeat produces, and the leak was permanent: the gate stayed shut and the app silently stopped picking up reindexes for the rest of the session. @@ -365,7 +365,7 @@ async def test_a_cancelled_dispatch_does_not_disable_reload_forever(index: Path) await pilot.pause() await run_search(pilot, app, "target") - # Both in ONE tick — the second cancels the first before it starts. + # Both in ONE tick: the second cancels the first before it starts. app._search.run("target") app._search.run("target") await wait_until(pilot, lambda: app._search.idle, timeout=10.0) @@ -373,7 +373,7 @@ async def test_a_cancelled_dispatch_does_not_disable_reload_forever(index: Path) await pilot.pause() assert not app._search._searches_running(), ( - "a cancelled dispatch left the gate shut — reload() is off for good" + "a cancelled dispatch left the gate shut: reload() is off for good" ) assert app._search.searcher is not None diff --git a/tests/test_searcher_reload_after_reindex.py b/tests/test_searcher_reload_after_reindex.py index c3e6dc9e..98cf0164 100644 --- a/tests/test_searcher_reload_after_reindex.py +++ b/tests/test_searcher_reload_after_reindex.py @@ -53,7 +53,7 @@ def test_searcher_reload_picks_up_new_commit(tmp_path: Path, tmp_index_dir: Path searcher = Searcher(index_dir=tmp_index_dir) assert searcher.search("alpha", limit=10, collection="notes") - # Reindex with a second doc — commits a new generation. + # Reindex with a second doc: commits a new generation. _write_md(docs / "second.md", "# Second\nbravo zero seconds marker.\n") build_index(roots=[docs], index_dir=tmp_index_dir, collection="notes") @@ -77,7 +77,7 @@ def test_get_file_chunks_survives_concurrent_reload(tmp_path: Path, tmp_index_di docs = tmp_path / "notes" # >= _PARALLEL_DECODE_THRESHOLD (50) sections so the ThreadPool decode - # path runs — that's where the cross-thread swap is dangerous. + # path runs: that's where the cross-thread swap is dangerous. body = "# Doc\n" + "".join( f"\n## Section {i}\nzero seconds marker body for section {i}.\n" for i in range(80) ) @@ -126,7 +126,7 @@ async def test_new_file_appears_on_next_query_without_restart( cfg: Config, tmp_path: Path, tmp_index_dir: Path ) -> None: """User scenario: with the app open, a new file is indexed; the very - next query surfaces it — no restart needed.""" + next query surfaces it: no restart needed.""" docs = tmp_path / "notes" _write_md(docs / "first.md", "# First\nalpha content here.\n") build_index(roots=[docs], index_dir=tmp_index_dir, collection="notes") diff --git a/tests/test_settings_async_loading.py b/tests/test_settings_async_loading.py index 424e5999..7eb3bfd7 100644 --- a/tests/test_settings_async_loading.py +++ b/tests/test_settings_async_loading.py @@ -32,7 +32,7 @@ class _StubApp: - """No event loop — call_from_thread will fail (suppressed), but + """No event loop: call_from_thread will fail (suppressed), but the cache populates regardless.""" def call_from_thread(self, *_args: object, **_kwargs: object) -> None: diff --git a/tests/test_settings_cache_maintenance.py b/tests/test_settings_cache_maintenance.py index b17cdde8..1791481f 100644 --- a/tests/test_settings_cache_maintenance.py +++ b/tests/test_settings_cache_maintenance.py @@ -94,7 +94,7 @@ def _make_chunk(seq: int = 0) -> Chunk: ) -# 1 — Indexing screen surfaces cache rows +# 1: Indexing screen surfaces cache rows @pytest.mark.asyncio @@ -162,14 +162,14 @@ async def test_cache_size_row_shows_count_and_size( assert "B" in v or "KB" in v or "MB" in v -# 2 — Cache rows are direct children of Indexing (no maintenance drill). +# 2: Cache rows are direct children of Indexing (no maintenance drill). @pytest.mark.asyncio async def test_cache_actions_inline_under_indexing( built_index: Path, cfg: Config, isolated_cache: Path ) -> None: - """No drill needed — Update cache / Rebuild / Free-cache live on the + """No drill needed: Update cache / Rebuild / Free-cache live on the Indexing screen itself.""" from fnd.tui.menu import SECTION_PDF_TEXTURE from fnd.tui.settings_screen import SettingsList, open_settings_section @@ -187,12 +187,12 @@ async def test_cache_actions_inline_under_indexing( assert "pdf_texture.cache_clear" in ids -# 3 — Confirm dialog chrome + keyboard +# 3: Confirm dialog chrome + keyboard @pytest.mark.asyncio async def test_clear_confirm_chrome(built_index: Path, cfg: Config, isolated_cache: Path) -> None: - """Clear is destructive — confirm uses $error border and the + """Clear is destructive: confirm uses $error border and the irreversible callout. Mirrors DeleteCollectionScreen chrome (OptionList Yes/Cancel, hint bar at bottom).""" cache = ExtractionCache(root=isolated_cache) @@ -221,7 +221,7 @@ async def test_clear_confirm_chrome(built_index: Path, cfg: Config, isolated_cac assert "Cancel" in hint -# 4 — Keyboard equivalence (arrows + enter) +# 4: Keyboard equivalence (arrows + enter) @pytest.mark.asyncio @@ -268,7 +268,7 @@ async def test_clear_yes_path(built_index: Path, cfg: Config, isolated_cache: Pa assert not isolated_cache.exists() or cache.entry_count() == 0 -# 5 — Esc cancels without side effect +# 5: Esc cancels without side effect @pytest.mark.asyncio @@ -288,7 +288,7 @@ async def test_clear_esc_cancels(built_index: Path, cfg: Config, isolated_cache: assert cache.entry_count() == 1 -# 7 — Root summary reflects cache state +# 7: Root summary reflects cache state def test_root_summary_includes_cache(isolated_cache: Path, cfg: Config) -> None: @@ -307,7 +307,7 @@ def test_root_summary_includes_cache(isolated_cache: Path, cfg: Config) -> None: cache = ExtractionCache(root=isolated_cache) cache.put("aa--v1", [_make_chunk(0)]) - # Pre-seed the lazy slot — bypasses the worker so the test runs + # Pre-seed the lazy slot: bypasses the worker so the test runs # deterministically without needing a Textual event loop. _CACHE["pdf_texture.summary.cache_short"] = ("cache 1 KB", time.monotonic()) _CACHE["pdf_texture.summary.engine"] = ("✗ engine off", time.monotonic()) diff --git a/tests/test_settings_description_invariants.py b/tests/test_settings_description_invariants.py index 099bae20..93adfa36 100644 --- a/tests/test_settings_description_invariants.py +++ b/tests/test_settings_description_invariants.py @@ -4,9 +4,9 @@ test instead of a user finding it: 1. Every selectable row has a non-empty user-facing description. -2. The `↗` glyph never appears in descriptions — it's reserved for +2. The `↗` glyph never appears in descriptions: it's reserved for labels on rows whose ``external_app=True``. -3. Labels don't end with `…` — that affordance moved to the trailing +3. Labels don't end with `…`: that affordance moved to the trailing `[ Run… ]` button form. """ @@ -92,7 +92,7 @@ def test_every_row_has_a_user_facing_description() -> None: def test_no_descriptions_contain_external_arrow() -> None: - """↗ is a visual affordance — never in description prose. + """↗ is a visual affordance: never in description prose. The render layer adds the leading ↗ on rows where ``external_app=True``; if anything ships ↗ in description text, @@ -113,7 +113,7 @@ def test_external_arrow_only_appears_on_external_app_labels() -> None: def test_no_labels_end_with_ellipsis() -> None: - """Labels in normal settings rows don't end with `…` — that + """Labels in normal settings rows don't end with `…`: that affordance lives in the trailing ``[ Run… ]`` action-button form (i.e. on ``action_label``, not the row label). @@ -155,7 +155,7 @@ def test_action_labels_are_not_generic_open() -> None: for it in items: if it.kind != KIND_ACTION: continue - # The default action verb is "Run" — acceptable as a generic + # The default action verb is "Run": acceptable as a generic # fallback. "Open" is too vague; reject it. if it.action_label.strip().rstrip("…") == "Open": offenders.append((it.id, it.label, it.action_label)) diff --git a/tests/test_settings_extras_install_progress.py b/tests/test_settings_extras_install_progress.py index 45d37b12..1cfc4fd3 100644 --- a/tests/test_settings_extras_install_progress.py +++ b/tests/test_settings_extras_install_progress.py @@ -1,4 +1,4 @@ -"""Install/uninstall progress modal — modal-lifecycle tests. +"""Install/uninstall progress modal: modal-lifecycle tests. Follows dev/docs/test_patterns/settings_screen.md §9: task lives on FNDApp, Background dismisses but task survives, Cancel SIGTERMs the @@ -53,7 +53,7 @@ def fake_sleep() -> list[list[str]]: return [[sleep, "0.05"], [sleep, "0.05"]] -# 1 — Modal mounts and renders chrome +# 1: Modal mounts and renders chrome @pytest.mark.asyncio @@ -85,7 +85,7 @@ async def test_progress_modal_chrome( await asyncio.wait_for(app._extras_task, timeout=3.0) -# 2 — Background dismisses modal, task survives +# 2: Background dismisses modal, task survives @pytest.mark.asyncio @@ -116,7 +116,7 @@ async def test_background_keeps_task_running( await asyncio.wait_for(app._extras_task, timeout=2.0) -# 3 — Cancel stops the subprocess +# 3: Cancel stops the subprocess @pytest.mark.asyncio @@ -149,7 +149,7 @@ async def test_cancel_stops_subprocess(built_index: Path, cfg: Config) -> None: assert app._extras_proc is None -# 4 — Idempotent: second start while running re-attaches modal +# 4: Idempotent: second start while running re-attaches modal @pytest.mark.asyncio @@ -160,7 +160,7 @@ async def test_double_start_reattaches(built_index: Path, cfg: Config) -> None: rather than a short sleep. The old ``sleep 0.5`` raced the pauses below: on a loaded runner it exited first, so the second call correctly started a fresh run and returned True. That failed as ``assert True is False`` on - Linux CI while macOS and Windows passed — the test's premise had lapsed, + Linux CI while macOS and Windows passed: the test's premise had lapsed, not the behaviour under test. """ sleep = shutil.which("sleep") or "/bin/sleep" @@ -200,12 +200,12 @@ async def test_double_start_reattaches(built_index: Path, cfg: Config) -> None: await asyncio.wait_for(app._extras_task, timeout=5.0) -# 5 — Worker emits done event on success +# 5: Worker emits done event on success @pytest.mark.asyncio async def test_worker_emits_done(cfg: Config) -> None: - """Direct unit test of run_install — bypass the modal.""" + """Direct unit test of run_install: bypass the modal.""" from fnd.tui.extras_install_progress import ProgressEvent, run_install sleep = shutil.which("sleep") or "/bin/sleep" @@ -227,7 +227,7 @@ class _FakeApp: assert phases[-1] == "done" -# 6 — Worker emits failed on missing executable +# 6: Worker emits failed on missing executable @pytest.mark.asyncio diff --git a/tests/test_settings_hint_per_kind.py b/tests/test_settings_hint_per_kind.py index a6dbb120..f49d1b1f 100644 --- a/tests/test_settings_hint_per_kind.py +++ b/tests/test_settings_hint_per_kind.py @@ -133,7 +133,7 @@ async def test_display_row_enter_does_nothing(built_index: Path, cfg: Config) -> break lst.action_activate() await pilot.pause() - # EditBar should still be hidden — Enter did not open it. + # EditBar should still be hidden: Enter did not open it. bar = app.screen.query_one(EditBar) assert "-hidden" in bar.classes @@ -141,7 +141,7 @@ async def test_display_row_enter_does_nothing(built_index: Path, cfg: Config) -> @pytest.mark.asyncio async def test_hint_says_open_in_editor_on_external_app(built_index: Path, cfg: Config) -> None: """External-app rows (config file / keybindings file) reveal in Finder - via Shift+⏎ and open in $EDITOR via ⏎ — both must appear in the hint.""" + via Shift+⏎ and open in $EDITOR via ⏎: both must appear in the hint.""" from fnd.tui.settings_screen import SettingsList, SettingsScreen app = FNDApp(index_dir=built_index, config=cfg) @@ -152,7 +152,7 @@ async def test_hint_says_open_in_editor_on_external_app(built_index: Path, cfg: screen = app.screen assert isinstance(screen, SettingsScreen) lst = screen.query_one(SettingsList) - # Root screen focuses the search input by default — focus the + # Root screen focuses the search input by default: focus the # list so the per-kind hint cluster kicks in instead of the # search-input cluster. lst.focus() diff --git a/tests/test_settings_indexing.py b/tests/test_settings_indexing.py index 9d224599..9384b4ef 100644 --- a/tests/test_settings_indexing.py +++ b/tests/test_settings_indexing.py @@ -39,7 +39,7 @@ def cfg(cfg_path: Path) -> Config: return load(cfg_path) -# 1 — Provider shape +# 1: Provider shape def test_provider_indexing_has_auto_resume_toggle() -> None: @@ -83,7 +83,7 @@ def test_indexer_auto_resume_persists(tmp_path: Path) -> None: assert load(cfg_path).defaults.indexer_auto_resume is True -# 1 — Chrome shape (pilot) +# 1: Chrome shape (pilot) @pytest.mark.asyncio @@ -112,7 +112,7 @@ async def test_indexing_subscreen_chrome(built_index: Path, cfg: Config) -> None assert "Auto-resume on launch" in labels -# 2 — Keyboard equivalence +# 2: Keyboard equivalence @pytest.mark.asyncio @@ -127,7 +127,7 @@ async def test_indexing_toggle_via_enter(built_index: Path, cfg: Config) -> None open_settings_section(app, SECTION_INDEXING) await settings_ready(pilot, app) lst = app.screen.query_one(SettingsList) - # Move cursor to the toggle row specifically — the Indexing + # Move cursor to the toggle row specifically: the Indexing # screen now has multiple selectable rows above it. for i, item in enumerate(lst._items): if item.id == "indexing.auto_resume": @@ -142,7 +142,7 @@ async def test_indexing_toggle_via_enter(built_index: Path, cfg: Config) -> None assert before != after -# 3 — Hint-bar content +# 3: Hint-bar content @pytest.mark.asyncio @@ -160,7 +160,7 @@ async def test_indexing_hint_bar(built_index: Path, cfg: Config) -> None: assert "Back" in text -# 4 — Detail-strip content +# 4: Detail-strip content @pytest.mark.asyncio @@ -185,7 +185,7 @@ async def test_indexing_detail_strip_populated(built_index: Path, cfg: Config) - assert strip._description, "auto-resume row should populate the detail strip" -# 6 — Cross-section search +# 6: Cross-section search @pytest.mark.asyncio diff --git a/tests/test_settings_input_focus_flow.py b/tests/test_settings_input_focus_flow.py index 84a5da90..853ec228 100644 --- a/tests/test_settings_input_focus_flow.py +++ b/tests/test_settings_input_focus_flow.py @@ -21,7 +21,7 @@ def built_index(fixtures_dir: Path, tmp_index_dir: Path) -> Path: @pytest.mark.asyncio async def test_root_menu_focuses_search_input(built_index: Path) -> None: - """Root menu open: the search Input owns focus — typing immediately filters.""" + """Root menu open: the search Input owns focus, typing immediately filters.""" from fnd.tui.settings_screen import SettingsScreen app = FNDApp(index_dir=built_index) @@ -37,7 +37,7 @@ async def test_root_menu_focuses_search_input(built_index: Path) -> None: @pytest.mark.asyncio async def test_submenu_open_focuses_list_not_input(built_index: Path) -> None: - """Drilling into a sub-menu (e.g. Preferences) focuses the list — + """Drilling into a sub-menu (e.g. Preferences) focuses the list: the user already chose what they wanted by drilling in. The filter Input is still reachable via `/` from any screen. """ @@ -61,7 +61,7 @@ async def test_submenu_open_focuses_list_not_input(built_index: Path) -> None: @pytest.mark.asyncio async def test_typing_immediately_routes_to_filter(built_index: Path) -> None: - """No focus shift needed — typed chars land in the filter Input.""" + """No focus shift needed: typed chars land in the filter Input.""" from fnd.tui.settings_screen import SettingsList, SettingsScreen app = FNDApp(index_dir=built_index) @@ -78,7 +78,7 @@ async def test_typing_immediately_routes_to_filter(built_index: Path) -> None: search = screen.query_one("#settings_search", Input) assert search.value == "p" # And the list refreshed in response (cross-section search - # widens the list — what matters is that filtering kicked in). + # widens the list: what matters is that filtering kicked in). after_items = list(screen.query_one(SettingsList)._items) assert after_items != before_items, "filter should refresh the list" @@ -168,7 +168,7 @@ async def test_up_below_top_moves_cursor_only(built_index: Path) -> None: the cursor up one row instead of jumping to the Input. Open Preferences (a sub-screen with several rows), focus the list, - move down a couple of rows, then press Up — focus stays on the list + move down a couple of rows, then press Up: focus stays on the list and cursor_index decreases by one selectable row. """ from fnd.tui.menu import SECTION_PREFERENCES diff --git a/tests/test_settings_menu_p2.py b/tests/test_settings_menu_p2.py index 60443862..30f8dca4 100644 --- a/tests/test_settings_menu_p2.py +++ b/tests/test_settings_menu_p2.py @@ -100,7 +100,7 @@ async def test_cursor_skips_headers_on_keybindings(built_index: Path) -> None: @pytest.mark.asyncio async def test_left_arrow_pops(built_index: Path) -> None: - """`←` is Esc (pop) — back-stack navigation when the list owns + """`←` is Esc (pop): back-stack navigation when the list owns focus. (The filter Input owns Left as text-cursor movement, so arrow down into the list first.)""" from fnd.tui.settings_screen import SettingsScreen @@ -167,7 +167,7 @@ async def test_palette_toggle(built_index: Path) -> None: assert isinstance(app.screen, SettingsScreen) app.action_open_command_palette() # This press CLOSES the stack, so wait for it to go rather than for a - # list to populate — the same action toggles both ways. + # list to populate: the same action toggles both ways. await wait_until( pilot, lambda: not isinstance(app.screen, SettingsScreen), diff --git a/tests/test_settings_nav_fixes.py b/tests/test_settings_nav_fixes.py index 8bd0703a..67df11f8 100644 --- a/tests/test_settings_nav_fixes.py +++ b/tests/test_settings_nav_fixes.py @@ -1,8 +1,8 @@ """Settings navigation fixes: -* Bug A — searching is navigation-only: Enter lands on the first match and +* Bug A: searching is navigation-only: Enter lands on the first match and does NOT fire its effect (no toggle flip, no drill, no side-effect). -* Bug B — returning from a drilled child screen keeps the cursor on the row +* Bug B: returning from a drilled child screen keeps the cursor on the row the user drilled from, not reset to the first row. """ diff --git a/tests/test_settings_p3_keybindings_invoke.py b/tests/test_settings_p3_keybindings_invoke.py index 70f753b1..9fffdf4f 100644 --- a/tests/test_settings_p3_keybindings_invoke.py +++ b/tests/test_settings_p3_keybindings_invoke.py @@ -19,7 +19,7 @@ def built_index(fixtures_dir: Path, tmp_index_dir: Path) -> Path: @pytest.mark.asyncio async def test_pressing_key_in_keybindings_invokes_action(built_index: Path) -> None: - """Spec: Keybindings › Press-key-to-invoke — pressing a listed key + """Spec: Keybindings › Press-key-to-invoke, pressing a listed key dispatches the action and closes the settings stack.""" from fnd.tui.settings_screen import SettingsList, SettingsScreen @@ -61,7 +61,7 @@ async def test_pressing_key_while_search_focused_does_not_invoke(built_index: Pa def test_drill_summary_mode_default_and_validation() -> None: - """Spec: Drill-cue preference — defaults to always_show; validates set.""" + """Spec: Drill-cue preference, defaults to always_show; validates set.""" from pydantic import ValidationError from fnd.config import Defaults @@ -83,7 +83,7 @@ def test_drill_summary_mode_default_and_validation() -> None: async def test_drill_mode_always_ellipsis( built_index: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - """Spec: Drill-cue preference — `always_ellipsis` mode renders `…` + """Spec: Drill-cue preference, `always_ellipsis` mode renders `…` instead of content summaries.""" from fnd.config import write_setting diff --git a/tests/test_settings_p3_reveal.py b/tests/test_settings_p3_reveal.py index edc84ff5..51e3e0af 100644 --- a/tests/test_settings_p3_reveal.py +++ b/tests/test_settings_p3_reveal.py @@ -17,7 +17,7 @@ reason="asserts the macOS `open -R` reveal; cross-platform reveal is covered by test_launcher.py", ) def test_reveal_runs_open_r_on_macos(tmp_path: Path) -> None: - """Spec: Reveal-in-Finder — uses `open -R ` on macOS.""" + """Spec: Reveal-in-Finder, uses `open -R ` on macOS.""" from fnd import opener p = tmp_path / "x.toml" @@ -41,7 +41,7 @@ def built_index(fixtures_dir: Path, tmp_index_dir: Path) -> Path: @pytest.mark.asyncio async def test_root_has_open_keybindings_file(built_index: Path) -> None: - """Spec: IA › Root — sibling action for the keybindings TOML.""" + """Spec: IA › Root, sibling action for the keybindings TOML.""" from fnd.tui import FNDApp from fnd.tui.settings_screen import SettingsList @@ -57,7 +57,7 @@ async def test_root_has_open_keybindings_file(built_index: Path) -> None: @pytest.mark.asyncio async def test_shift_enter_on_open_config_calls_reveal(built_index: Path) -> None: - """Spec: Reveal pattern — Shift+Enter on the Open config row reveals + """Spec: Reveal pattern, Shift+Enter on the Open config row reveals config.toml in Finder.""" from fnd.tui import FNDApp from fnd.tui.settings_screen import SettingsList, SettingsScreen diff --git a/tests/test_settings_p3_search.py b/tests/test_settings_p3_search.py index 1b00e3de..53c6f262 100644 --- a/tests/test_settings_p3_search.py +++ b/tests/test_settings_p3_search.py @@ -30,7 +30,7 @@ def _seed_config(fixtures_dir: Path) -> Config: @pytest.mark.asyncio async def test_walk_all_sections_includes_every_leaf(built_index: Path, fixtures_dir: Path) -> None: - """Spec: Search behaviour › Index — walker covers Preferences, + """Spec: Search behaviour › Index, walker covers Preferences, Collections, Keybindings, and root-level actions.""" from fnd.tui.menu import KIND_HEADER, walk_all_sections @@ -44,7 +44,7 @@ async def test_walk_all_sections_includes_every_leaf(built_index: Path, fixtures # Collections section includes the per-collection drill row. assert "default" in labels # Keybindings keys (sample). The provider derives from the - # action registry — label is now the short title (Action + # action registry: label is now the short title (Action # footer_label / command), description carries the long-form # explanation that surfaces in the DetailStrip. assert any(item.label == "Quit" for _, item in all_items) @@ -57,7 +57,7 @@ async def test_walk_all_sections_includes_every_leaf(built_index: Path, fixtures @pytest.mark.asyncio async def test_walk_includes_scope_pseudo_row(built_index: Path, fixtures_dir: Path) -> None: - """Spec: Use cases › D — pre-empt confusion about active scope by + """Spec: Use cases › D, pre-empt confusion about active scope by surfacing a sidebar pointer in cross-section results.""" from fnd.tui.menu import walk_all_sections @@ -78,7 +78,7 @@ async def test_walk_includes_scope_pseudo_row(built_index: Path, fixtures_dir: P @pytest.mark.asyncio async def test_search_on_root_finds_preferences_leaf(built_index: Path, fixtures_dir: Path) -> None: - """Spec: Search behaviour — typing on root surfaces leaves from + """Spec: Search behaviour, typing on root surfaces leaves from every section, with the breadcrumb on each row.""" from textual.widgets import Input @@ -104,7 +104,7 @@ async def test_search_on_root_finds_preferences_leaf(built_index: Path, fixtures async def test_search_on_keybindings_finds_preference( built_index: Path, fixtures_dir: Path ) -> None: - """Spec: Cross-section search is global — searching from a sub-screen + """Spec: Cross-section search is global, searching from a sub-screen finds items in other sections.""" from textual.widgets import Input @@ -130,7 +130,7 @@ async def test_search_enter_navigates_only_then_second_enter_acts( built_index: Path, fixtures_dir: Path ) -> None: """Spec (revised): search is navigation-only. Enter in the search box - lands focus on the first match and fires NO effect — no edit bar, no + lands focus on the first match and fires NO effect: no edit bar, no toggle, no drill, no side-effect. A second Enter on the now-focused list performs the action (here: opens the scalar's edit bar inline).""" from textual.widgets import Input @@ -169,7 +169,7 @@ async def test_search_enter_navigates_only_then_second_enter_acts( def test_search_result_label_has_bold_substring_for_query() -> None: - """Spec: Search › Match display — matched substring rendered bold.""" + """Spec: Search › Match display, matched substring rendered bold.""" from fnd.tui.menu import KIND_SCALAR, MenuItem from fnd.tui.settings_screen import _render_row @@ -204,7 +204,7 @@ def test_render_row_no_highlight_when_query_misses() -> None: @pytest.mark.asyncio async def test_zero_match_shows_empty_state_hint(built_index: Path) -> None: - """Spec: Search › Empty-state hint — `No matches for ''` placeholder.""" + """Spec: Search › Empty-state hint; `No matches for ''` placeholder.""" from textual.widgets import Input from fnd.tui.settings_screen import SettingsList, SettingsScreen @@ -225,7 +225,7 @@ async def test_zero_match_shows_empty_state_hint(built_index: Path) -> None: def test_filter_haystack_excludes_description() -> None: - """Spec: Search index covers label/key/keywords/breadcrumb — NOT + """Spec: Search index covers label/key/keywords/breadcrumb, NOT description prose. Indexing descriptions muddies results.""" import asyncio diff --git a/tests/test_settings_p3_visual.py b/tests/test_settings_p3_visual.py index bba9289e..e4974b18 100644 --- a/tests/test_settings_p3_visual.py +++ b/tests/test_settings_p3_visual.py @@ -11,7 +11,7 @@ def test_indexer_filetypes_exposed_and_complete() -> None: - """Spec: Add Collection wizard › Includes — file types come from a + """Spec: Add Collection wizard › Includes, file types come from a single source of truth, not hardcoded in two places.""" from fnd.config import INDEXER_FILETYPES from fnd.kinds import ALL_KIND_IDS @@ -33,7 +33,7 @@ def test_indexer_filetypes_exposed_and_complete() -> None: def test_f3_no_longer_in_keymap() -> None: - """Spec: Locked decisions — F3 dropped.""" + """Spec: Locked decisions, F3 dropped.""" from fnd.tui.actions import load_keymap keymap = load_keymap() @@ -43,12 +43,12 @@ def test_f3_no_longer_in_keymap() -> None: def test_detail_strip_renders_description_and_metadata() -> None: - """Spec: Visual system › Detail strip — 2 lines, description then + """Spec: Visual system › Detail strip, 2 lines, description then metadata in $text-muted.""" from fnd.tui.widgets.detail_strip import DetailStrip strip = DetailStrip() - strip._description = "Result limit (1–1000) — max results returned per query." + strip._description = "Result limit (1 to 1000): max results returned per query." strip._metadata = "Stored in defaults.result_limit · Applies on next search" rendered = strip._render_lines() assert len(rendered) == 2 @@ -57,7 +57,7 @@ def test_detail_strip_renders_description_and_metadata() -> None: def test_row_with_key_renders_bracketed_accent() -> None: - """Spec: Visual system › Key style — bracketed `[o]` accent.""" + """Spec: Visual system › Key style, bracketed `[o]` accent.""" from fnd.tui.menu import KIND_ACTION, MenuItem from fnd.tui.settings_screen import _render_row @@ -75,7 +75,7 @@ def test_row_with_key_renders_bracketed_accent() -> None: def test_root_container_hugs_content() -> None: - """Spec: Visual system › Container — height: auto, not 1fr.""" + """Spec: Visual system › Container, height: auto, not 1fr.""" from fnd.tui.settings_screen import SettingsScreen css = SettingsScreen.CSS @@ -98,7 +98,7 @@ def built_index(fixtures_dir: Path, tmp_index_dir: Path) -> Path: async def test_root_rows_show_trailing_summaries( built_index: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - """Spec: IA › Root — every drill row shows what's inside (always_show mode).""" + """Spec: IA › Root, every drill row shows what's inside (always_show mode).""" from fnd.tui import FNDApp from fnd.tui.settings_screen import SettingsList, SettingsScreen @@ -128,7 +128,7 @@ async def test_root_rows_show_trailing_summaries( async def test_collection_row_shows_source_count_and_ranking( built_index: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - """Spec: IA › Collections sub-screen — each collection row's trailing + """Spec: IA › Collections sub-screen, each collection row's trailing shows source count and `ranking:` with scope dot prefix.""" from fnd.config import ( CollectionConfig, @@ -168,7 +168,7 @@ async def test_collection_row_shows_source_count_and_ranking( async def test_source_row_shows_filetypes_and_path_warning( tmp_path: Path, built_index: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - """Spec: IA › Sources sub-screen — source rows show file-types and + """Spec: IA › Sources sub-screen, source rows show file-types and `⚠ path not found` when the path no longer resolves.""" from fnd.config import ( CollectionConfig, @@ -212,7 +212,7 @@ async def test_source_row_shows_filetypes_and_path_warning( @pytest.mark.asyncio async def test_detail_strip_updates_on_cursor_move(built_index: Path) -> None: - """Spec: Visual system › Detail strip — populates on focus change.""" + """Spec: Visual system › Detail strip, populates on focus change.""" from fnd.tui import FNDApp from fnd.tui.settings_screen import SettingsList, SettingsScreen from fnd.tui.widgets.detail_strip import DetailStrip @@ -248,7 +248,7 @@ async def test_detail_strip_updates_on_cursor_move(built_index: Path) -> None: async def test_hint_bar_appends_reveal_when_cursor_on_reveal_capable_row( built_index: Path, ) -> None: - """Spec: Hint bar — append `Shift+⏎ Reveal` when row supports reveal.""" + """Spec: Hint bar, append `Shift+⏎ Reveal` when row supports reveal.""" from fnd.tui import FNDApp from fnd.tui.settings_screen import SettingsList, SettingsScreen @@ -267,7 +267,7 @@ async def test_hint_bar_appends_reveal_when_cursor_on_reveal_capable_row( idx = next(i for i, it in enumerate(lst._items) if it.id == "root.open_config_file") lst.cursor_index = idx await pilot.pause() - # Assert on the resolved cluster (the actual logic) — Static.render + # Assert on the resolved cluster (the actual logic): Static.render # in the test pilot doesn't always reflect the last .update() call. cluster = screen._hint_cluster() labels = [label for _, label in cluster] @@ -278,7 +278,7 @@ async def test_hint_bar_appends_reveal_when_cursor_on_reveal_capable_row( @pytest.mark.asyncio async def test_hint_bar_keybindings_variant(built_index: Path) -> None: - """Spec: Hint bar — Keybindings screen shows `⏎ Run · [key] Run directly · Esc Back`.""" + """Spec: Hint bar, Keybindings screen shows `⏎ Run · [key] Run directly · Esc Back`.""" from fnd.tui import FNDApp from fnd.tui.settings_screen import SettingsList, SettingsScreen @@ -300,7 +300,7 @@ async def test_hint_bar_keybindings_variant(built_index: Path) -> None: @pytest.mark.asyncio async def test_hint_bar_search_focused_variant(built_index: Path) -> None: - """Spec: Hint bar — Search input focused shows results / open-first / clear hints.""" + """Spec: Hint bar, Search input focused shows results / open-first / clear hints.""" from textual.widgets import Input from fnd.tui import FNDApp @@ -361,7 +361,7 @@ async def test_hint_bar_edit_bar_open_variant(built_index: Path) -> None: @pytest.mark.asyncio async def test_root_screen_shows_version_status_line(built_index: Path) -> None: - """Spec: Use case A4 — version visible at the bottom of the root menu.""" + """Spec: Use case A4, version visible at the bottom of the root menu.""" from textual.widgets import Static from fnd import __version__ @@ -382,7 +382,7 @@ async def test_root_screen_shows_version_status_line(built_index: Path) -> None: @pytest.mark.asyncio async def test_subscreen_omits_version_status(built_index: Path) -> None: - """Sub-screens don't carry the version line — only the root does.""" + """Sub-screens don't carry the version line: only the root does.""" from fnd.tui import FNDApp from fnd.tui.settings_screen import SettingsScreen @@ -410,7 +410,7 @@ async def test_preferences_refreshes_trailing_after_picker_pops( ) -> None: """Regression: drilling into a KIND_PICKER setting (e.g. Drill row summaries) commits the new value via the picker, which pops. The - parent SettingsScreen must re-render its trailings on resume — + parent SettingsScreen must re-render its trailings on resume, otherwise the user sees the old value until they leave and return.""" from fnd.tui import FNDApp from fnd.tui.menu import SECTION_PREFERENCES @@ -460,7 +460,7 @@ async def test_preferences_refreshes_trailing_after_picker_pops( @pytest.mark.asyncio async def test_on_reindex_complete_swaps_searcher(fixtures_dir: Path, tmp_index_dir: Path) -> None: """Regression: after a reindex finishes the in-memory Searcher must - be rebuilt — otherwise the captured ``self._index.searcher()`` keeps + be rebuilt, otherwise the captured ``self._index.searcher()`` keeps returning hits from the pre-rebuild generation and the user sees zero results until they restart the app.""" from fnd.index import build_index @@ -475,7 +475,7 @@ async def test_on_reindex_complete_swaps_searcher(fixtures_dir: Path, tmp_index_ app._indexer.on_reindex_complete() assert app._search.searcher is not None assert app._search.searcher is not original, ( - "Searcher instance was not replaced — stale searcher would keep " + "Searcher instance was not replaced: stale searcher would keep " "querying the pre-rebuild index generation" ) @@ -524,7 +524,7 @@ async def test_on_screen_resume_rebuilds_items_from_provider( before_ids = [it.id for it in lst._items] # Simulate a child screen mutating config: append a second source. cfg.collections["default"].sources.append(SourceConfig(path=_Path(str(tmp_path)))) - # Fire on_screen_resume directly — equivalent to a child popping. + # Fire on_screen_resume directly: equivalent to a child popping. screen.on_screen_resume() await pilot.pause() after_ids = [it.id for it in lst._items] @@ -615,7 +615,7 @@ async def test_collections_sidebar_toggle_preserves_cursor( await pilot.pause() cursor_before = tree.cursor_line assert cursor_before > 0, "expected cursor to land on a non-root row" - # Fire the selection directly — Tree.action_select_cursor in + # Fire the selection directly: Tree.action_select_cursor in # headless tests doesn't always post NodeSelected via the focus # path. Posting the message exercises the real handler. tree.post_message(Tree.NodeSelected(bravo)) @@ -634,7 +634,7 @@ async def test_cursor_move_does_not_call_render_all( fixtures_dir: Path, tmp_index_dir: Path ) -> None: """Regression / perf: arrow-key cursor movement must NOT call - SettingsList._render_all — that path rebuilds every row's Rich + SettingsList._render_all: that path rebuilds every row's Rich Text and was the dominant per-keystroke cost on long lists like Keybindings.""" from fnd.index import build_index @@ -671,7 +671,7 @@ def _counting_render() -> None: assert counter["n"] == baseline, ( f"watch_cursor_index called _render_all " f"{counter['n'] - baseline} times across 10 cursor moves; " - "should be zero — only -cursor class toggles, no row rerenders" + "should be zero: only -cursor class toggles, no row rerenders" ) diff --git a/tests/test_settings_p3_wizard.py b/tests/test_settings_p3_wizard.py index 5855cc8e..71ff4428 100644 --- a/tests/test_settings_p3_wizard.py +++ b/tests/test_settings_p3_wizard.py @@ -85,7 +85,7 @@ async def test_add_collection_pushes_wizard_with_expected_fields(built_index: Pa @pytest.mark.asyncio async def test_path_validation_inline(tmp_path: Path, built_index: Path) -> None: - """Spec: Wizard › Source path — live ✓/✗ inline validation.""" + """Spec: Wizard › Source path, live ✓/✗ inline validation.""" from textual.widgets import Input, Static from fnd.tui.settings_screen import ( @@ -117,7 +117,7 @@ def error_text() -> str: # Path validation is debounced to avoid a per-keystroke disk walk. The # debounce plus the walk plus the repaint is real work, and sleeping the - # debounce is a wait for it only while the machine is idle — on Windows + # debounce is a wait for it only while the machine is idle: on Windows # the second read returned the FIRST path's verdict. bar.query_one("#editor_input", Input).value = str(tmp_path / "nope") await wait_until( @@ -140,7 +140,7 @@ def error_text() -> str: async def test_save_writes_collection_and_reindexes( tmp_path: Path, built_index: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - """Spec: Wizard › Save — write_collection + reindex + drop on per-collection sub-screen.""" + """Spec: Wizard › Save, write_collection + reindex + drop on per-collection sub-screen.""" from fnd.config import load from fnd.tui.settings_screen import ( AddCollectionWizard, @@ -195,7 +195,7 @@ async def test_save_writes_collection_and_reindexes( async def test_esc_discards_wizard_with_no_side_effects( tmp_path: Path, built_index: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - """Spec: Wizard › Esc — cancelling after typing a name does NOT + """Spec: Wizard › Esc, cancelling after typing a name does NOT create an empty collection.""" from fnd.config import default_config_path, load from fnd.tui.settings_screen import AddCollectionWizard @@ -298,7 +298,7 @@ async def test_source_form_shows_include_globs_as_ticked_file_types( async def test_save_with_missing_name_shows_inline_error( built_index: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - """Spec: Locked decision #12 — inline error, no toast.""" + """Spec: Locked decision #12, inline error, no toast.""" from textual.widgets import Static from fnd.tui.settings_screen import AddCollectionWizard diff --git a/tests/test_settings_structured_pdf.py b/tests/test_settings_structured_pdf.py index ead271d1..15c509c0 100644 --- a/tests/test_settings_structured_pdf.py +++ b/tests/test_settings_structured_pdf.py @@ -1,8 +1,8 @@ -"""Structured PDF section + confirm screen — tests follow +"""Structured PDF section + confirm screen: tests follow dev/docs/test_patterns/settings_screen.md. The confirm screen body is state-dependent (install vs uninstall copy). -Step 6b wires the actual install/uninstall worker — these tests only +Step 6b wires the actual install/uninstall worker: these tests only verify the disclosure + Yes/Cancel pattern. """ @@ -53,7 +53,7 @@ def _fake_installed(monkeypatch: pytest.MonkeyPatch) -> None: # pyright: ignore monkeypatch.setattr("fnd.extras.installed_packages", lambda e: list(e.packages)) -# 1 — Indexing screen surfaces structured-PDF rows +# 1: Indexing screen surfaces structured-PDF rows @pytest.mark.usefixtures("_fake_not_installed") @@ -105,7 +105,7 @@ async def test_install_label_flips_when_installed(built_index: Path, cfg: Config assert "Uninstall" in row.label -# 2 — Confirm screen chrome +# 2: Confirm screen chrome @pytest.mark.usefixtures("_fake_not_installed") @@ -154,7 +154,7 @@ async def test_uninstall_confirm_chrome_when_installed(built_index: Path, cfg: C assert screen.has_class("-recoverable") -# 3 — Body content covers the cost narrative +# 3: Body content covers the cost narrative @pytest.mark.usefixtures("_fake_not_installed") @@ -162,7 +162,7 @@ async def test_uninstall_confirm_chrome_when_installed(built_index: Path, cfg: C async def test_install_disclosure_covers_costs(built_index: Path, cfg: Config) -> None: """Install body must mention disk + ML weights + per-PDF cost so the user knows what they're opting into. The body uses the - Outcome / Cost / Safety template — checks reflect that.""" + Outcome / Cost / Safety template: checks reflect that.""" from fnd.tui.menu import _open_pdf_install_confirm app = FNDApp(index_dir=built_index, config=cfg) @@ -180,7 +180,7 @@ async def test_install_disclosure_covers_costs(built_index: Path, cfg: Config) - assert "structured" in body -# 4 — Keyboard equivalence +# 4: Keyboard equivalence @pytest.mark.usefixtures("_fake_not_installed") @@ -216,7 +216,7 @@ async def test_install_esc_cancels(built_index: Path, cfg: Config) -> None: assert not isinstance(app.screen, StructuredPdfConfirmScreen) -# 5 — Cross-section search +# 5: Cross-section search @pytest.mark.usefixtures("_fake_not_installed") diff --git a/tests/test_settings_visual_language.py b/tests/test_settings_visual_language.py index 3d858b15..704da4eb 100644 --- a/tests/test_settings_visual_language.py +++ b/tests/test_settings_visual_language.py @@ -267,7 +267,7 @@ def test_action_button_visible_under_realistic_width() -> None: def test_keybindings_row_has_no_trailing_button() -> None: """Documentation rows on the Keybindings cheat sheet (KIND_ACTION - with a ``key`` field) MUST NOT render a `[ Run ]` affordance — + with a ``key`` field) MUST NOT render a `[ Run ]` affordance: the leading [key] glyph is the affordance, and a trailing button on every row both clutters the page and pushes [key] off the right edge under narrow widths.""" @@ -315,7 +315,7 @@ def test_keybindings_row_fits_under_narrow_width() -> None: ) def test_hint_cluster_action_label_matches_kind(kind: str, expected_verb: str) -> None: """The cursor row's kind determines the ⏎ label so the footer never lies.""" - # Hint-cluster derivation tested indirectly here — render is the + # Hint-cluster derivation tested indirectly here: render is the # actual end-user path. Direct pilot tests live in # test_settings_indexing.py / cache_maintenance.py. from fnd.tui.menu import ( diff --git a/tests/test_sidebar_layout.py b/tests/test_sidebar_layout.py index 16fed0e2..76fff430 100644 --- a/tests/test_sidebar_layout.py +++ b/tests/test_sidebar_layout.py @@ -1,6 +1,6 @@ """Policy tests for the dynamic sidebar height allocator. -The allocator is pure — measured demands in, cell heights out — so the whole +The allocator is pure (measured demands in, cell heights out) so the whole policy is pinned here without a running app. Panels are given in priority order (Results first). """ @@ -40,7 +40,7 @@ def test_fits_content_sizes_each_and_pools_slack_in_priority() -> None: def test_short_priority_hands_space_to_hungry_secondaries() -> None: # The case a static 50% floor gets wrong: Results wants almost nothing but - # the secondaries are long — the slack must flow to them, not sit empty in + # the secondaries are long: the slack must flow to them, not sit empty in # Results. h = allocate(45, _panels(5, 42, 42)) assert h["R"] == 5 @@ -69,7 +69,7 @@ def test_priority_reclaims_unused_secondary_space() -> None: def test_short_secondary_is_satisfied_before_a_long_one_scrolls() -> None: # Max-min fairness: a 6-row filter list beside a 42-row collections list - # must be shown in full while collections scrolls — not both cut to equal + # must be shown in full while collections scrolls, not both cut to equal # shares. h = allocate(45, _panels(52, 42, 6)) assert h["F"] == 6, "short secondary should get its full demand" @@ -100,7 +100,7 @@ def test_tiny_terminal_still_gives_priority_the_majority() -> None: def test_never_over_allocates_the_column() -> None: # Fuzz a spread of demands/sizes; the allocation must never exceed avail. # Includes tiny columns (1-9 rows) where a naive per-panel floor of 1 would - # over-allocate — the case CodeRabbit caught that the >=10 sweep missed. + # over-allocate: the case CodeRabbit caught that the >=10 sweep missed. for avail in (1, 2, 3, 5, 7, 9, 10, 16, 24, 45, 60): for r in (1, 5, 30, 200): for c in (1, 4, 40): diff --git a/tests/test_sidebar_preview_regressions.py b/tests/test_sidebar_preview_regressions.py index aeb236d7..a819ac7c 100644 --- a/tests/test_sidebar_preview_regressions.py +++ b/tests/test_sidebar_preview_regressions.py @@ -34,7 +34,7 @@ def built_index(fixtures_dir: Path, tmp_index_dir: Path) -> Path: def cfg(fixtures_dir: Path, tmp_path: Path) -> Config: """A self-contained config with three collections so the collections panel populates deterministically. These tests assert on the panel / - saved-scope state, which is driven by the config — without an explicit + saved-scope state, which is driven by the config, without an explicit one the app would read whatever config happens to be on disk, so it passed on a developer machine with collections configured and timed out on a clean CI runner with an empty config.""" @@ -74,16 +74,16 @@ async def test_preview_loads_after_back_to_back_queries(built_index: Path) -> No pilot, lambda: bool(app._search.groups) and app._preview.parent_id is not None, timeout=30.0, - message="test setup — the initial query never produced a preview", + message="test setup: the initial query never produced a preview", ) await run_search(pilot, app, "penguin") - assert app._search.groups, "test setup — second query produced no results" + assert app._search.groups, "test setup: second query produced no results" await wait_until( pilot, lambda: app._preview.parent_id is not None, timeout=30.0, message=( - "preview pane stayed empty on the second query — the cursor " + "preview pane stayed empty on the second query: the cursor " "landed on the same line as the previous query and the " "NodeHighlighted event was suppressed" ), @@ -202,7 +202,7 @@ async def test_toggle_with_active_query_reruns_without_focus_shift( app = FNDApp(index_dir=built_index, config=cfg, initial_query="blue penguin sandwich") async with app.run_test() as pilot: await safe_pause(pilot) - assert app._search.groups, "test setup — initial query produced no results" + assert app._search.groups, "test setup: initial query produced no results" ctree = app.query_one("#collections_panel_tree", Tree) ctree.focus() await settle(pilot) @@ -251,7 +251,7 @@ async def test_collapse_state_survives_collection_cli_flag( fpane = app.query_one("#filters_pane") # Scope override took effect. assert app._scope.collections == ["default"] - # Panel layout was restored from disk — wait for the saved + # Panel layout was restored from disk: wait for the saved # collapsed state to settle onto the trees, including the # expanded_collections / expanded_filter_branches lists which # restore through a separate async path that can lag the diff --git a/tests/test_sidebar_reflow_integration.py b/tests/test_sidebar_reflow_integration.py index d1458555..c580aa37 100644 --- a/tests/test_sidebar_reflow_integration.py +++ b/tests/test_sidebar_reflow_integration.py @@ -58,7 +58,7 @@ async def test_short_filters_pane_does_not_outsize_results(cfg: Config, idx: Pat assert results.region.height > filters_pane.region.height assert filters_pane.region.height <= 10, ( f"filters pane {filters_pane.region.height} rows for a handful of " - "filter rows — it ballooned" + "filter rows: it ballooned" ) diff --git a/tests/test_sigint_kill_switch.py b/tests/test_sigint_kill_switch.py index b918977f..6c984ada 100644 --- a/tests/test_sigint_kill_switch.py +++ b/tests/test_sigint_kill_switch.py @@ -1,4 +1,4 @@ -"""Two-press Ctrl+C escape hatch — sets TEXTUAL_ALLOW_SIGNALS so the +"""Two-press Ctrl+C escape hatch: sets TEXTUAL_ALLOW_SIGNALS so the tty driver converts ^C to SIGINT even from a blocked event loop, then installs a handler that exits the app cleanly on the first press and unconditionally hard-exits on any subsequent press. @@ -74,7 +74,7 @@ def test_first_sigint_schedules_app_exit_on_the_loop() -> None: def test_second_sigint_unconditionally_hard_exits() -> None: - """Any second ^C — even seconds after the first — must hard-exit. + """Any second ^C, even seconds after the first, must hard-exit. Previous time-window logic missed real-world press cadence.""" app = _stub_app() fake_loop = MagicMock() diff --git a/tests/test_snippet_anchors_on_highlights.py b/tests/test_snippet_anchors_on_highlights.py index adcd4de0..f7a613f0 100644 --- a/tests/test_snippet_anchors_on_highlights.py +++ b/tests/test_snippet_anchors_on_highlights.py @@ -3,7 +3,7 @@ ``_make_snippet`` used to locate its window with literal substring containment, so a hit reached through the stemmer ("testing" for "test"), a wildcard, or a quoted phrase found no anchor at all and fell back to the chunk's opening 240 -characters — a listed row whose snippet showed no match. Measured at 26 of 993 +characters: a listed row whose snippet showed no match. Measured at 26 of 993 rows on a real corpus before the change. """ diff --git a/tests/test_source_app_config.py b/tests/test_source_app_config.py index aa7fca51..d43116c8 100644 --- a/tests/test_source_app_config.py +++ b/tests/test_source_app_config.py @@ -2,10 +2,10 @@ The new optional fields on :class:`fnd.config.SourceConfig`: -* ``app`` — single app id (sugar for "use this app for every filetype it +* ``app``: single app id (sugar for "use this app for every filetype it handles"). -* ``app_for`` — explicit per-filetype mapping (``{"md": "obsidian"}``). -* ``app_params`` — free-form key/value bag for template variables +* ``app_for``: explicit per-filetype mapping (``{"md": "obsidian"}``). +* ``app_params``: free-form key/value bag for template variables (``vault``, custom user vars). References to unknown app ids are rejected at the top-level @@ -83,7 +83,7 @@ def test_config_rejects_unknown_app_in_source_app_for() -> None: def test_config_user_app_id_visible_to_source_validation() -> None: - """A source can reference an id defined in [apps.] — not just + """A source can reference an id defined in [apps.], not just built-ins.""" Config.model_validate( { @@ -129,7 +129,7 @@ def test_open_smart_passes_vault_through_to_request( monkeypatch.setattr(apps_mod, "_obsidian_app_exists", lambda: True) # Make config loading return a minimal Config that defaults md→obsidian. # opener.open_smart does ``from fnd.config import load as load_config`` - # at call time — patch the source attribute so the lookup sees the stub. + # at call time: patch the source attribute so the lookup sees the stub. import fnd.config as cfg_mod monkeypatch.setattr( diff --git a/tests/test_source_form_app_picker.py b/tests/test_source_form_app_picker.py index b28eb87c..18675de1 100644 --- a/tests/test_source_form_app_picker.py +++ b/tests/test_source_form_app_picker.py @@ -22,7 +22,7 @@ class _FakeSettingsList: - """Stand-in for the form's SettingsList — only refresh_values is + """Stand-in for the form's SettingsList: only refresh_values is called by the actions we exercise here.""" def __init__(self) -> None: @@ -118,7 +118,7 @@ def test_set_app_obsidian_leaves_vault_empty_when_no_vault_found( # save-close round-trip were collapsed into integration coverage: # the registry contents are already pinned by tests/test_apps_registry.py, # and the SourceFormScreen's persistence path is implicitly exercised -# by tests/test_clone_source.py (every field — app + app_params -# included — round-trips through write_collection + load). The form- +# by tests/test_clone_source.py (every field, app + app_params +# included, round-trips through write_collection + load). The form- # specific behaviour above (vault auto-detect, set/clear of the app # override) is what couldn't be reached via lower-level tests. diff --git a/tests/test_stall_watch.py b/tests/test_stall_watch.py index 9983ad66..094eb5af 100644 --- a/tests/test_stall_watch.py +++ b/tests/test_stall_watch.py @@ -1,6 +1,6 @@ """The stall watch must fire, name what was running, and stay off by default. -A diagnostic that silently stops reporting is worse than none — it turns "no +A diagnostic that silently stops reporting is worse than none: it turns "no stalls logged" into evidence when it is really an absence of evidence. So the tests here are that it arms only when asked, that a genuinely blocked loop produces a line, and that the line carries the attribution that makes it useful. @@ -45,7 +45,7 @@ class _FakePrefetch: class _FakeApp: - """Only what the watch reads — it must not need a running TUI to work.""" + """Only what the watch reads: it must not need a running TUI to work.""" def __init__(self) -> None: self.lines: list[str] = [] @@ -84,7 +84,7 @@ async def test_a_blocked_loop_is_reported_with_what_was_running() -> None: watch = StallWatch(app, threshold_ms=150) # type: ignore[arg-type] watch.start() await asyncio.sleep(0.1) - # Block the loop the way a capture does — synchronously, no awaits — and + # Block the loop the way a capture does (synchronously, no awaits) and # then FINISH, clearing the marker before the watch can wake. That is what # real work does, and it is why sampling only on waking named the successor # and exonerated the culprit every time. @@ -132,7 +132,7 @@ async def test_cpu_time_separates_real_work_from_a_sleeping_process() -> None: watch = StallWatch(app, threshold_ms=150) # type: ignore[arg-type] watch.start() await asyncio.sleep(0.1) - # A BUSY block — real work, not a sleep. Bounded by CPU time, as + # A BUSY block: real work, not a sleep. Bounded by CPU time, as # StallWatch measures it: a starved run gets a fraction of a core, so a # wall-clock spin burned 78-193ms of its 400ms and failed a correct figure. cpu_target = time.process_time() + 0.4 @@ -145,7 +145,7 @@ async def test_cpu_time_separates_real_work_from_a_sleeping_process() -> None: assert stalls, f"a 400ms busy block went unreported: {app.lines}" cpu_ms = float(stalls[0].split("cpu=", 1)[1].split("ms", 1)[0]) assert cpu_ms > 200, ( - f"work that spun for 400ms reported only {cpu_ms}ms of CPU — the figure " + f"work that spun for 400ms reported only {cpu_ms}ms of CPU: the figure " f"cannot separate a blocked loop from an idle one: {stalls[0]}" ) @@ -192,7 +192,7 @@ def _stack(*frames: tuple[str, str]) -> traceback.StackSummary: expected = "presenter.py:_mount_chunks_async < app.py:_process_messages" assert stack_key(posix) == expected assert stack_key(windows) == expected, ( - "a Windows stack keyed off something other than its frames — " + "a Windows stack keyed off something other than its frames: " "unnormalised separators drop every frame and collapse to ''" ) @@ -219,6 +219,6 @@ async def test_a_blocked_loop_that_burns_nothing_reports_almost_no_cpu() -> None cpu_ms = float(stalls[0].split("cpu=", 1)[1].split("ms", 1)[0]) # An upper bound, so contention can only push the figure further under it. assert cpu_ms < 50, ( - f"a loop that slept for 450ms reported {cpu_ms}ms of CPU — the figure " + f"a loop that slept for 450ms reported {cpu_ms}ms of CPU: the figure " f"cannot separate a blocked loop from real work: {stalls[0]}" ) diff --git a/tests/test_state.py b/tests/test_state.py index 3e4fb2bb..1fe1adda 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -29,7 +29,7 @@ def test_save_then_load_round_trips_filters(tmp_path: Path) -> None: """Filter selections must round-trip alongside scope state. Empty ``filter_kinds`` = "all kinds"; ``filter_date == 'any'`` = "any - date" — the absence of an explicit selection. + date": the absence of an explicit selection. """ p = tmp_path / "scope.toml" original = UiState( @@ -42,7 +42,7 @@ def test_save_then_load_round_trips_filters(tmp_path: Path) -> None: def test_load_partial_filters_table(tmp_path: Path) -> None: - """A scope.toml without [filters] should still load — the filters + """A scope.toml without [filters] should still load: the filters feature shouldn't break anyone whose state predates it.""" p = tmp_path / "scope.toml" p.write_text( diff --git a/tests/test_stopword_highlight.py b/tests/test_stopword_highlight.py index 4a357d90..8e9971ab 100644 --- a/tests/test_stopword_highlight.py +++ b/tests/test_stopword_highlight.py @@ -1,4 +1,4 @@ -"""Stopwords add highlight noise with no value — don't bold them doc-wide. +"""Stopwords add highlight noise with no value: don't bold them doc-wide. A query like ``segmentation and defence in depth`` should highlight the content words but not light up every ``and`` / ``in`` in the document. A *quoted* phrase @@ -55,12 +55,12 @@ def _stopword_lit(text: str, spec: MatchSpec, word: str) -> bool: def test_incontext_stopword_lit_between_two_matches() -> None: spec = MatchSpec.from_query("defence in depth", auto_fuzzy=False) assert _stopword_lit("a layered defence in depth model", spec, "in") - # hyphenated form tokenizes to defence/in/depth — same contiguous run. + # hyphenated form tokenizes to defence/in/depth: same contiguous run. assert _stopword_lit("the defence-in-depth model", spec, "in") def test_incontext_stopword_lit_in_partial_fragment() -> None: - """'adjacent to any match' — a fragment with one content neighbour still + """'adjacent to any match': a fragment with one content neighbour still lights the connector (user's 'in depth' / 'defence in' cases).""" spec = MatchSpec.from_query("defence in depth", auto_fuzzy=False) assert _stopword_lit("research in depth here", spec, "in") # in+depth diff --git a/tests/test_stopword_recall.py b/tests/test_stopword_recall.py index 8595a448..447329f6 100644 --- a/tests/test_stopword_recall.py +++ b/tests/test_stopword_recall.py @@ -1,7 +1,7 @@ """Unquoted queries must not return stopword-only matches. A chunk that overlaps the query only via a stopword ("and"/"in"/"the") -carries ~zero IDF — it ranks at the tail but was still retrieved, pure +carries ~zero IDF: it ranks at the tail but was still retrieved, pure noise (and, since stopword highlighting was dropped, shows no highlight). A chunk must match at least one CONTENT term to be returned. Quoted phrases keep their stopwords. diff --git a/tests/test_strong_signal.py b/tests/test_strong_signal.py index f4e80785..811a742d 100644 --- a/tests/test_strong_signal.py +++ b/tests/test_strong_signal.py @@ -71,7 +71,7 @@ def test_strong_signal_fires_for_unambiguous_keyword(cfg: Config, unambiguous_in def test_strong_signal_does_not_fire_for_ambiguous_query( cfg: Config, unambiguous_index: Path ) -> None: - """A common word spread across many docs — bypass must NOT fire; + """A common word spread across many docs: bypass must NOT fire; fusion runs as default.""" from fnd.fusion import ( STRONG_SIGNAL_MIN_NORM_GAP, @@ -95,7 +95,7 @@ def test_strong_signal_does_not_fire_for_ambiguous_query( # Bypass must not fire: regime is fusion (or cascade if extremely sparse). assert trace.regime != "strong-signal" assert trace.strong_signal.fired is False - # Probe directly — gap should be tight, normalized top score modest. + # Probe directly: gap should be tight, normalized top score modest. probe = searcher._candidates( "note filler", window=100, collection="notes", metadata_filter=None ).hits @@ -109,7 +109,7 @@ def test_strong_signal_does_not_fire_for_ambiguous_query( def test_intent_disables_strong_signal_bypass(cfg: Config, unambiguous_index: Path) -> None: - """Same unambiguous query, but with intent supplied — bypass must + """Same unambiguous query, but with intent supplied: bypass must be disabled, fusion runs.""" from fnd.layered import _evaluate_strong_signal, search_layered from fnd.query import Searcher @@ -147,7 +147,7 @@ def test_normalise_bm25_monotone_and_bounded() -> None: assert normalise_bm25(-5.0) == 0.0 # negative scores clamped to 0 assert 0.0 < normalise_bm25(0.1) < 1.0 assert normalise_bm25(1.0) == pytest.approx(0.5) - # Monotone — higher input → higher output + # Monotone: higher input → higher output assert normalise_bm25(0.5) < normalise_bm25(1.5) < normalise_bm25(5.0) - # Asymptote — large values approach but never reach 1 + # Asymptote: large values approach but never reach 1 assert 0.99 < normalise_bm25(100.0) < 1.0 diff --git a/tests/test_table_match_coords.py b/tests/test_table_match_coords.py index a1b14785..a3d75d36 100644 --- a/tests/test_table_match_coords.py +++ b/tests/test_table_match_coords.py @@ -41,7 +41,7 @@ def compose(self) -> ComposeResult: assert getattr(dt, "_fnd_match_coord", None) == coords[0] -# A header cell AND the first data row's SAME column both match — both map to +# A header cell AND the first data row's SAME column both match: both map to # (0, col) (the header has no cursor coordinate of its own), so without dedup # the coordinate would appear twice: inflating the count and making n/b land on # the same cell twice. diff --git a/tests/test_table_scroll_anchor.py b/tests/test_table_scroll_anchor.py index 742abb3b..96162fe3 100644 --- a/tests/test_table_scroll_anchor.py +++ b/tests/test_table_scroll_anchor.py @@ -3,7 +3,7 @@ The W3 table renders every row as one full-height ``DataTable`` (no per-cell widgets, no internal scroll), so the matched cell is not its own scrollable widget. ``StructuralScrollStrategy._anchor_region`` resolves the cell's screen -region as the scroll anchor — and returns ``None`` (a retry signal) while the +region as the scroll anchor, and returns ``None`` (a retry signal) while the table's rows are still mounting, so the controller retries instead of committing a scroll to the table top. That cold-mount race is what repeatedly stranded deep-table matches at the top; these lock the contract that prevents diff --git a/tests/test_tag_query.py b/tests/test_tag_query.py index bf65d011..181c6ec8 100644 --- a/tests/test_tag_query.py +++ b/tests/test_tag_query.py @@ -143,7 +143,7 @@ def test_same_tag_across_sources_is_or_not_and(tmp_path: Path) -> None: """A tag fanned across sources means 'from either source'. ANDing them would demand the same tag in frontmatter AND Finder, which - a real file essentially never satisfies — this is what `--tag x` with + a real file essentially never satisfies: this is what `--tag x` with both sources enabled compiles to. """ index = _build(tmp_path) diff --git a/tests/test_texture_cache_versioning.py b/tests/test_texture_cache_versioning.py index e9821e01..f20c542e 100644 --- a/tests/test_texture_cache_versioning.py +++ b/tests/test_texture_cache_versioning.py @@ -37,7 +37,7 @@ def _chunk(md: str = "## H\n\nbody") -> Chunk: def test_texture_signature_is_coarse_and_stable() -> None: """The cache key must NOT churn on pymupdf4llm patch bumps or cfg-flag - tweaks — only on a manual TEXTURE_VERSION bump.""" + tweaks: only on a manual TEXTURE_VERSION bump.""" sig = pdfmod.texture_signature() assert sig == f"tex-v{pdfmod.TEXTURE_VERSION}" assert "pymupdf4llm" not in sig @@ -78,7 +78,7 @@ def test_promote_only_current_engine_entries(tmp_path: Path) -> None: assert migrated == 1 assert failed == 0 assert cache.get(f"{cur}--tex-v1") is not None - # The genuinely-older entry is left in place — check via get() (which + # The genuinely-older entry is left in place: check via get() (which # round-trips through entry_path) so the assertion is filename-encoding # agnostic (Windows sanitises the ``|`` in the on-disk name). assert cache.get(f"{old}--pymupdf4llm-1.20|cfg-OLDHASH") is not None @@ -98,7 +98,7 @@ def test_signature_bump_reuses_prior_texture( pytest.skip("pymupdf4llm not installed") cache = PdfStructureCache(root=tmp_path) monkeypatch.setattr(pdfmod, "_cache_singleton", cache) - list(pdfmod.extract(FIXTURE)) # current version — populates cache + list(pdfmod.extract(FIXTURE)) # current version: populates cache monkeypatch.setattr(pdfmod, "TEXTURE_VERSION", 99) # "major update" hits_before = cache.hits chunks = list(pdfmod.extract(FIXTURE)) # must reuse via get_any @@ -111,7 +111,7 @@ def test_force_fresh_ignores_prior_signature( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """Re-texturise-outdated mode (force_fresh) must NOT reuse an - older-signature entry — it re-extracts under the current signature.""" + older-signature entry: it re-extracts under the current signature.""" if not pdfmod._HAS_PYMUPDF4LLM: pytest.skip("pymupdf4llm not installed") cache = PdfStructureCache(root=tmp_path) @@ -130,7 +130,7 @@ def test_force_fresh_reextracts_even_current_signature( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """Rebuild (force_fresh) must re-extract even a PDF already textured at - the CURRENT signature — bypassing the cache entirely. Regression for a + the CURRENT signature: bypassing the cache entirely. Regression for a Rebuild that served the current-signature cache and so silently ran a plain incremental update instead of re-texturising.""" if not pdfmod._HAS_PYMUPDF4LLM: diff --git a/tests/test_texture_capabilities.py b/tests/test_texture_capabilities.py index 78c7338e..ea0353a6 100644 --- a/tests/test_texture_capabilities.py +++ b/tests/test_texture_capabilities.py @@ -2,8 +2,8 @@ Measured on this corpus: 18 PDFs hold an empty texture under the current signature, all written while pymupdf4llm resolved to 1.27.2.3, and every one of -them texturises under 1.28.2. ``texture_signature`` is coarse on purpose — it -does not move with the engine — so without this the emptiness is served forever. +them texturises under 1.28.2. ``texture_signature`` is coarse on purpose (it +does not move with the engine) so without this the emptiness is served forever. """ from __future__ import annotations @@ -181,7 +181,7 @@ def test_extract_keeps_an_empty_entry_from_the_same_engine( def test_a_written_entry_records_what_produced_it( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - """The stamp has to be WRITTEN, not just read — asserted on the entry as it + """The stamp has to be WRITTEN, not just read: asserted on the entry as it lands, since the extraction runs in a subprocess.""" import json @@ -205,7 +205,7 @@ def test_a_written_entry_records_what_produced_it( def test_a_refused_entry_is_not_read_twice(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """Durable reuse globs every signature, including the entry ``get`` just - refused — a second decode of the same blob for no gain.""" + refused: a second decode of the same blob for no gain.""" from fnd.extract import pdf pdf_path = tmp_path / "doc.pdf" diff --git a/tests/test_texture_orphans.py b/tests/test_texture_orphans.py index 8ae969af..19582494 100644 --- a/tests/test_texture_orphans.py +++ b/tests/test_texture_orphans.py @@ -37,7 +37,7 @@ def _ch() -> list[Chunk]: def test_count_and_prune_orphans(tmp_path: Path) -> None: cache = PdfStructureCache(root=tmp_path) cache.put("live1--tex-v2", _ch()) - cache.put("live1--tex-v1", _ch()) # same content, older sig — still live + cache.put("live1--tex-v1", _ch()) # same content, older sig: still live cache.put("dead1--tex-v2", _ch()) cache.put("dead2--tex-v2", _ch()) live = {"live1"} diff --git a/tests/test_toggle_tree.py b/tests/test_toggle_tree.py index 102b1eb5..aa6e6caa 100644 --- a/tests/test_toggle_tree.py +++ b/tests/test_toggle_tree.py @@ -1,4 +1,4 @@ -"""Behavioural tests for the reusable ToggleTree — the three bug classes the +"""Behavioural tests for the reusable ToggleTree: the three bug classes the ad-hoc trees hit must be impossible here.""" from __future__ import annotations diff --git a/tests/test_tui_explain.py b/tests/test_tui_explain.py index 2187b91d..2b70673d 100644 --- a/tests/test_tui_explain.py +++ b/tests/test_tui_explain.py @@ -52,7 +52,7 @@ async def test_explain_overlay_captures_trace_and_toggles(cfg: Config, small_ind async with app.run_test() as pilot: await pilot.pause() await run_search(pilot, app, "mitochondrion") - # Trace captured on the app — regime depends on corpus IDF, but + # Trace captured on the app: regime depends on corpus IDF, but # the trace itself must always be populated when groups exist. assert app._search.latest_trace is not None assert app._search.latest_trace.query == "mitochondrion" diff --git a/tests/test_tui_metadata_filter.py b/tests/test_tui_metadata_filter.py index ea894eda..fdd9ad31 100644 --- a/tests/test_tui_metadata_filter.py +++ b/tests/test_tui_metadata_filter.py @@ -50,7 +50,7 @@ async def test_tui_invalid_filter_does_not_run_search(tui_corpus: Path) -> None: inp.value = "[Project ==] foo" await pilot.press("enter") await pilot.pause() - # Filter has invalid DSL syntax — should clear groups, not crash. + # Filter has invalid DSL syntax: should clear groups, not crash. assert app._search.groups == [] # type: ignore[attr-defined] @@ -63,6 +63,6 @@ async def test_tui_unclosed_bracket_does_not_run_search(tui_corpus: Path) -> Non inp.value = "[Project == 'Website' foo" # unclosed [ await pilot.press("enter") await pilot.pause() - # ValueError from split_metadata_filter — should not run search, + # ValueError from split_metadata_filter: should not run search, # leave groups empty (or as previous, depending on implementation). assert app._search.groups == [] # type: ignore[attr-defined] diff --git a/tests/test_unlocatable_signal_is_visible.py b/tests/test_unlocatable_signal_is_visible.py index eb8db6c9..139d3538 100644 --- a/tests/test_unlocatable_signal_is_visible.py +++ b/tests/test_unlocatable_signal_is_visible.py @@ -7,7 +7,7 @@ truncated it at the border on every row that had one; * the preview's border notice was refreshed only when the ▲/▼ counts changed or the document re-mounted. Stepping between section rows of the same file - does neither — and a chunk with no match doesn't scroll — so the border kept + does neither, and a chunk with no match doesn't scroll, so the border kept showing the previous row's state. These tests pin the reachability, not the computation (which diff --git a/tests/test_upgrade_banner.py b/tests/test_upgrade_banner.py index d214cf6c..52914fb7 100644 --- a/tests/test_upgrade_banner.py +++ b/tests/test_upgrade_banner.py @@ -1,5 +1,5 @@ """The 'outdated documents' count must key off the coarse texture -signature, not the fine-grained per-flag extractor signature — otherwise a +signature, not the fine-grained per-flag extractor signature, otherwise a minor app update flags the whole corpus as outdated. """ @@ -46,7 +46,7 @@ def test_current_engine_entries_not_counted_after_promotion( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """A current-engine entry (cfg matches current) is promoted to the - coarse signature and then NOT counted as outdated — the guarantee the + coarse signature and then NOT counted as outdated: the guarantee the user asked about.""" monkeypatch.setattr("fnd.cache.default_cache_dir", lambda: tmp_path) cache = PdfStructureCache(root=tmp_path) diff --git a/tests/test_ux_a2_footer.py b/tests/test_ux_a2_footer.py index b75b8329..117312f2 100644 --- a/tests/test_ux_a2_footer.py +++ b/tests/test_ux_a2_footer.py @@ -2,7 +2,7 @@ Lazygit shows ~4-6 hints at any moment, prioritised by what the user can usefully do given their current focus. FND's default Textual -``Footer`` dumps every bound action — busy, hard to scan. This test +``Footer`` dumps every bound action: busy, hard to scan. This test suite pins the curated behaviour: result-pane actions appear when the results tree is focused, query actions when the query input is, and a small set of global actions are always visible. @@ -35,7 +35,7 @@ def _footer_text(app: FNDApp) -> str: @pytest.mark.asyncio async def test_footer_shows_at_most_six_hints(built_index: Path) -> None: - """Hard cap on visible hints — beyond this it stops being a glance + """Hard cap on visible hints: beyond this it stops being a glance and becomes a list. lazygit's footer holds 5-6 hints comfortably.""" app = FNDApp(index_dir=built_index) async with app.run_test() as pilot: @@ -50,14 +50,14 @@ async def test_footer_shows_at_most_six_hints(built_index: Path) -> None: @pytest.mark.asyncio async def test_footer_query_context_shows_submit_not_open(built_index: Path) -> None: """When the query input is focused, the most relevant action is - 'submit query' (Enter). 'Open file' is irrelevant — there's no + 'submit query' (Enter). 'Open file' is irrelevant: there's no selected file yet.""" app = FNDApp(index_dir=built_index) async with app.run_test() as pilot: await pilot.pause() # Default focus is the query input. text = _footer_text(app) - # Open / peek / default-app are result actions — they should not + # Open / peek / default-app are result actions: they should not # appear when no result is even selectable. assert "Open" not in text or "Peek" not in text or "Default" not in text, ( f"query-context footer should hide result-only actions: {text!r}" @@ -68,7 +68,7 @@ async def test_footer_query_context_shows_submit_not_open(built_index: Path) -> async def test_footer_results_context_shows_open(built_index: Path) -> None: """When the results tree is focused with a hit selected, the open-related actions (Open / Default / Peek) become the most - relevant — they should appear in the footer.""" + relevant: they should appear in the footer.""" app = FNDApp(index_dir=built_index, initial_query="blue penguin sandwich") async with app.run_test() as pilot: await pilot.pause() diff --git a/tests/test_ux_a_pane_titles.py b/tests/test_ux_a_pane_titles.py index 0f96288b..e4649727 100644 --- a/tests/test_ux_a_pane_titles.py +++ b/tests/test_ux_a_pane_titles.py @@ -36,7 +36,7 @@ async def test_results_pane_title_shows_counts_after_search(built_index: Path) - @pytest.mark.asyncio async def test_results_pane_title_empty_state(built_index: Path) -> None: """Before any query, the title should still render (something like - ``Results``) — not be empty or crash.""" + ``Results``), not be empty or crash.""" app = FNDApp(index_dir=built_index) async with app.run_test() as pilot: await pilot.pause() @@ -62,13 +62,13 @@ async def test_preview_pane_title_shows_file_name_when_focused(built_index: Path preview = app.query_one("#preview_pane", VerticalScroll) title = str(preview.border_title or "") - # The fixture file is "test.pdf" — the basename should appear. + # The fixture file is "test.pdf": the basename should appear. assert ".pdf" in title.lower() or ".md" in title.lower(), title @pytest.mark.asyncio async def test_status_bar_widget_is_removed(built_index: Path) -> None: - """The top status bar is gone — its only content (active scope) is + """The top status bar is gone: its only content (active scope) is shown in the Collections panel border title instead.""" app = FNDApp(index_dir=built_index, initial_query="blue penguin sandwich") async with app.run_test() as pilot: diff --git a/tests/test_ux_b_persistence_and_collapse.py b/tests/test_ux_b_persistence_and_collapse.py index 19240b6c..d6017f67 100644 --- a/tests/test_ux_b_persistence_and_collapse.py +++ b/tests/test_ux_b_persistence_and_collapse.py @@ -1,4 +1,4 @@ -"""UX-B — scope persistence + section collapse-to-header.""" +"""UX-B: scope persistence + section collapse-to-header.""" from __future__ import annotations @@ -104,8 +104,8 @@ async def test_app_restores_persisted_scope_on_launch( async def test_left_at_collapsed_root_collapses_panel( cfg_two: Config, two_index: Path, state_path: Path ) -> None: - """Left arrow on a top-level collapsed node — with no further parent - to walk up to — collapses the whole panel to its header strip + """Left arrow on a top-level collapsed node, with no further parent + to walk up to, collapses the whole panel to its header strip (CSS ``.collapsed`` class).""" app = FNDApp(index_dir=two_index, config=cfg_two) async with app.run_test() as pilot: diff --git a/tests/test_ux_b_smart_collapse.py b/tests/test_ux_b_smart_collapse.py index 18f60aff..78716025 100644 --- a/tests/test_ux_b_smart_collapse.py +++ b/tests/test_ux_b_smart_collapse.py @@ -1,10 +1,10 @@ """UX-B: Left arrow collapses parent from child (lazygit semantics). The standard Textual Tree binding for ``left`` only collapses the -focused node — useless when the cursor is on a leaf, since there's +focused node: useless when the cursor is on a leaf, since there's nothing under the leaf to collapse. Lazygit's UX: ``left`` on a leaf moves the cursor to its parent and collapses *that*. ``left`` on an -already-collapsed branch behaves the same — keep walking up. This +already-collapsed branch behaves the same: keep walking up. This turns ``left`` into a single-key "back out" gesture. """ @@ -43,7 +43,7 @@ async def test_left_on_leaf_collapses_parent(built_index: Path) -> None: assert focused is not None # Sanity: cursor is on a leaf whose parent is the file node. assert focused.parent is first_file - # Press Left — should collapse first_file and move cursor onto it. + # Press Left: should collapse first_file and move cursor onto it. await pilot.press("left") await pilot.pause() assert not first_file.is_expanded, "Left on a leaf should collapse its parent" @@ -102,7 +102,7 @@ async def test_left_on_expanded_branch_just_collapses_it(built_index: Path) -> N await pilot.pause() assert tree.cursor_node is not first_file, "cursor must not land on an expanded file row" # Collapse the parent via Left. With the parent now collapsed, - # the previously-visible child row is gone — cursor should + # the previously-visible child row is gone: cursor should # naturally end up on the parent row (which is now selectable). await pilot.press("left") await pilot.pause() diff --git a/tests/test_ux_c_score_bars.py b/tests/test_ux_c_score_bars.py index 9b57d863..b16ca1d6 100644 --- a/tests/test_ux_c_score_bars.py +++ b/tests/test_ux_c_score_bars.py @@ -29,7 +29,7 @@ def test_score_bar_full_at_max() -> None: def test_score_bar_empty_at_zero_max() -> None: - """Defensive: a max of 0 (no scored results) shouldn't divide by zero — + """Defensive: a max of 0 (no scored results) shouldn't divide by zero, return blank cells preserving column alignment.""" bar = _score_bar(score=0.0, max_score=0.0, width=4) assert bar == " " @@ -44,7 +44,7 @@ def test_score_bar_proportional_in_middle() -> None: @pytest.mark.asyncio async def test_results_tree_labels_omit_score_bars(fixtures_dir: Path, tmp_index_dir: Path) -> None: - """The label format shouldn't carry block-graph glyphs — they read + """The label format shouldn't carry block-graph glyphs: they read as visual artefacts on most terminals and the user vetoed them.""" build_index(roots=[fixtures_dir], index_dir=tmp_index_dir, collection="default") app = FNDApp(index_dir=tmp_index_dir, initial_query="blue penguin sandwich") diff --git a/tests/test_ux_created_filter.py b/tests/test_ux_created_filter.py index 9e62c626..37bba8ee 100644 --- a/tests/test_ux_created_filter.py +++ b/tests/test_ux_created_filter.py @@ -153,7 +153,7 @@ async def test_created_persists_across_restart( @pytest.mark.asyncio async def test_created_enter_toggles_off(cfg_one_collection: Config, mixed_index: Path) -> None: - """Enter on the selected value clears it back to 'any' — no separate 'any' + """Enter on the selected value clears it back to 'any': no separate 'any' row to navigate to, matching every other filter toggle.""" app = FNDApp(index_dir=mixed_index, config=cfg_one_collection) async with app.run_test() as pilot: diff --git a/tests/test_ux_d_collections_panel.py b/tests/test_ux_d_collections_panel.py index e4afb803..7e119d0b 100644 --- a/tests/test_ux_d_collections_panel.py +++ b/tests/test_ux_d_collections_panel.py @@ -88,7 +88,7 @@ async def test_enter_toggles_collection_scope( await pilot.pause() # Whichever collection is first alphabetically should now be active. assert len(app._scope.collections) == 1, app._scope.collections - # Press Enter again — should toggle off. + # Press Enter again: should toggle off. await pilot.press("enter") await pilot.pause() assert app._scope.collections == [] @@ -110,7 +110,7 @@ async def test_active_collection_marked_in_label( await pilot.pause() first = next(iter(tree.root.children)) label = str(first.label) - # Active marker — using ● for active, ○ for inactive. + # Active marker: using ● for active, ○ for inactive. assert "●" in label, f"active collection should be marked: {label!r}" @@ -120,7 +120,7 @@ async def test_panel_header_shows_active_count( ) -> None: """The X/Y active count lives in the tree's ``border_title`` (matches the results pane's styling). Originally this was a separate Static - above the tree — moved into the border title to make both panels + above the tree: moved into the border title to make both panels look identical.""" app = FNDApp(index_dir=two_collection_index, config=cfg_two_collections) async with app.run_test() as pilot: diff --git a/tests/test_ux_f_filters_panel.py b/tests/test_ux_f_filters_panel.py index 04b223c0..4ed4b692 100644 --- a/tests/test_ux_f_filters_panel.py +++ b/tests/test_ux_f_filters_panel.py @@ -1,4 +1,4 @@ -"""UX-F: Filters panel — third left-column panel. +"""UX-F: Filters panel, third left-column panel. File-type filter (multi-select) and Date filter (single-select radio) compose into the user's query as DSL clauses (``kind:(pdf md)``, @@ -37,7 +37,7 @@ async def _click_row( """Click ``node``'s row at ``column``, on the row it actually occupies. Three things must hold before a coordinate click can land, and a miss is - silent — ``Pilot.click`` computes ``widget.region.offset + offset`` and + silent: ``Pilot.click`` computes ``widget.region.offset + offset`` and happily delivers the event to whatever else is painted there, so the only symptom is the assertion that follows timing out much later. @@ -45,7 +45,7 @@ async def _click_row( built its lines, and y=-1 lands a row ABOVE the widget. 2. It must be scrolled into view. ``line`` counts the tree's *virtual* lines while the click offset is *viewport*-relative, so the two diverge - by ``scroll_offset.y`` — expanding a couple of levels is enough to push + by ``scroll_offset.y``: expanding a couple of levels is enough to push a leaf out of a short sidebar pane and send the click to the wrong row. 3. The click must be verified. ``click()`` returns whether the event landed on the target widget; asserting it turns a miss into an immediate, named @@ -100,7 +100,7 @@ async def test_filters_panel_mounts(cfg_one_collection: Config, mixed_index: Pat labels = [str(c.label) for c in tree.root.children] joined = "\n".join(labels) assert "File type" in joined, joined - # "Modified" reads more clearly than "Date" — the underlying + # "Modified" reads more clearly than "Date": the underlying # field is the file's mtime, not creation/published date. assert "Modified" in joined, joined @@ -159,7 +159,7 @@ async def test_single_click_toggles_kind_leaf_once( Regression: ``ResultsTree._on_click`` used to call ``super()._on_click``, but Textual ALSO dispatches the base ``Tree._on_click`` while walking the - MRO — so a single click ran the handler twice, toggling on then off (net + MRO, so a single click ran the handler twice, toggling on then off (net nothing). This drives a real click (MouseDown/Up/Click) through the pump. """ app = FNDApp(index_dir=mixed_index, config=cfg_one_collection) @@ -196,7 +196,7 @@ async def test_single_click_toggles_kind_leaf_once( async def test_single_click_on_header_arrow_expands_once( cfg_one_collection: Config, mixed_index: Path ) -> None: - """Clicking a collapsed section header's expand arrow opens it — once. + """Clicking a collapsed section header's expand arrow opens it: once. Same double-dispatch root cause seen from the *toggle* branch: two ``_toggle_node`` calls per click meant expand+collapse = a dead arrow. @@ -263,7 +263,7 @@ async def test_rapid_kind_toggles_coalesce_to_one_search( Widen the debounce window rather than racing it: the mid-burst assertion used to hold only if three ``pilot.pause()`` round-trips finished inside - 0.12 s of wall-clock, which a loaded runner does not guarantee — the timer + 0.12 s of wall-clock, which a loaded runner does not guarantee; the timer then fired legitimately and the test failed on *correct* behaviour. With the window pinned open the burst cannot leak a search, and the coalesced one is flushed by hand instead of by sleeping. @@ -296,7 +296,7 @@ def counting_run(q: str) -> None: await pilot.pause() assert calls["n"] == 0, "no search should fire mid-burst (debounced)" assert app._scope._filter_search_timer is not None, "burst left no debounce pending" - # Fire the pending debounce directly — deterministic, and it asserts the + # Fire the pending debounce directly: deterministic, and it asserts the # same thing sleeping past the window did: the burst coalesced to one. app._scope._run_filter_search() assert calls["n"] == 1, f"burst should coalesce to one search, got {calls['n']}" @@ -431,7 +431,7 @@ async def test_enter_on_filetype_leaf_keeps_cursor( cfg_one_collection: Config, mixed_index: Path ) -> None: """Bug: toggling a file-type filter with Enter used to rebuild the tree and - drift the cursor a row down. It now repaints in place — cursor stays put.""" + drift the cursor a row down. It now repaints in place: cursor stays put.""" app = FNDApp(index_dir=mixed_index, config=cfg_one_collection) async with app.run_test() as pilot: await pilot.pause() @@ -486,7 +486,7 @@ async def test_filetype_filter_pruned_to_present_kinds( cfg_one_collection: Config, mixed_index: Path ) -> None: """The file-type filter only offers kinds present in scope (md + txt here); - absent kinds/categories (pdf, code, …) are omitted — like the Tags filter.""" + absent kinds/categories (pdf, code, …) are omitted, like the Tags filter.""" app = FNDApp(index_dir=mixed_index, config=cfg_one_collection) async with app.run_test() as pilot: await pilot.pause() diff --git a/tests/test_ux_g_preview_polish.py b/tests/test_ux_g_preview_polish.py index 7ef72489..c52595b1 100644 --- a/tests/test_ux_g_preview_polish.py +++ b/tests/test_ux_g_preview_polish.py @@ -19,7 +19,7 @@ def test_trim_redundant_heading_strips_filename_prefix() -> None: """``Templates > Iterators`` on file ``Templates - Notes.md`` should - drop the leading ``Templates`` segment — it just repeats the filename + drop the leading ``Templates`` segment: it just repeats the filename above. The deeper segment is what locates the match.""" out = _trim_redundant_heading( "Templates > Iterators", title="Templates", path="/x/Templates - Notes.md" @@ -40,7 +40,7 @@ def test_trim_redundant_heading_strips_word_in_long_filename() -> None: def test_trim_redundant_heading_drops_when_only_filename() -> None: """A single-segment heading equal to the filename leaves nothing - useful — caller falls back to a chunk locator.""" + useful: caller falls back to a chunk locator.""" out = _trim_redundant_heading("Templates", title="Templates", path="/x/Templates.md") assert out == "" @@ -152,7 +152,7 @@ async def test_md_match_chunk_renders_via_markdown_widget_with_highlight( """Matched markdown chunks render via Textual's Markdown widget tree (FNDMarkdown), with the matched word carrying a search-highlight span on its block's Content. Replaces the legacy per-line layout - assertion — the structural renderer keeps tables / fenced code / + assertion: the structural renderer keeps tables / fenced code / lists rendering correctly even when a chunk contains a match. """ from fnd.tui.widgets.markdown import FNDMarkdown @@ -170,7 +170,7 @@ async def test_md_match_chunk_renders_via_markdown_widget_with_highlight( pane = app.query_one("#preview_pane", VerticalScroll) def any_highlight() -> bool: - """A block under some FNDMarkdown carries a search-highlight span — + """A block under some FNDMarkdown carries a search-highlight span: the visible match indicator. The widget mounting is not the span landing: Windows rendered one a frame before the other.""" for md in pane.query(FNDMarkdown): diff --git a/tests/test_ux_j_cascade_fallback.py b/tests/test_ux_j_cascade_fallback.py index de10b34c..a9fdeb13 100644 --- a/tests/test_ux_j_cascade_fallback.py +++ b/tests/test_ux_j_cascade_fallback.py @@ -74,7 +74,7 @@ async def test_typo_query_falls_back_to_cascade(cfg: Config, fuzzy_index: Path) @pytest.mark.asyncio async def test_exact_query_uses_fusion_path(cfg: Config, fuzzy_index: Path) -> None: """A clean exact-match query (no typos) should be served by the - fusion path — pass_index == 0 (lex) is the default attribution.""" + fusion path: pass_index == 0 (lex) is the default attribution.""" app = FNDApp(index_dir=fuzzy_index, config=cfg, collection="notes") async with app.run_test() as pilot: await pilot.pause() diff --git a/tests/test_ux_j_fusion_regression.py b/tests/test_ux_j_fusion_regression.py index 64486379..d72c4f2a 100644 --- a/tests/test_ux_j_fusion_regression.py +++ b/tests/test_ux_j_fusion_regression.py @@ -74,9 +74,9 @@ def wide_index(tmp_path: Path, tmp_index_dir: Path) -> Path: @pytest.mark.asyncio async def test_fusion_default_returns_many_files(cfg: Config, wide_index: Path) -> None: - """A corpus with templates in 15 files should produce 15 result rows - — the fusion path must oversample enough chunks for the grouper to - fill ``limit`` files. The pre-fix wiring produced 2-3 files.""" + """A corpus with templates in 15 files should produce 15 result rows: + the fusion path must oversample enough chunks for the grouper to + fill ``limit`` files. The pre-fix wiring produced 2-3 files.""" app = FNDApp(index_dir=wide_index, config=cfg, collection="notes") async with app.run_test() as pilot: await pilot.pause() @@ -88,7 +88,7 @@ async def test_fusion_default_returns_many_files(cfg: Config, wide_index: Path) @pytest.mark.asyncio async def test_fusion_preserves_bm25_score_range(cfg: Config, wide_index: Path) -> None: - """Hit scores must stay in the BM25 range (1+) — RRF fused values + """Hit scores must stay in the BM25 range (1+): RRF fused values of 0.001-0.07 leak the implementation through to the UI.""" app = FNDApp(index_dir=wide_index, config=cfg, collection="notes") async with app.run_test() as pilot: @@ -119,7 +119,7 @@ async def test_fusion_preserves_bm25_score_range(cfg: Config, wide_index: Path) key = (top.parent_id, top.chunk_seq) assert key in bm25, "top hit absent from single-pass results" # The reranker may scale scores slightly, but they should be - # within an order of magnitude — RRF would be off by 10x. + # within an order of magnitude: RRF would be off by 10x. assert top.score >= bm25[key] * 0.5, ( f"top hit score {top.score} diverges from single-pass " f"BM25 {bm25[key]}; fusion shouldn't rewrite scores." diff --git a/tests/test_ux_j_scrollbar_markers.py b/tests/test_ux_j_scrollbar_markers.py index 84697bcc..cc2ef9a6 100644 --- a/tests/test_ux_j_scrollbar_markers.py +++ b/tests/test_ux_j_scrollbar_markers.py @@ -57,7 +57,7 @@ def test_renderer_overlays_marker_glyph_on_match_rows() -> None: def test_renderer_blank_when_no_match_map() -> None: """A renderer with an empty match map should fall through to the - parent's render — no marker glyph appears.""" + parent's render: no marker glyph appears.""" renderer = MatchAwareScrollBarRender( virtual_size=100, window_size=10, @@ -73,7 +73,7 @@ def test_renderer_blank_when_no_match_map() -> None: def test_renderer_line_precise_maps_to_exact_cell() -> None: """Line-precise contract: a 1000-line file with a single match at line - 500, painted on a 10-cell track, places exactly one marker — at + 500, painted on a 10-cell track, places exactly one marker: at cell 5 (mid). The chunk-uniform path can't pin this; the line- precise path must.""" renderer = MatchAwareScrollBarRender( @@ -98,7 +98,7 @@ def test_renderer_line_precise_handles_extremes() -> None: first and last track cells respectively, never out of bounds. Tested at the ``_marker_cells`` layer because the thumb at the top - of the bar occludes cell 0 in ``__rich_console__`` output — that's + of the bar occludes cell 0 in ``__rich_console__`` output: that's a thumb-clipping property of the parent ScrollBarRender, not a line-precise mapping property, so we isolate the latter here. """ @@ -113,15 +113,15 @@ def test_renderer_line_precise_handles_extremes() -> None: total_lines=600, ) assert renderer._marker_cells(size=6) == {0, 5} - # Also check a different track height — markers stay within range. + # Also check a different track height: markers stay within range. cells = renderer._marker_cells(size=20) assert cells == {0, 19} def test_renderer_prefers_line_precise_over_chunk_map() -> None: - """When both fields are populated, the line-precise mapping wins - — match_map is ignored. Lets callers migrate piecemeal without - silently double-counting markers.""" + """When both fields are populated, the line-precise mapping wins: + match_map is ignored. Lets callers migrate piecemeal without + silently double-counting markers.""" renderer = MatchAwareScrollBarRender( virtual_size=600, window_size=10, @@ -149,7 +149,7 @@ def _thumb_glyphs(renderer: ThinScrollBarRender, height: int = 8) -> list[str]: def test_thin_renderer_paints_box_vertical_thumb_not_full_block() -> None: """The thin renderer paints thumb cells as the box-drawing vertical - (the pane border's glyph), as foreground over a transparent track — + (the pane border's glyph), as foreground over a transparent track: never a reverse-video full-cell block.""" renderer = ThinScrollBarRender( virtual_size=100, @@ -175,8 +175,8 @@ def test_thin_renderer_paints_box_vertical_thumb_not_full_block() -> None: def test_thin_thumb_size_is_constant_across_scroll_positions() -> None: - """The thumb is a fixed number of cells regardless of scroll position - — it must not resize as the user scrolls (only ``top`` moves).""" + """The thumb is a fixed number of cells regardless of scroll position: + it must not resize as the user scrolls (only ``top`` moves).""" counts = set() for pos in range(0, 81, 5): # 0 .. max_scroll for virtual 100, window 20 renderer = ThinScrollBarRender( @@ -206,7 +206,7 @@ def _horizontal_glyphs(renderer: ThinScrollBarRender, width: int = 20) -> str: def test_thin_renderer_horizontal_uses_box_glyph_not_stock_block() -> None: """Horizontal bars (wide tables, unwrapped code fences) must read the same - thin weight as the vertical bar — the centralised renderer covers both + thin weight as the vertical bar: the centralised renderer covers both orientations rather than falling through to stock partial-block glyphs.""" renderer = ThinScrollBarRender( virtual_size=200, diff --git a/tests/test_ux_tags_filter.py b/tests/test_ux_tags_filter.py index a2d10d1a..3ecfa41e 100644 --- a/tests/test_ux_tags_filter.py +++ b/tests/test_ux_tags_filter.py @@ -252,7 +252,7 @@ async def test_expanded_section_header_stays_under_cursor(cfg: Config, tagged_in """An expanded section header (File type) is reachable, not skipped. It was once treated as a dead row, but Enter/click on a section header now - collapses the section — so the cursor must be able to land on it. Skipping + collapses the section, so the cursor must be able to land on it. Skipping it also caused a *click* on an expanded header to drift the cursor down onto (and toggle) its first child instead of collapsing the header. """ @@ -306,7 +306,7 @@ async def test_toggling_a_tag_keeps_cursor_and_focus(cfg: Config, tagged_index: """Toggling must not throw the user into the results pane. The pane rebuilds on every toggle (counts change), and the re-run search - used to grab focus — between them the cursor left the tag being pressed. + used to grab focus: between them the cursor left the tag being pressed. """ app = FNDApp(index_dir=tagged_index, config=cfg) async with app.run_test() as pilot: diff --git a/tests/test_vocabulary.py b/tests/test_vocabulary.py index b5803f6c..18b2af9f 100644 --- a/tests/test_vocabulary.py +++ b/tests/test_vocabulary.py @@ -130,8 +130,8 @@ def test_separator_only_scope_is_an_error_not_a_silent_widening(blank: str) -> N # ── the no-collections-configured branch ────────────────────────────────── # -# Names can't be checked against an empty config — an ad-hoc -# `fnd index --collection` name would look exactly like a typo — but the value +# Names can't be checked against an empty config: an ad-hoc +# `fnd index --collection` name would look exactly like a typo, but the value # still has to parse and still has to name something. @@ -146,7 +146,7 @@ def test_ad_hoc_name_passes_through_when_nothing_is_configured() -> None: def test_comma_list_still_splits_when_nothing_is_configured() -> None: - """Unvalidated is not unparsed — one comma-joined term matches nothing.""" + """Unvalidated is not unparsed: one comma-joined term matches nothing.""" issues = FilterIssues() assert resolve_collection_option("one,two", _empty(), issues) == ["one", "two"] assert not issues @@ -167,7 +167,7 @@ def test_all_still_means_everything_with_no_config() -> None: def test_every_exact_field_has_a_vocabulary() -> None: """``_check_query_filters`` maps EXACT query fields to vocabularies by name. - A new EXACT field in the registry with no vocabulary would go unvalidated — + A new EXACT field in the registry with no vocabulary would go unvalidated: fail here rather than let that happen quietly.""" from fnd.query_fields import REGISTRY, FieldValue diff --git a/tests/test_walk_index_dir.py b/tests/test_walk_index_dir.py index 0754c5f4..3f3e5e49 100644 --- a/tests/test_walk_index_dir.py +++ b/tests/test_walk_index_dir.py @@ -34,7 +34,7 @@ def test_walk_indexes_ordinary_json(tmp_path: Path) -> None: def test_walk_skips_index_dir_used_as_the_root(tmp_path: Path) -> None: - """An index dir passed directly as a scan root yields nothing — the + """An index dir passed directly as a scan root yields nothing: the per-child guard doesn't cover the root itself.""" index = tmp_path / "index" index.mkdir() diff --git a/tests/test_walk_per_source.py b/tests/test_walk_per_source.py index 76a0d7f9..f28510e6 100644 --- a/tests/test_walk_per_source.py +++ b/tests/test_walk_per_source.py @@ -52,7 +52,7 @@ def test_frontmatter_filter_excludes_non_matching_md(tmp_path: Path) -> None: def test_frontmatter_filter_only_applies_to_md(tmp_path: Path) -> None: - """A filter on a source that contains pdf files leaves the pdfs alone — + """A filter on a source that contains pdf files leaves the pdfs alone: the filter is md-only by design (no other format has YAML frontmatter).""" root = tmp_path / "mixed" _touch(root / "a.md", "---\nProject: Other\n---\nbody\n") @@ -87,7 +87,7 @@ def test_excludes_still_apply_under_filter(tmp_path: Path) -> None: def test_invalid_frontmatter_excludes_file(tmp_path: Path) -> None: """Per spec: frontmatter parse errors fail closed (filter returns False) - so a typo in one note doesn't kill the index — but it's also not + so a typo in one note doesn't kill the index, but it's also not silently included.""" root = tmp_path / "notes" _touch(root / "bad.md", "---\nfoo:\n nested: not allowed\n---\nbody\n") diff --git a/tests/test_warmth_in_app.py b/tests/test_warmth_in_app.py index cf023934..295890ea 100644 --- a/tests/test_warmth_in_app.py +++ b/tests/test_warmth_in_app.py @@ -1,7 +1,7 @@ """Warmth end to end: coverage's store, the poll, and the arrows. The unit tests pin the classification and the glyph. What they cannot see -is the wiring — that the presenter can resolve a width and a query +is the wiring: that the presenter can resolve a width and a query signature to ask the store anything at all, that the poll reaches the tree, and that the progress line picks its plan from the same fact the arrow paints. All of that only exists in a running app. @@ -48,7 +48,7 @@ def warm_index(tmp_path: Path, tmp_index_dir: Path) -> Path: def hold_everything(app: FNDApp) -> None: """Report every chunk as held, without building real captures. - Patches ``has``, which is what warmth probes with — ``get`` promotes on + Patches ``has``, which is what warmth probes with: ``get`` promotes on read and warmth must never do that. ``count`` too, or the double contradicts itself: whole-file readiness is counted rather than probed, so a file could read fully warmed while this claims to hold nothing. @@ -65,7 +65,7 @@ def hold_nothing(app: FNDApp) -> None: @pytest.mark.asyncio async def test_readiness_is_answerable_in_a_live_app(warm_index: Path) -> None: """Needs a laid-out pane (for the capture width) and a committed query - (for the signature). Before either, the map is empty rather than wrong — + (for the signature). Before either, the map is empty rather than wrong: reporting the whole list cold would be a lie, not a default.""" app = FNDApp(index_dir=warm_index) async with app.run_test() as pilot: @@ -89,7 +89,7 @@ async def test_readiness_is_answerable_in_a_live_app(warm_index: Path) -> None: @pytest.mark.asyncio async def test_the_file_being_captured_reads_as_warming(warm_index: Path) -> None: """Captures run serially on one off-screen host, so exactly one file is - ever in this state — which is what makes it a single marker walking + ever in this state, which is what makes it a single marker walking outward from the cursor rather than noise across the list.""" app = FNDApp(index_dir=warm_index) async with app.run_test() as pilot: @@ -119,7 +119,7 @@ async def test_the_poll_repaints_the_arrows(warm_index: Path) -> None: app._results.refresh_warmth() # Not "everything is COLD": coverage is running underneath, so the file # it is capturing right now legitimately reads WARMING. Pinning that is - # a timing-dependent test — it passed on macOS and Linux and failed on + # a timing-dependent test: it passed on macOS and Linux and failed on # Windows. What this step means is that nothing is READY yet. assert not any(st.is_served for st in tree.warm_states.values()) @@ -130,8 +130,8 @@ async def test_the_poll_repaints_the_arrows(warm_index: Path) -> None: @pytest.mark.asyncio async def test_the_timer_drives_the_poll_without_being_asked(warm_index: Path) -> None: - """The wiring test. Warmth changes with no user input at all — a capture - landing, or coverage stepping to the next file — so nothing in the app's + """The wiring test. Warmth changes with no user input at all (a capture + landing, or coverage stepping to the next file) so nothing in the app's normal event flow would repaint it.""" app = FNDApp(index_dir=warm_index) async with app.run_test() as pilot: @@ -154,7 +154,7 @@ async def test_the_timer_drives_the_poll_without_being_asked(warm_index: Path) - async def test_a_ready_file_is_priced_as_a_warm_navigation(warm_index: Path) -> None: """The progress line and the arrow must read the same fact. A file whose hits are all captured mounts by blitting them, so pricing it with a cold - plan would overstate every jump into it — and the chunk cache, which the + plan would overstate every jump into it, and the chunk cache, which the classification used to ask, cannot see captures at all.""" app = FNDApp(index_dir=warm_index) async with app.run_test() as pilot: @@ -181,7 +181,7 @@ async def test_a_ready_file_is_priced_as_a_warm_navigation(warm_index: Path) -> async def test_a_new_query_does_not_inherit_the_old_arrows(warm_index: Path) -> None: """A new query clears the capture store. Warmth is keyed by parent_id, so a file listed by both searches would otherwise keep its READY arrow until - the next poll — promising an instant jump whose captures had just been + the next poll: promising an instant jump whose captures had just been thrown away.""" app = FNDApp(index_dir=warm_index) async with app.run_test() as pilot: @@ -203,11 +203,11 @@ async def test_a_new_query_does_not_inherit_the_old_arrows(warm_index: Path) -> # On the GLYPH, not just the map. Clearing the map instead of seeding # it COLD leaves every row unknown, and an unknown row falls through to - # Textual's stock arrow — byte-identical to the ready one. The map + # Textual's stock arrow: byte-identical to the ready one. The map # assertion above passes for that case, so it cannot catch a row that # fails towards READY, which is the defect the seeding exists to stop. rows = [n for n in tree.root.children if isinstance(n.data, dict)] - assert rows, "setup — the new query produced no rows" + assert rows, "setup: the new query produced no rows" hollow = {ResultsTree.ICON_BUILDING, ResultsTree.ICON_BUILDING_EXPANDED} icons = {tree.render_label(n, Style(), Style()).plain[:2] for n in rows} assert icons <= hollow, ( @@ -217,12 +217,12 @@ async def test_a_new_query_does_not_inherit_the_old_arrows(warm_index: Path) -> def test_probing_warmth_does_not_reorder_the_capture_store() -> None: """The store promotes on READ so the file being read is never the oldest - entry — a cache that drops what you are looking at is worse than no cache. + entry: a cache that drops what you are looking at is worse than no cache. Warmth probes every listed file twice a second. Asking through ``get`` re-promoted all of them in results-list order on every tick, which - neutralised that protection and left the top result — usually the file on - screen — as the first eviction victim. Probing is not use. + neutralised that protection and left the top result, usually the file on + screen, as the first eviction victim. Probing is not use. """ from fnd.tui.preview.frozen_store import ChunkCaptureStore @@ -284,14 +284,14 @@ async def test_every_state_is_actually_coloured(warm_index: Path) -> None: assert seen[WarmState.COLD] != seen[WarmState.READY], "cold and ready look the same" assert seen[WarmState.WARMING] == seen[WarmState.READY], ( - "warming and ready are both warm — they differ by glyph, not hue" + "warming and ready are both warm: they differ by glyph, not hue" ) @pytest.mark.asyncio async def test_the_arrow_colours_follow_the_theme(warm_index: Path) -> None: """Read from component classes rather than baked-in hex, so a theme change - moves them. Cold is the score column's blue; warm is the theme accent — + moves them. Cold is the score column's blue; warm is the theme accent: the same variable the progress line's fill uses.""" app = FNDApp(index_dir=warm_index) async with app.run_test() as pilot: @@ -321,7 +321,7 @@ async def test_warming_survives_the_gap_between_captures(warm_index: Path) -> No Sampled CONCURRENTLY, because the gap is what matters and it is invisible from inside a capture: under the old code the flag was set on entry to every capture and cleared the moment each one returned. Only an observer - running between them — which is exactly what the 2 Hz poll is — can tell + running between them, which is exactly what the 2 Hz poll is, can tell the two arrangements apart. """ app = FNDApp(index_dir=warm_index) @@ -331,7 +331,7 @@ async def test_warming_survives_the_gap_between_captures(warm_index: Path) -> No preview = app._preview parent_id = app._search.groups[0].parent_id chunks = await preview._coverage_chunks(parent_id) - assert len(chunks) >= 2, "setup — need at least two chunks to capture" + assert len(chunks) >= 2, "setup: need at least two chunks to capture" async def slow_capture(*_a: object, **_k: object) -> None: # Long enough that the inter-capture backoff yields a real window. @@ -363,9 +363,9 @@ async def sampler() -> None: stop.set() await watcher - assert observed, "setup — the sampler never ran" + assert observed, "setup: the sampler never ran" assert None not in observed, ( - "the warming marker was dropped between captures — the arrows would " + "the warming marker was dropped between captures: the arrows would " f"flicker back to cold mid-file: {observed}" ) assert preview.coverage_parent is None, "the marker outlived the file" @@ -374,7 +374,7 @@ async def sampler() -> None: @pytest.mark.asyncio async def test_the_warm_host_captures_one_at_a_time(warm_index: Path) -> None: """The off-screen screen and its container are shared, and the width is set - on the SCREEN — so two callers interleaving at any of capture()'s awaits + on the SCREEN, so two callers interleaving at any of capture()'s awaits would lay each other's widget out at the wrong width. Coverage and the stale-strip repair are independent asyncio tasks and @@ -413,7 +413,7 @@ async def test_a_fully_warmed_file_is_priced_as_a_warm_navigation(warm_index: Pa """FULL is strictly warmer than READY, and the two readers must not drift. Selecting the warm plan by comparing against READY alone prices the most - warmed file in the app as a cold navigation — every phase of the jump + warmed file in the app as a cold navigation: every phase of the jump overstated for the one file that has nothing left to build. """ app = FNDApp(index_dir=warm_index) diff --git a/tests/test_warmth_state.py b/tests/test_warmth_state.py index 9f6f58de..958966c7 100644 --- a/tests/test_warmth_state.py +++ b/tests/test_warmth_state.py @@ -1,7 +1,7 @@ """How ready a file is, and what the arrow says about it. -Navigation cost is bimodal — a jump whose hits are captured is a blit, one -that has to build can be seconds — so the arrow is the only thing on screen +Navigation cost is bimodal: a jump whose hits are captured is a blit, one +that has to build can be seconds, so the arrow is the only thing on screen that says which is coming. These tests pin the classification and the glyph/colour it maps to. """ @@ -38,7 +38,7 @@ def test_a_file_being_captured_reads_as_warming() -> None: def test_warming_only_applies_while_something_is_missing() -> None: - """Once every hit is held the file is READY even mid-pass — coverage may + """Once every hit is held the file is READY even mid-pass: coverage may still be filling the gaps between matches, and that changes nothing the user can feel.""" assert state([3, 9], {3, 9}, warming=True) is WarmState.READY @@ -53,7 +53,7 @@ def test_a_file_with_no_listed_hits_is_ready() -> None: def test_whole_file_coverage_is_not_required() -> None: """The margin around a hit is captured too, but a jump lands on the hit - and lazy mount fills the rest — counting whole-file coverage here would + and lazy mount fills the rest: counting whole-file coverage here would leave a file reading cold through work the user cannot see.""" assert state([5], {5}) is WarmState.READY @@ -83,7 +83,7 @@ def icon_of(tree: ResultsTree, node: Any) -> str: def test_a_ready_file_looks_exactly_like_an_ordinary_row(tree: ResultsTree) -> None: - """Ready is the unremarkable state — it deliberately renders the stock + """Ready is the unremarkable state: it deliberately renders the stock arrow, so the indicator only ever draws the eye when a jump is going to cost something. Asserting the glyph alone would pass with the feature removed, so the contrast against an unready row is the real check. @@ -98,7 +98,7 @@ def test_a_ready_file_looks_exactly_like_an_ordinary_row(tree: ResultsTree) -> N def test_an_unready_file_gets_the_hollow_arrow(tree: ResultsTree) -> None: - """Shape, not just colour, carries the fact that changes a decision — at + """Shape, not just colour, carries the fact that changes a decision: at one cell a hue change alone is hard to read, and it has to survive a colourblind user or a low-contrast theme.""" node = file_node(tree, "a") @@ -175,8 +175,8 @@ def test_an_unchanged_map_does_no_work_at_all(tree: ResultsTree) -> None: def test_a_styled_label_does_not_displace_the_icon_span(tree: ResultsTree) -> None: """The icon's style is inherited from the stock render, read off the first - span. Real file labels are styled Text — the score column is coloured and - the whole row is dimmed — so if a label's own spans could land first, the + span. Real file labels are styled Text (the score column is coloured and + the whole row is dimmed) so if a label's own spans could land first, the icon would inherit the wrong style and lose its toggle meta with it. """ from rich.text import Text @@ -210,7 +210,7 @@ def test_a_file_coverage_cannot_capture_is_not_left_cold_forever() -> None: """A hit with no off-screen builder can never satisfy "all hits captured". Plain kinds take the flat path, and so does a markdown chunk over - ``MARKDOWN_MAX_CHARS`` — measured live, a 48,414-character seminar + ``MARKDOWN_MAX_CHARS``: measured live, a 48,414-character seminar transcript against the 40,000 cap. Coverage skips those chunks, so the file's row sat cold for the whole session while the user waited for warming that could not happen, and every pass re-planned it. @@ -236,7 +236,7 @@ def test_one_unservable_hit_does_not_condemn_an_otherwise_warm_file() -> None: Measured live: a 37-hit PDF sat cold indefinitely with THIRTY-SIX of its hits captured, because chunk 687 was 46,266 characters against the 40,000 renderer cap. Every jump but one was already instant. A per-file flag would - not rescue this — the file is mostly capturable, so it would never be + not rescue this: the file is mostly capturable, so it would never be flagged, and the readiness test would keep waiting on the one chunk that can never come. """ @@ -289,7 +289,7 @@ def test_a_file_with_every_chunk_captured_reads_as_full() -> None: def test_full_outranks_ready() -> None: """Every hit is covered by every chunk being covered, so the stronger - claim wins — otherwise the arrow would never leave the hits-only colour.""" + claim wins, otherwise the arrow would never leave the hits-only colour.""" assert ( warm_state( hit_seqs=[3], diff --git a/tests/workflows/conftest.py b/tests/workflows/conftest.py index 9488c3ff..ec5f1553 100644 --- a/tests/workflows/conftest.py +++ b/tests/workflows/conftest.py @@ -47,7 +47,7 @@ def cfg_three(mini_corpus: Path, tmp_path: Path) -> Config: @pytest.fixture def cfg_one(mini_corpus: Path) -> Config: - """One collection — minimal for single-collection workflows.""" + """One collection: minimal for single-collection workflows.""" return Config( defaults=Defaults(), collections={"default": CollectionConfig(sources=[SourceConfig(path=mini_corpus)])}, diff --git a/tests/workflows/test_cache_actions.py b/tests/workflows/test_cache_actions.py index 4464169f..6d0b0f36 100644 --- a/tests/workflows/test_cache_actions.py +++ b/tests/workflows/test_cache_actions.py @@ -1,4 +1,4 @@ -"""Cache maintenance workflows — end-to-end. +"""Cache maintenance workflows: end-to-end. Covers: - Update cache (currently a stub; will surface a notify) diff --git a/tests/workflows/test_delete_collection.py b/tests/workflows/test_delete_collection.py index 2c0c0166..632f5601 100644 --- a/tests/workflows/test_delete_collection.py +++ b/tests/workflows/test_delete_collection.py @@ -1,4 +1,4 @@ -"""Delete collection workflow — end-to-end.""" +"""Delete collection workflow: end-to-end.""" from __future__ import annotations @@ -83,7 +83,7 @@ async def test_delete_drops_index_off_main_thread( cfg_path = tmp_path / "config.toml" monkeypatch.setattr("fnd.config.default_config_path", lambda: cfg_path) - # Seed the on-disk config the delete path edits + reloads — otherwise + # Seed the on-disk config the delete path edits + reloads, otherwise # delete_collection no-ops on an absent collection and the reload assertion # passes vacuously. for cname, ccfg in cfg_three.collections.items(): diff --git a/tests/workflows/test_first_reindex_warning.py b/tests/workflows/test_first_reindex_warning.py index 318a3bdd..ad3a3a60 100644 --- a/tests/workflows/test_first_reindex_warning.py +++ b/tests/workflows/test_first_reindex_warning.py @@ -1,4 +1,4 @@ -"""First-reindex warning workflow — end-to-end. +"""First-reindex warning workflow: end-to-end. Verifies the modal: - Only shows when pdf-structure is installed and the marker is @@ -65,7 +65,7 @@ def test_mark_seen_then_has_been_seen(tmp_path: Path) -> None: @pytest.mark.asyncio async def test_start_option_marks_seen(tmp_path: Path) -> None: """Selecting 'Start' on the warning marks it seen and dismisses - with True. Doesn't need a full app — push the screen directly.""" + with True. Doesn't need a full app: push the screen directly.""" from textual.app import App from textual.widgets import OptionList diff --git a/tests/workflows/test_install_pdf_structure.py b/tests/workflows/test_install_pdf_structure.py index e41409f0..513e6a7b 100644 --- a/tests/workflows/test_install_pdf_structure.py +++ b/tests/workflows/test_install_pdf_structure.py @@ -1,4 +1,4 @@ -"""Install pdf-structure workflow — end-to-end-ish. +"""Install pdf-structure workflow: end-to-end-ish. We don't actually run uv pip install in tests (it would hit the network and take minutes). Instead we exercise the parts the user @@ -78,7 +78,7 @@ async def test_confirm_screen_mounts( async def test_progress_modal_handles_failed_event( app_factory: Callable[[Config], FNDApp], cfg_one: Config ) -> None: - """The user reported 'Install failed. exit 1' — the modal must + """The user reported 'Install failed. exit 1': the modal must not crash on a failed event and must offer Close to dismiss.""" from textual.widgets import OptionList diff --git a/tests/workflows/test_per_collection_update.py b/tests/workflows/test_per_collection_update.py index 8ddf6aa7..9c090c8c 100644 --- a/tests/workflows/test_per_collection_update.py +++ b/tests/workflows/test_per_collection_update.py @@ -1,4 +1,4 @@ -"""Per-collection Update index workflow — end-to-end. +"""Per-collection Update index workflow: end-to-end. Triggers Update index for a single collection (no chain). The chain bookkeeping fields on the app must remain at single-run @@ -30,7 +30,7 @@ async def test_single_collection_update_does_not_show_chain_suffix( async with app.run_test(size=(120, 40)) as pilot: await pilot.pause() # Skip the first-reindex warning if pdf-structure is installed - # in this dev env — that's a separate workflow. + # in this dev env: that's a separate workflow. from fnd.tui.first_reindex_warning import mark_seen as _mark_seen _mark_seen() diff --git a/tests/workflows/test_toggles.py b/tests/workflows/test_toggles.py index 7f5b35e7..9c7129d6 100644 --- a/tests/workflows/test_toggles.py +++ b/tests/workflows/test_toggles.py @@ -1,4 +1,4 @@ -"""Toggle workflows — Auto-resume + Update cache at index time. +"""Toggle workflows: Auto-resume + Update cache at index time. Each toggle should: - Read from defaults on mount. diff --git a/tests/workflows/test_uninstall_pdf_structure.py b/tests/workflows/test_uninstall_pdf_structure.py index e3b73e4f..fa779711 100644 --- a/tests/workflows/test_uninstall_pdf_structure.py +++ b/tests/workflows/test_uninstall_pdf_structure.py @@ -1,4 +1,4 @@ -"""Uninstall pdf-structure workflow — end-to-end-ish. +"""Uninstall pdf-structure workflow: end-to-end-ish. Verifies: 1. Uninstall commands target fnd's actual venv (not cwd). diff --git a/tests/workflows/test_update_all.py b/tests/workflows/test_update_all.py index 5745634f..3fee6454 100644 --- a/tests/workflows/test_update_all.py +++ b/tests/workflows/test_update_all.py @@ -1,4 +1,4 @@ -"""Update all collections — end-to-end. +"""Update all collections: end-to-end. The workflow: 1. Settings menu drilled into Indexing or Collections. diff --git a/tests/workflows/test_update_all_index_docs.py b/tests/workflows/test_update_all_index_docs.py index fd11736b..0c85b492 100644 --- a/tests/workflows/test_update_all_index_docs.py +++ b/tests/workflows/test_update_all_index_docs.py @@ -1,4 +1,4 @@ -"""Update all collections — verify docs land in the Tantivy index. +"""Update all collections: verify docs land in the Tantivy index. Catches the bug where the chain visibly walks every collection (and the modal renders "Done.") but the LAST collection's docs are missing from @@ -45,8 +45,8 @@ def _per_collection_doc_counts(index_dir: Path, names: list[str]) -> dict[str, i @pytest.mark.asyncio async def test_chain_commits_final_collection_docs(tmp_path: Path) -> None: - """Four-collection chain must leave docs for every collection — - including the final one — in the index after the chain completes.""" + """Four-collection chain must leave docs for every collection, + including the final one: in the index after the chain completes.""" names = ["alpha", "beta", "gamma", "delta"] # Distinct corpora per collection so parent_id-based delete-then- # re-add does not let each chain step wipe the prior step's docs