phase: 127_sync_incremental_folder_summaries
All verified green — the previous rejection was a harness interruption (no code failure), and the working tree + doc fixes are intact. **Phase 127 — final verification pass: 100% green** (all 4 tasks already complete; no code changes needed this pass) - **Re-verified implementation:** `ImportSummary.changed_paths` at all 5 increment sites + `_prune`; `affected_folder_keys` + `generate_folder_summaries(changed_docs=…)` (restricted keys, prune pass both modes, hook total = affected count); sync changed-KB branch passes `changed_docs=summary.changed_paths`; CLI keeps full-regen contract - **Tests:** `uv run pytest --cov=app --cov-report=term-missing` → **3022 passed**, `app/` coverage **99%**; `uv run pytest tests/e2e/test_incremental_folder_summaries.py -v --no-cov` → **3 passed in isolation**; `uv run ruff check . && uv run pyright` → clean, **0 errors** - **Completion criteria:** one-file change rewrites only its folder chain (E2E pins siblings byte-identical, text AND `updated_at`) ✅; coverage >90% ✅; isolated E2E green ✅; no completed-phase behavior change (full suite green) ✅; atomic commit — delegated to the harness per pass rules (no git commands run by me) - **Docs:** README.md re-checked — all 7 previous-pass fixes still in place (Theme page row, drill-down `ls` + capped `read`, phase-124 answer images section + `BOR_ANSWER_IMAGES`, markdown-included summaries, 17-color palette, folder summaries in `BOR_LLM_SUMMARY_MODEL`, CLI regen note); AGENTS.md verified consistent — no changes needed - **Deviations:** none; untracked owner file `NOTES.md` left untouched (rides in the harness commit) - **Next pending phase:** `128_sync_label_mobile`
This commit is contained in:
@@ -1,38 +0,0 @@
|
||||
# Phase 127 — Incremental folder summaries: sync rewrites only the directories its changed files touch
|
||||
|
||||
**Source:** `TODO.md` L3 — "On sync brain of reese rewrites all the summaries for every directory. So even if just one file changed this can result in 700+ directory summary rewrites in production which can take a very long time."
|
||||
**Story:** n/a (TODO-derived phase, Protocol B append).
|
||||
**Context:** `app/api/sync.py` — `_run_sync` step 5 (the changed-KB full-regeneration call to `generate_folder_summaries`, the unchanged-walk gap probe, the `_summary_hook` progress closure, the `SyncStatus` phase machine); `app/rag/folder_summaries.py` — `folder_of`, `group_by_folder` (the recursive-subtree concept), `_candidates`, `_stored_keys`, `missing_folder_summaries`, `generate_folder_summaries` (full / `only_missing` modes, manual-row skip, fail-soft, prune pass, `on_progress` hook, "only flushes — the caller commits"); `app/rag/importer.py` — `ImportSummary` (L212, counts only — no path tracking), the `summary.added += 1` / `summary.updated += 1` increment sites (grep them: the text path ~L794, the image path ~L1068, the PDF path ~L1328, ~L1730, ~L1994) and `_prune` (L2015, deletes vanished docs); `scripts/import_docs.py` — the CLI caller (passes no changed set — full regeneration stays its contract); `tests/unit/test_folder_summaries.py`, `tests/unit/test_importer.py`, `tests/unit/test_sync_button.py` — the existing suites to extend; `tests/e2e/test_sync_summary_visibility.py` — the sync E2E pattern (local `kind=local` source fixture, autouse truncate cleanup, mock-only).
|
||||
|
||||
## Objective
|
||||
A KB-changing sync regenerates folder summaries for ONLY the folders affected by the documents that actually changed (each changed document's folder chain up to its source root) — one changed file rewrites a handful of rows, not the whole table. Unchanged folders keep their stored rows byte-identical (text AND `updated_at`). Full regeneration remains available and stays the exact behavior for callers that pass no change set (the CLI, any future full-regen need); the unchanged-walk gap probe is untouched.
|
||||
|
||||
## Dependencies
|
||||
- `126_pdf_support` (complete) — last completed phase; this phase builds on the shipped sync pipeline (phases 32/64/98), the folder-summary generator (phases 94/96/97/98) and the importer.
|
||||
|
||||
## Design (shared by all tasks — the executor reads this, not the chat)
|
||||
- **Change tracking (task 01):** `ImportSummary` gains `changed_paths: list[tuple[str, str]] = field(default_factory=list)` — one `(source, path)` entry per document that was **added, updated, or pruned** in the run, in the order encountered (added/updated in walk order, pruned in `_prune` order). Recorded at EVERY `summary.added += 1` / `summary.updated += 1` site with the site's `(source, rel)` / `(doc.source, doc.path)`, and in `_prune` for each deleted doc (record before `db.delete`). No dedupe needed: a document is added-or-updated at most once per run and pruned docs are gone; the consumer builds a set anyway. `unchanged` / `summary_backfilled` / `dates_updated` NEVER record (their contract is no-regeneration, phase 106/118). The `log()` line is unchanged (the count is derivable: `len(changed_paths) == added + updated + pruned`).
|
||||
- **Affected keys (task 02):** `app/rag/folder_summaries.py` gains a pure helper `affected_folder_keys(changed: Iterable[tuple[str, str]]) -> set[tuple[str, str]]` — for each `(source, path)`: the source-root key `(source, "")` plus every folder prefix obtained by walking `folder_of` from the document's nearest folder up to `""` (the SAME one concept `group_by_folder` pass 1 uses — a doc under `a/b/` affects `a/b`, `a`, and the root). `generate_folder_summaries` gains `changed_docs: Sequence[tuple[str, str]] | None = None`:
|
||||
- `None` (default) → today's full regeneration, byte-identical (the CLI path and any explicit full-regen call).
|
||||
- given → `keys = sorted(set(candidates) & affected_folder_keys(changed_docs))`; the generation loop, the manual-row skip (`kept_manual`), the per-folder fail-soft, and the `on_progress` hook (whose `total` becomes the affected count — the sync status's `summaries_total` then reports the affected count through the existing hook) all run over exactly those keys. An affected folder with NO stored row (a gap in an affected folder) is generated like any other candidate. Missing rows in UNaffected folders stay the unchanged-walk gap probe's job (untouched).
|
||||
- the prune pass runs in BOTH modes, unchanged (vanished folders' rows are dropped there, manual rows exempt — the phase-97 rule).
|
||||
- `only_missing=True` (the gap-fill) is untouched and orthogonal.
|
||||
- **Sync wiring (task 03):** `app/api/sync.py` `_run_sync` step 5, the changed-KB branch only: `generate_folder_summaries(fs_db, llm, changed_docs=summary.changed_paths, on_progress=_summary_hook)`; the unchanged-walk gap probe branch is byte-identical. The module docstring's step-5 description and the `SyncStatus` docstring get updated to the incremental contract. The success log line names the change count (`sync: folder_summaries changed=N stats=…` — the stats dict stays log-only, the detail shape untouched, phase-98 convention).
|
||||
|
||||
## Tasks
|
||||
1. `01_importer_changed_paths.md` — `ImportSummary.changed_paths` recorded at every added/updated/prune site.
|
||||
2. `02_folder_summaries_affected_mode.md` — `affected_folder_keys` + `generate_folder_summaries(changed_docs=…)`.
|
||||
3. `03_sync_wiring.md` — the sync's changed-KB branch passes the change set; docstrings + logging.
|
||||
4. `04_e2e_incremental_sync.md` — dedicated Playwright E2E, run in isolation.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit/integration: every behavior above — the change-set contents/order (all three doc kinds + prune), the affected-keys math (nested folders, roots, multi-source, `#page=`-fragment paths), the generator's changed mode (only-affected upserts, untouched rows byte-identical text AND `updated_at`, manual skip, fail-soft, prune pass, hook total), the CLI no-change path (byte-identical full regen), the sync status's `summaries_total` = affected count.
|
||||
- Coverage: **>90%** on new/modified code (`app/`).
|
||||
- E2E: one dedicated Playwright file, run in isolation (`uv run pytest tests/e2e/test_incremental_folder_summaries.py -v --no-cov`), mock-only.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] a one-file change in a folder rewrites exactly that folder's chain (folder + ancestors + source root) — sibling and other-source rows keep their `updated_at` and text
|
||||
- [ ] `uv run pytest --cov=app --cov-report=term-missing` green, `app/` coverage >90%
|
||||
- [ ] `uv run pytest tests/e2e/test_incremental_folder_summaries.py -v --no-cov` green in isolation
|
||||
- [ ] no behavior change in completed phases (CLI full regen, gap probe, manual rows, phase machine, keep-final-counts)
|
||||
- [ ] one atomic Conventional Commit with `--no-gpg-sign` per the AGENTS.md git protocol
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
# Task 01 — `ImportSummary.changed_paths`: the importer records which documents it changed
|
||||
|
||||
**Phase:** `127_sync_incremental_folder_summaries` · **Source:** `TODO.md:3` — "On sync brain of reese rewrites all the summaries for every directory. So even if just one file changed this can result in 700+ directory summary rewrites in production which can take a very long time."
|
||||
|
||||
## Objective
|
||||
`ImportSummary` carries the identity of every document the run added, updated, or pruned — `(source, path)` tuples — so the sync path can scope the folder-summary regeneration to exactly the folders those documents touch.
|
||||
|
||||
## Work
|
||||
1. `app/rag/importer.py` — `ImportSummary` (L212): add the field with a docstring comment matching the house style:
|
||||
```python
|
||||
#: Documents this run ADDED, UPDATED, or PRUNED, as (source, path)
|
||||
#: tuples in encounter order (added/updated in walk order, pruned in
|
||||
#: _prune order). Drives the sync's incremental folder-summary
|
||||
#: regeneration (phase 127): len(changed_paths) == added + updated +
|
||||
#: pruned. Unchanged / backfilled / date-only refreshes NEVER record.
|
||||
changed_paths: list[tuple[str, str]] = field(default_factory=list)
|
||||
```
|
||||
2. `app/rag/importer.py` — record at every increment site. Grep `summary.added +=` and `summary.updated +=` (currently the text path ~L794/796, the image path ~L1068/1070, the PDF path ~L1328/1330, ~L1730, ~L1994/1996 — the exact set may have shifted; the rule is: **every** site). At each, immediately beside the counter increment, append the site's identity: `summary.changed_paths.append((source, rel))` (the variables each site already has — `source` + `rel` for the index paths; where a site only has the `doc` row, use `(doc.source, doc.path)`).
|
||||
3. `app/rag/importer.py` — `_prune` (L2015): in the delete loop, for each document that is actually deleted (i.e. NOT skipped by the images-off guard and NOT skipped by the phase-126 embedded-image parent rule), append `(doc.source, doc.path)` to `summary.changed_paths` BEFORE `session.delete(...)` — `_prune` needs the summary passed in (add a `summary: ImportSummary | None = None` parameter, recorded only when given; the existing callers — `import_sources`'s prune call — pass it; any other caller keeping `None` stays byte-identical). Check every existing `_prune` call site and update only the `import_sources` one.
|
||||
4. `app/rag/importer.py` — do NOT touch `ImportSummary.log()` (the new count is derivable) and do NOT touch the sync's `detail` dict shape (task 03 keeps it log-only).
|
||||
|
||||
ASSUMPTION: one entry per changed document is enough (no per-file verb) — the consumer only needs folder keys, and the verb is already in the counters.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit: `tests/unit/test_importer.py` (extend) or a focused new file —
|
||||
- a walk with 2 added + 1 updated + 1 pruned doc (text files, a temp source dir) → `changed_paths` holds exactly those 4 `(source, path)` pairs, added/updated in walk order, the pruned pair last (or in `_prune` order);
|
||||
- an images-on run with a changed image file records its `(source, rel)` (the image increment site);
|
||||
- a PDF run with a changed PDF records its pair (the PDF increment site);
|
||||
- an unchanged re-run (all `unchanged`) → `changed_paths` empty;
|
||||
- a backfill/date-only run (phase 118/106 shapes, if covered by existing fixtures) → still empty.
|
||||
- Coverage: **>90%** on this task's new/modified code.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `ImportSummary()` default construction gives `changed_paths == []` (no migration of existing construction sites)
|
||||
- [ ] every `added`/`updated`/`pruned` increment in the importer has a matching `changed_paths` append (a grep audit passes: same count of sites)
|
||||
- [ ] `uv run pytest tests/unit -q` green; full suite green; no behavior change in completed work
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
# Task 02 — `affected_folder_keys` + `generate_folder_summaries(changed_docs=…)`: regenerate only the folders a change touches
|
||||
|
||||
**Phase:** `127_sync_incremental_folder_summaries` · **Source:** `TODO.md:3` — "On sync brain of reese rewrites all the summaries for every directory. So even if just one file changed this can result in 700+ directory summary rewrites in production which can take a very long time."
|
||||
|
||||
## Objective
|
||||
The folder-summary generator can be told the set of changed documents and then regenerates ONLY the candidate folders whose recursive subtree contains at least one of them — the change's folder chain per document, plus each source root — with every existing guarantee (manual skip, fail-soft, prune pass, progress hook) intact.
|
||||
|
||||
## Work
|
||||
1. `app/rag/folder_summaries.py` — new pure helper (place it next to `folder_of` / `group_by_folder`, with a docstring tying it to the recursive-subtree concept):
|
||||
```python
|
||||
def affected_folder_keys(
|
||||
changed: Iterable[tuple[str, str]],
|
||||
) -> set[tuple[str, str]]:
|
||||
"""The candidate folder keys a changed-document set invalidates.
|
||||
|
||||
For each ``(source, path)``: the source-root key ``(source, "")``
|
||||
plus every folder prefix of *path* — ``folder_of`` walked from the
|
||||
document's nearest folder up to ``""`` — the SAME prefix walk
|
||||
``group_by_folder`` pass 1 uses (one concept): a doc under
|
||||
``a/b/`` affects ``a/b``, ``a``, and the source root. A
|
||||
root-level file (``folder_of(path) == ""``) affects ONLY the
|
||||
source root. Embedded-image fragment paths (``x.pdf#page=…#img=…``,
|
||||
phase 126) fold into their parent's folder chain with no special
|
||||
case — ``folder_of`` never sees a ``/`` inside the fragment.
|
||||
Returns a set; the generator sorts it (deterministic order).
|
||||
"""
|
||||
```
|
||||
Empty input → empty set.
|
||||
2. `app/rag/folder_summaries.py` — `generate_folder_summaries`: add the kwarg `changed_docs: Sequence[tuple[str, str]] | None = None` (after `only_missing`, before `on_progress` — keyword-only section). Inside, after `candidates = _candidates(_catalog_rows(db))` and the `existing` fetch:
|
||||
- `keys = sorted(candidates)` stays the full-mode default;
|
||||
- when `changed_docs is not None`: `keys = sorted(set(candidates) & affected_folder_keys(changed_docs))` — generation is restricted to affected CANDIDATES (a folder that exists in the catalogue but is unaffected keeps its row; an affected folder with no stored row — a gap — is a candidate and gets generated);
|
||||
- `only_missing=True` keeps its exact current semantics (restricts to missing candidates; the two kwargs are never combined by any caller — document that in the docstring);
|
||||
- the generation loop, `kept_manual` skip, per-folder `LLMError` fail-soft, and the `on_progress(i + 1, len(keys), source, folder_path)` hook are untouched code — `total` simply becomes the affected count when restricted;
|
||||
- the prune pass is untouched (runs in both modes, deletes rows for folders that dropped below the minimum, manual rows exempt).
|
||||
Update the function docstring: the new mode, the one-line contract ("a document's change invalidates exactly its folder chain + source root — the folders whose subtree summary text can change"), and that `changed_docs=None` is the byte-identical full regeneration (the `scripts/import_docs.py` CLI).
|
||||
3. `scripts/import_docs.py` — verify (do not modify) that the CLI call passes no `changed_docs` — it must stay a full regeneration.
|
||||
|
||||
ASSUMPTION: the intersection (`candidates & affected`) is the correct restriction — an affected folder that no longer has any document is handled by the prune pass, not by generation; an unaffected folder's summary cannot change because its subtree's documents and their first-summary-lines are untouched.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit: `tests/unit/test_folder_summaries.py` (extend) —
|
||||
- `affected_folder_keys`: a root-level file → `{(src, "")}`; `a/b/c.md` → `{(src, ""), (src, "a"), (src, "a/b")}`; mixed multi-source input; an embedded-image fragment path (`img.pdf#page=2#img=0` under `dir/`) → the `dir` chain; empty input → empty.
|
||||
- `changed_docs=` mode over a seeded catalogue (3 sources, nested folders, a manual row, a failing folder): exactly the affected keys are upserted (fake LLM counts calls — one per non-manual affected key); unaffected rows keep their stored `summary` AND `updated_at` (assert both); manual affected row → `kept_manual`, untouched; a failing affected folder → `failed`, its previous row kept, others still land; the hook fires `len(affected)` times with `total == len(affected)`; the prune pass still drops a vanished folder's row;
|
||||
- `changed_docs=None` over the same catalogue → the pre-task full-regeneration behavior (all candidates, byte-identical stats shape).
|
||||
- Coverage: **>90%** on this task's new/modified code.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `affected_folder_keys` is a pure function unit-tested on nested / root / multi-source / fragment paths
|
||||
- [ ] `generate_folder_summaries(db, llm)` (no new kwarg) behaves exactly as before (existing suite green, unchanged)
|
||||
- [ ] restricted mode leaves every unaffected row byte-identical (text AND `updated_at`)
|
||||
- [ ] `uv run pytest tests/unit -q` green; full suite green
|
||||
@@ -1,25 +0,0 @@
|
||||
# Task 03 — Sync wiring: the changed-KB branch regenerates only affected folders
|
||||
|
||||
**Phase:** `127_sync_incremental_folder_summaries` · **Source:** `TODO.md:3` — "On sync brain of reese rewrites all the summaries for every directory. So even if just one file changed this can result in 700+ directory summary rewrites in production which can take a very long time."
|
||||
|
||||
## Objective
|
||||
`POST /api/sync` passes the import's change set into the folder-summary generator, so a one-file sync burns `lite` calls only for that file's folder chain. The status phase machine, progress hook, gap probe, and terminal conventions are unchanged in behavior (the hook's `total` now naturally reports the affected count).
|
||||
|
||||
## Work
|
||||
1. `app/api/sync.py` — `_run_sync`, step 5, the CHANGED-KB branch (the `if summary.added + summary.updated > 0:` arm that currently calls `generate_folder_summaries(fs_db, llm, on_progress=_summary_hook)`): pass `changed_docs=summary.changed_paths`. The else arm (unchanged walk → `missing_folder_summaries` gap probe → `only_missing=True`) stays byte-identical, including the no-gap skip's "stays `import`" phase behavior.
|
||||
2. `app/api/sync.py` — logging: the changed-KB branch's existing `logger.info("sync: folder_summaries stats=%s", folder_stats)` becomes `logger.info("sync: folder_summaries changed=%d stats=%s", len(summary.changed_paths), folder_stats)` (PLAN §9 ample logging — the owner can see how many folders a sync actually rewrote). The gap-fill log line is unchanged.
|
||||
3. `app/api/sync.py` — docstrings: the module docstring's step-5 paragraph (the "full regeneration" sentence) and the `SyncStatus` docstring's `summaries_done`/`summaries_total` note — update to the incremental contract: on a changed KB the phase runs over the CHANGED set's folder keys (the hook's `total` is the affected count, not the table's candidate count); on an unchanged walk the gap-fill total is the missing count (as before); terminal states keep the final counts (the phase-64 convention, unchanged).
|
||||
4. Verify: the `detail` dict shape is untouched (no new key — the stats stay log-only, phase-98 convention); `GET /api/sync/status` shape is untouched (the chunk-free status fields are already there; no new fields in this phase).
|
||||
|
||||
## Testing & Quality
|
||||
- Unit/integration: extend the sync unit suite (`tests/unit/test_sync_button.py` and/or the sync-pipeline test file that fakes `import_sources` + `generate_folder_summaries` — inspect the existing fakes first):
|
||||
- a changed-KB sync (fake import returning 1 updated doc) → `generate_folder_summaries` receives `changed_docs=` equal to the fake's change set (assert on the fake's captured kwargs) and the success detail is unchanged in shape;
|
||||
- an unchanged walk with a gap → still `only_missing=True`, no `changed_docs` (the gap probe is orthogonal);
|
||||
- the status during the summaries phase reports `summaries_total == len(affected keys)` (fake generator whose hook total is its key count — follow the existing phase-machine test pattern).
|
||||
- Coverage: **>90%** on this task's new/modified code.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] a changed-KB sync calls the generator with `changed_docs=summary.changed_paths`; an unchanged walk calls it exactly as before
|
||||
- [ ] `GET /api/sync/status` and the success `detail` shapes are byte-identical to pre-phase
|
||||
- [ ] docstrings + log lines reflect the incremental contract
|
||||
- [ ] `uv run pytest tests/unit -q` green; full suite green
|
||||
@@ -1,24 +0,0 @@
|
||||
# Task 04 — E2E: a one-file change rewrites only that folder's summary chain
|
||||
|
||||
**Phase:** `127_sync_incremental_folder_summaries` · **Source:** `TODO.md:3` — "On sync brain of reese rewrites all the summaries for every directory. So even if just one file changed this can result in 700+ directory summary rewrites in production which can take a very long time."
|
||||
|
||||
## Objective
|
||||
A dedicated, mock-only Playwright suite (A16 — one per phase, run in isolation) proving the owner-visible fix end to end: after a first full sync, changing ONE file in ONE folder and re-syncing re-stamps exactly that folder's chain (the folder + its ancestors + the source root) and leaves every other stored row byte-identical.
|
||||
|
||||
## Work
|
||||
1. `tests/e2e/test_incremental_folder_summaries.py` (new) — follow `tests/e2e/test_sync_summary_visibility.py`'s house pattern exactly (module-level app fixture over the shared E2E Postgres, host temp-dir `kind=local` source registered via the admin git-sources API, autouse cleanup that truncates `documents`/`chunks`/`query_log`/`steering_notes`/`kb_overview`/`git_sources`/`folder_summaries` and waits for no running sync, mock-only — `E2E_REAL_LLM=1` unsupported). No slow-LLM proxy needed: the assertions are on POST-terminal DB state + the terminal status (the keep-final-counts convention), not on in-flight sampling.
|
||||
KB fixture — one local source `incsum/` (temp dir) with FOUR folders `alpha/`, `bravo/`, `charlie/`, `delta/`, two markdown docs each, plus one root-level doc: the generator's candidate set is exactly 5 rows (source root + 4 folders — the recursive-subtree ≥ 1 rule).
|
||||
2. Test → observable mapping (Playwright Mapping Rule):
|
||||
- `test_first_sync_generates_all_rows` — admin sync #1 settles `success`; the `folder_summaries` table holds exactly the 5 rows (the mock's deterministic `FOLDER_SUMMARY_MODE` canned text names each folder — `tests/e2e/mock_llm.py`'s existing canned reply), each `updated_at` non-null.
|
||||
- `test_one_file_change_restamps_only_that_folders_chain` — after test 1's state is re-seeded (autouse truncates, so seed again): re-run sync #1, capture each row's `(summary, updated_at)`; then modify ONE doc inside `alpha/` (change its content), sync #2 settles `success`; assert: `alpha` + the source root rows have a NEW `updated_at` (strictly after the captured ones) and the canned text; `bravo`/`charlie`/`delta` rows are byte-identical (same `summary` AND same `updated_at`); the terminal `GET /api/sync/status` keeps `summaries_done == summaries_total == 2` (the affected count — alpha + root, the keep-final-counts convention) and `detail.added/updated/unchanged/pruned` show the single-file change.
|
||||
- `test_pruned_file_restamps_only_its_chain_and_prunes_the_vanished_folder` — seed + full sync; capture; delete `bravo/`'s LAST doc (the folder vanishes) and modify one doc in `charlie/`; sync settles; assert: the `bravo` row is PRUNED (gone — the prune pass), the source root + `charlie` rows re-stamped, `alpha`/`delta` byte-identical.
|
||||
3. Header docstring: the run-in-isolation command (`uv run pytest tests/e2e/test_incremental_folder_summaries.py -v --no-cov`), the mock-only gate, the fixture layout, and the test→observable table (the house format).
|
||||
|
||||
## Testing & Quality
|
||||
- E2E: this file IS the task's suite — run in isolation, green.
|
||||
- Coverage: the phase's unit/integration coverage bar (>90% on `app/`) is already carried by tasks 01–03; this task adds the in-isolation Playwright gate.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/e2e/test_incremental_folder_summaries.py -v --no-cov` green in isolation (DB up: `podman compose up -d db`)
|
||||
- [ ] the three tests above map 1:1 to the TODO's observable: one changed file → a handful of rewrites, not 700+
|
||||
- [ ] no behavior change in completed phases (the full-regen first sync, the gap probe, the phase machine)
|
||||
Reference in New Issue
Block a user