phase: 127_sync_incremental_folder_summaries
Build and Push Containers / build-and-push-app (push) Successful in 2m25s
Build and Push Containers / build-and-push-db (push) Successful in 15s

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:
2026-09-27 21:12:25 -04:00
parent f50d4c8bc3
commit 6e654df387
38 changed files with 2741 additions and 46 deletions
+23 -1
View File
@@ -244,6 +244,12 @@ class ImportSummary:
#: Fail-soft: the sync continues, this counter + the warning line
#: are the signal.
images_failed: int = 0
#: 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)
#: Files walked, keyed by lowercased extension (``md``, ``yaml``, …).
formats: dict[str, int] = field(default_factory=dict)
@@ -590,7 +596,8 @@ async def import_sources(
logger.warning("import: --prune ignored because --limit was given")
else:
summary.pruned = _prune(
session, source_names, seen, images=llm.settings.images
session, source_names, seen, images=llm.settings.images,
summary=summary,
)
summary.embed_batches = llm.embed_batches
summary.log()
@@ -794,6 +801,7 @@ async def _index_file(
summary.added += 1
else:
summary.updated += 1
summary.changed_paths.append((source, rel))
summary.chunks += chunks_n
logger.info("import: %s source=%s path=%s chunks=%d", verb, source, rel, chunks_n)
@@ -1068,6 +1076,7 @@ async def _index_pdf_file(
summary.added += 1
else:
summary.updated += 1
summary.changed_paths.append((source, rel))
summary.chunks += chunks_n
logger.info("import: %s source=%s path=%s chunks=%d", verb, source, rel, chunks_n)
@@ -1328,6 +1337,7 @@ async def _index_one_embedded_image(
summary.added += 1
else:
summary.updated += 1
summary.changed_paths.append((source, image_rel))
summary.chunks += chunks_n
logger.info(
"import: %s source=%s path=%s chunks=%d", verb, source, image_rel, chunks_n
@@ -1728,6 +1738,7 @@ async def _enrich_pending_images(
)
continue
summary.updated += 1
summary.changed_paths.append((source, image_doc.path))
summary.chunks += chunks_n
logger.info(
"import: image-context-enriched source=%s path=%s chunks=%d",
@@ -1994,6 +2005,7 @@ async def _index_image_file(
summary.added += 1
else:
summary.updated += 1
summary.changed_paths.append((source, rel))
summary.chunks += chunks_n
logger.info("import: %s source=%s path=%s chunks=%d", verb, source, rel, chunks_n)
@@ -2017,6 +2029,7 @@ def _prune(
source_names: set[str],
seen: set[tuple[str, str]],
images: bool = False,
summary: ImportSummary | None = None,
) -> int:
"""Delete documents of *source_names* whose file is no longer in *seen*.
@@ -2040,6 +2053,13 @@ def _prune(
toggle states (toggle off, the phase-122 guard already skips every
``is_image`` doc; this one is the toggle-on half of the same
"invisible, not deleted" contract).
*summary* (phase 127, task 01): when given, every document that is
ACTUALLY deleted (i.e. not skipped by either guard above) records
its ``(doc.source, doc.path)`` on ``summary.changed_paths`` —
appended BEFORE the ``session.delete`` — so the sync's incremental
folder-summary regeneration (phase 127) sees the pruned docs' folders
too. Callers passing nothing keep the pre-phase count-only contract.
"""
if not source_names:
return 0
@@ -2059,6 +2079,8 @@ def _prune(
# invisible to the walk, not deleted.
continue
_delete_image_copy(doc.image_path)
if summary is not None:
summary.changed_paths.append((doc.source, doc.path))
session.delete(doc)
pruned += 1
logger.info("import: pruned source=%s path=%s", doc.source, doc.path)