Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions features/029-chapters-regroup/.state
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
feature: 029-chapters-regroup
mode: act
pipeline_state: READY_FOR_COMMIT
branch: feat/029-chapters-regroup
last_updated: 2026-08-06
---
39 changes: 39 additions & 0 deletions features/029-chapters-regroup/CONTRACT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
feature: 029-chapters-regroup
status: validated
validated_at: 2026-08-06
validated_by: orquestrador (Fable 5); premissas da exploração de 2026-08-05 reconferidas contra main pós-#70
---

# CONTRACT — 029-chapters-regroup

## Premissas técnicas verificadas

| # | Premissa | Verificação | Estado |
|---|---|---|---|
| 1 | Os 7 furos de `_*` existem e são os únicos | exploração 2026-08-05: `lint.py:33,53`, `heal.py:85-89`, `archive.py:44-45,72`, `compile.py:592-593`, `stats.py:4-8`; honram: search, embeddings, lexical, graph, api | **confirmado** |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: Premise 1 claims the '7 furos' are the only ones and that the API honors the _* convention, but kb/api/articles.py:39 (the fingerprint rglob) only skips symlinks and does not exclude _*/.* paths, so files under _chapters/ still get scanned/counted there. The actual index is built through graph.build_link_index (which honors _*), so there is no user-visible break — but the premise as recorded is inaccurate and RF-01's 'nenhum arquivo ... listado/contado' semantics isn't fully met at that site. Consider filtering _/. parts in the fingerprint too, or revising the premise/contract text to reflect this residual gap.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At features/029-chapters-regroup/CONTRACT.md, line 14:

<comment>Premise 1 claims the '7 furos' are the only ones and that the API honors the `_*` convention, but kb/api/articles.py:39 (the fingerprint rglob) only skips symlinks and does not exclude `_*`/`.*` paths, so files under `_chapters/` still get scanned/counted there. The actual index is built through graph.build_link_index (which honors `_*`), so there is no user-visible break — but the premise as recorded is inaccurate and RF-01's 'nenhum arquivo ... listado/contado' semantics isn't fully met at that site. Consider filtering `_`/`.` parts in the fingerprint too, or revising the premise/contract text to reflect this residual gap.</comment>

<file context>
@@ -0,0 +1,39 @@
+
+| # | Premissa | Verificação | Estado |
+|---|---|---|---|
+| 1 | Os 7 furos de `_*` existem e são os únicos | exploração 2026-08-05: `lint.py:33,53`, `heal.py:85-89`, `archive.py:44-45,72`, `compile.py:592-593`, `stats.py:4-8`; honram: search, embeddings, lexical, graph, api | **confirmado** |
+| 2 | heal pode tocar `_summaries` hoje | `heal.py:85-89` exclui só `_index.md` e `.heal_backup`; 1.027 summaries elegíveis ao sorteio | **confirmado — bug latente que C1 corrige** |
+| 3 | `manifest.book` existe para agrupar | 028 materializou 849 ligações com `book`; 126 `unresolved` fora | **confirmado** |
</file context>

| 2 | heal pode tocar `_summaries` hoje | `heal.py:85-89` exclui só `_index.md` e `.heal_backup`; 1.027 summaries elegíveis ao sorteio | **confirmado — bug latente que C1 corrige** |
| 3 | `manifest.book` existe para agrupar | 028 materializou 849 ligações com `book`; 126 `unresolved` fora | **confirmado** |
| 4 | Manutenção de manifest em move existe | `update_article_path`/`mark_archived` (028 B1/B4) com testes | **confirmado** |
| 5 | Mover arquivo não exige re-embedar | build por hash/relpath descarta e re-adota; `_chapters/` fica invisível após C1 | **confirmado** |
| 6 | Sem cron do kb | `crontab -l` reverificado 2026-08-05 | **confirmado** |
| 7 | `find_orphans` marcaria os 975 pós-move | 973/975 sem backlink (medição do ADR) — C1 antes de C4 é hard gate | **confirmado** |

## Premissas de produto

- Move para `_chapters/` SÓ com aprovação explícita do dono no C4, ciente de que a wiki visível esvazia (~5 artigos + unresolved) até o compile multi-fonte.
- `unresolved` nunca é movido por inferência — permanece na wiki como pendência humana.
- Commit por livro; tag antes do lote; tudo move, nunca unlink.
- Golden set preservado em disco; perda de retrievability aceita pelo ADR-0018 e registrada.

## Riscos aceitos

| Risco | Mitigação |
|---|---|
| Move em massa com path novo quebra wikilinks qualificados dos unresolved que ficam | Relatório do C4 lista; wikilink por stem continua resolvendo via graph (stem inalterado) |
| Nome de livro sujo (sufixos de fonte) vira slug ruim | Sanitização com teste; relatório mostra o slug antes do apply |
| Bench sem objeto pós-move | Aceito; registrado no REPORT |

## Gate de TDD

2 condições binárias de risco (I/O em store real; output estrutural do plano de move) → `test-design` com `test-red` base; cada módulo adotante do C1 tem teste próprio de exclusão.
27 changes: 27 additions & 0 deletions features/029-chapters-regroup/PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# PLAN — 029-chapters-regroup

**Branch:** `feat/029-chapters-regroup`
**Data:** 2026-08-06
**Spec:** `features/029-chapters-regroup/SPEC.md` · **Domain:** `features/027-noise-retro/DOMAIN.md`

## Contexto técnico

| Campo | Valor |
|---|---|
| Alvo | `kb/fsutil.py` (helper), `kb/lint.py:33,53`, `kb/heal.py:85-104`, `kb/archive.py:44-45,72`, `kb/compile.py:592-593`, `kb/stats.py:4-8`, novo `kb/regroup.py`, `kb/cli.py` |
| Reuso | `graph._e_artigo` (semântica de referência), `archive.move_to_archive`, `state.mark_archived`/`update_article_path`/`load_manifest`, `compile.update_index`, `refresh_embeddings_index`, maquinaria de lote HITL das 027/028 |
| Estratégia de testes | test-design: I/O em store real (move em massa + manifest) e output estrutural (plano de move é contrato do gate humano) |

## Desenho

1. **C1 — `kb/fsutil.iter_articles(wiki_dir)`**: generator com a semântica de `graph._e_artigo` (`_`/`.` em qualquer componente, symlink fora). Adotado em: `lint.find_ambiguous_wikilinks` e `lint_wiki`; `heal._sample_paths`; `archive.find_orphans`/`find_by_age`; `compile.update_index`; `stats`. `graph._e_artigo` passa a delegar ao helper (uma semântica, um lugar).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: C1's description is inaccurate and will mislead future readers documenting the single-source-of-truth goal. heal._sample_paths does not exist (heal.py samples inline via random.sample), and graph._e_artigo does not delegate to iter_articles — it retains its own _/. predicate and lacks the symlink filter iter_articles applies, so the "uma semântica, um lugar" centralization is only partially realized. Suggest correcting the PLAN to reference the real sampling call site and noting the predicate/symlink divergence, or actually delegating _e_artigo to the helper.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At features/029-chapters-regroup/PLAN.md, line 17:

<comment>C1's description is inaccurate and will mislead future readers documenting the single-source-of-truth goal. `heal._sample_paths` does not exist (heal.py samples inline via random.sample), and `graph._e_artigo` does not delegate to `iter_articles` — it retains its own `_`/`.` predicate and lacks the symlink filter `iter_articles` applies, so the "uma semântica, um lugar" centralization is only partially realized. Suggest correcting the PLAN to reference the real sampling call site and noting the predicate/symlink divergence, or actually delegating `_e_artigo` to the helper.</comment>

<file context>
@@ -0,0 +1,27 @@
+
+## Desenho
+
+1. **C1 — `kb/fsutil.iter_articles(wiki_dir)`**: generator com a semântica de `graph._e_artigo` (`_`/`.` em qualquer componente, symlink fora). Adotado em: `lint.find_ambiguous_wikilinks` e `lint_wiki`; `heal._sample_paths`; `archive.find_orphans`/`find_by_age`; `compile.update_index`; `stats`. `graph._e_artigo` passa a delegar ao helper (uma semântica, um lugar).
+2. **C2 — heal sem unlink**: `_remove_stub` usa `move_to_archive` (dest `ARCHIVE_DIR/<rel>`), remove o `_backup` caseiro para stubs (o backup versionado do archive cobre) e chama `mark_archived`. `.heal_backup` legado permanece intocado.
+3. **C3 — `kb/regroup.py`**: `plan_regroup(wiki_dir, manifest)` → grupos `{book: [(artigo, destino)]}` + `unresolved: [artigo]`; destinos `wiki/_chapters/<book-slug>/<nome>` e summary espelho em `wiki/_summaries/_chapters/<book-slug>/`. `apply_book(book)` move via `move_to_archive`-like (mesma contenção, mas destino é `_chapters/` — usar move com `atomic`/`shutil.move` + backup versionado próprio), `update_article_path`, `update_index`, refresh. Book-slug: slugify do nome do livro (nomes vindos de metadata têm " -- Anna's Archive" etc. — sanitizar).
</file context>

2. **C2 — heal sem unlink**: `_remove_stub` usa `move_to_archive` (dest `ARCHIVE_DIR/<rel>`), remove o `_backup` caseiro para stubs (o backup versionado do archive cobre) e chama `mark_archived`. `.heal_backup` legado permanece intocado.
3. **C3 — `kb/regroup.py`**: `plan_regroup(wiki_dir, manifest)` → grupos `{book: [(artigo, destino)]}` + `unresolved: [artigo]`; destinos `wiki/_chapters/<book-slug>/<nome>` e summary espelho em `wiki/_summaries/_chapters/<book-slug>/`. `apply_book(book)` move via `move_to_archive`-like (mesma contenção, mas destino é `_chapters/` — usar move com `atomic`/`shutil.move` + backup versionado próprio), `update_article_path`, `update_index`, refresh. Book-slug: slugify do nome do livro (nomes vindos de metadata têm " -- Anna's Archive" etc. — sanitizar).
4. **C4 — ops**: preflight + tag `pre-regroup-<data>` → `regroup scan` → relatório por livro (contagem, exemplos, unresolved) → aprovação explícita → apply livro a livro (commit por livro) → `kb index build` + smoke + tela.

## Riscos

- `update_index`/embeddings ficam consistentes porque C1 os ensina a ignorar `_chapters/` ANTES de qualquer move (ordem C1 → C3 é hard gate).
- Slug de livro com nomes sujos de metadata → sanitização com teste.
- 126 `unresolved` permanecem na wiki — visíveis no relatório, decisão humana posterior.
- Bench/golden perdem objeto após o move — aceito (ADR), registrado.
41 changes: 41 additions & 0 deletions features/029-chapters-regroup/REPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# REPORT — 029-chapters-regroup

**Estado:** `DONE_WITH_CONCERNS`
**Branch:** `feat/029-chapters-regroup`

## Contexto

Terceira e última feature do esforço de higiene (etapa 3 do ADR-0018). Antes dela: sete pontos do engine ignoravam a convenção `_*` (o heal podia deletar summary hoje), o heal era a última remoção destrutiva (`unlink`), e os 975 artigos de capítulo seguiam misturados na wiki visível.

## Mudanças (engine)

- **C1 — `kb/fsutil.iter_articles`**: semântica única de artigo vivo (`_*`, `.*`, symlink fora), adotada por lint, heal, archive (órfãos e idade), `update_index` e stats. Fecha o bug latente do heal sobre `_summaries` e o cenário em que `find_orphans` arrastaria `_chapters/` inteiro para `archive/`.
- **C2 — heal sem unlink** (V7 mínimo): stub vai para `archive/` com hierarquia e backup versionado; manifest marcado `archived`. A unificação do formato do `.heal_backup` legado fica como dívida registrada.
- **C3 — `kb regroup scan|apply --book`**: plano por proveniência do manifest (nunca cosseno), slug sanitizado por livro, summaries espelhados, `unresolved` jamais movido por inferência, commit por livro via `move_to_archive` com raiz de contenção em `_chapters/`.

## Lote final (C4, gate explícito do dono)

Decisão do dono em 2026-08-06: **mover 37 livros (630 artigos); `transcripts-youtube` (207) e `harness` (14) ficam** — não são livros, e com os 124 `unresolved` a plataforma mantém ~345 artigos visíveis até o compile multi-fonte.

Executado em 2026-08-06: **37 livros, 630 artigos + summaries movidos, 37 commits** (um por livro), zero erros, tag `pre-regroup-2026-08-06`. Vault final: **345 artigos vivos** (207 transcripts + 14 harness + 124 unresolved — soma exata), 630 em `_chapters/`. Smoke: índice em 345/345 artigos (2.666 chunks), `kb search` e `kb stats` respondendo, plataforma com home povoada e artigo vivo abrindo (tela conferida).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: The final count of '124 unresolved' (and implied 851 with provenance) contradicts the feature's own baseline of 126 unresolved stated in SPEC.md, CONTRACT.md premise 3, and PLAN.md. The −2 delta implies 2 articles left the unresolved set during C4 but the report never documents that event, breaking the documentary-traceability standard the project follows. Reconcile the numbers against the baseline or add a note explaining the two resolved articles.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At features/029-chapters-regroup/REPORT.md, line 20:

<comment>The final count of '124 unresolved' (and implied 851 with provenance) contradicts the feature's own baseline of 126 unresolved stated in SPEC.md, CONTRACT.md premise 3, and PLAN.md. The −2 delta implies 2 articles left the unresolved set during C4 but the report never documents that event, breaking the documentary-traceability standard the project follows. Reconcile the numbers against the baseline or add a note explaining the two resolved articles.</comment>

<file context>
@@ -0,0 +1,41 @@
+
+Decisão do dono em 2026-08-06: **mover 37 livros (630 artigos); `transcripts-youtube` (207) e `harness` (14) ficam** — não são livros, e com os 124 `unresolved` a plataforma mantém ~345 artigos visíveis até o compile multi-fonte.
+
+Executado em 2026-08-06: **37 livros, 630 artigos + summaries movidos, 37 commits** (um por livro), zero erros, tag `pre-regroup-2026-08-06`. Vault final: **345 artigos vivos** (207 transcripts + 14 harness + 124 unresolved — soma exata), 630 em `_chapters/`. Smoke: índice em 345/345 artigos (2.666 chunks), `kb search` e `kb stats` respondendo, plataforma com home povoada e artigo vivo abrindo (tela conferida).
+
+## Incidente do ciclo (registrado, não escondido)
</file context>


## Incidente do ciclo (registrado, não escondido)

O RED do C2 expôs a **terceira ocorrência do dia** da mesma classe: teste sem isolamento moveu stubs de fixture para o `archive/` do vault real e regenerou o `_index.md` real. Limpeza aprovada pelo dono; fix estrutural: o piso autouse do conftest agora cobre `ARCHIVE_DIR` e `kb.compile.WIKI_DIR`, como cobre `STATE_DIR` desde 2026-07-29.

## Validação

**1.042 passed** (13 testes novos de fsutil/regroup + reescritas de heal), ruff limpo, appeasement exit 0, cobertura 93%.

## Riscos e dívida

| Item | Estado |
|---|---|
| 28 menções a paths `wiki/` em `claims.jsonl`/`knowledge.json` dangling pós-move | Aceito pelo ADR; jobs decay/contradiction não instalados |
| Golden set do bench sem objeto (851 slugs referenciados) | Aceito pelo ADR; golden preservado em disco |
| `.heal_backup` legado com formato próprio | Dívida registrada (V7 completo) |
| `transcripts-youtube`/`harness` seguem como artigo de capítulo | Decisão do dono; reavaliar quando o compile multi-fonte definir o que é tema |

## Próximos passos

O esforço do ADR-0018 (etapas 1–3) fecha aqui. O próximo é o **compile multi-fonte** (pré-requisitos agora existem: proveniência materializada, `library/` íntegra, `_chapters/` populado) — esforço novo, com wayfinder/spec próprio.
59 changes: 59 additions & 0 deletions features/029-chapters-regroup/SPEC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
feature: 029-chapters-regroup
title: Convenção _* honrada em todo o engine e reagrupamento por livro
epic: infra
status: done
created: 2026-08-06
pr:
---

# Convenção `_*` honrada em todo o engine e reagrupamento por livro

## Objetivo

Hoje sete pontos do engine ignoram a convenção `_*` (o heal pode deletar summary **hoje**; pós-move, `find_orphans` arrastaria `_chapters/` inteiro para `archive/`) e os 975 artigos de capítulo seguem misturados na wiki; ao final, `_*` é invisível para todo o engine, nenhuma remoção é `unlink`, e os capítulos estão agrupados por livro em `wiki/_chapters/` — com o move final executado apenas sob aprovação explícita do dono, ciente de que a wiki visível esvazia até o compile multi-fonte existir.

## Requisitos funcionais

- [x] RF-01 [P1] (C1): Dado qualquer diretório `_*` ou `.*` sob `wiki/`, quando `lint`, `heal`, `archive` (órfãos e idade), `update_index` e `stats` rodarem, então nenhum arquivo dentro dele é listado, amostrado, indexado, contado ou marcado como candidato — mesma semântica já usada por search/embeddings/graph.
- [x] RF-02 [P1] (C2): Dado um stub detectado pelo heal, quando a remoção executar, então o arquivo vai para `archive/` com a semântica de `move_to_archive` (hierarquia + backup versionado), nunca `unlink`; o manifest é atualizado (`mark_archived`).
- [x] RF-03 [P1] (C3): Dado `kb regroup scan`, quando rodar, então agrupa os artigos vivos por livro via proveniência do manifest (com os `unresolved` listados como braço de decisão humana) e imprime o plano de move `wiki/<...> → wiki/_chapters/<livro>/`, sem alterar nada.
- [x] RF-04 [P1] (C3): Dado `kb regroup apply --book <slug>`, quando executar, então move os artigos do livro (e seus summaries, para `wiki/_summaries/_chapters/<livro>/`) preservando o nome, atualiza `update_article_path` no manifest, regenera `_index.md`, refresca embeddings e commita **por livro**.
- [x] RF-05 [P1] (C4): Dado o lote final, quando o dono aprovar explicitamente ("a wiki esvazia"), então os livros aprovados são movidos um a um, com tag git prévia e smoke de search/stats ao final; sem aprovação, nada se move.
- [x] RF-06 [P2]: Dado um artigo `unresolved` (sem proveniência), quando o regroup rodar, então ele permanece na wiki e aparece no relatório como pendência humana — nunca é movido por inferência.

## Requisitos técnicos

- Helper único (`kb/fsutil.iter_articles(wiki_dir)` ou equivalente) com a semântica de `graph._e_artigo` (`_` e `.`), adotado nos sete pontos; sem mudança de assinatura pública dos comandos.
- `heal` mantém `--commit` e o registro de backup; a política de remoção única (move) fecha a dívida V7 no escopo do heal — a unificação do formato de backup do `.heal_backup` legado fica fora.
- Agrupamento pela chave `book` do manifest (materializada na 028); cosseno/`MAPA-DE-TEMAS.md` são insumo do relatório para temas transversais, não critério de move.
- Golden set e links do workspace `teach` quebram no move — perda aceita pelo ADR-0018, registrada no REPORT.
- Checar `claims.jsonl`/`knowledge.json` por referências a paths antes do C4; dangle aceito e registrado se houver.

## Mudanças de API/CLI

- Novo sub-app `regroup`: `kb regroup scan` e `kb regroup apply --book <slug> [--no-commit|--commit]`.
- `heal`: mudança de comportamento documentada (stub vai para `archive/` em vez de `unlink`).

## Testes

- Unit: `iter_articles` exclui `_*`/`.*`/symlink; cada adotante coberto (lint não lista, heal não sorteia, archive não marca, update_index não indexa, stats não conta artigo sob `_*`); heal move stub com backup e marca manifest; regroup agrupa por `book`, ignora `unresolved`, calcula destinos.
- Integration: vault fixture com manifest → `regroup scan` imprime plano; `apply --book` move artigo+summary, atualiza manifest, regenera índice, commita por livro (git fixture); artigo movido some de search/stats/_index.
- Manual (C4, HITL): preflight + tag → relatório de grupos → aprovação explícita do dono → apply livro a livro → `kb index build` + smoke + tela da plataforma.

## Dados de contexto

| Chave | Valor |
|-------|-------|
| Estimativa | 8h |
| Bloqueador | não |
| Risk | média-alta — move em massa; mitigada por commit por livro, tags, manifest e gate final humano |

## Dependências

- 028 mergeada (PR #70): `manifest.book`, `mark_archived`, `update_article_path`.
- DOMAIN compartilhado: `features/027-noise-retro/DOMAIN.md`.

## Notas

Terceira e última feature do esforço (etapa 3 do ADR-0018). Estado do vault na abertura: 975 artigos vivos, 856 entradas de manifest (849 com proveniência, 126 `unresolved` fora), 10 topics canônicos.
60 changes: 60 additions & 0 deletions features/029-chapters-regroup/TASKS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# TASKS — 029-chapters-regroup

**Spec:** `features/029-chapters-regroup/SPEC.md`
**Plan:** `features/029-chapters-regroup/PLAN.md`

```yaml
- id: C1
priority: P1
parallel: false
depends_on: []
ac_ref: RF-01
tag: AFK
vertical_slice: yes
behavior: "iter_articles único honra _*/.*/symlink e é adotado por lint, heal, archive, update_index e stats; heal deixa de poder tocar _summaries hoje."
verify: "python -m pytest tests/unit/test_fsutil_articles.py tests/unit/test_lint.py tests/unit/test_heal.py tests/unit/test_archive.py tests/unit/test_stats.py -q"
state: passing
```

```yaml
- id: C2
priority: P1
parallel: false
depends_on: [C1]
ac_ref: RF-02
tag: AFK
vertical_slice: yes
behavior: "Stub removido pelo heal vai para archive/ com backup versionado e manifest marcado archived — nunca unlink (V7 mínimo)."
verify: "python -m pytest tests/unit/test_heal.py -q"
state: passing
```

```yaml
- id: C3
priority: P1
parallel: false
depends_on: [C1]
ac_ref: RF-03, RF-04, RF-06
tag: AFK
vertical_slice: yes
behavior: "kb regroup scan agrupa por manifest.book com unresolved como braço humano; apply --book move artigo+summary para _chapters/, atualiza manifest e índices, commita por livro."
verify: "python -m pytest tests/unit/test_regroup.py tests/integration/test_regroup_cli.py -q"
state: passing
```

```yaml
- id: C4
priority: P1
parallel: false
depends_on: [C2, C3]
ac_ref: RF-05
tag: HITL
vertical_slice: yes
behavior: "Gate final do dono: relatório de grupos → aprovação explícita (a wiki esvazia) → apply livro a livro com tag prévia → index build + smoke + tela."
verify: "relatório aprovado + contagens + tela"
state: passing
```

## Definition of Done

`state: passing` = verify verde + (HITL) evidência de aprovação e tela. Feature fecha com C1–C4 passing e `quality-gate` PASS.
12 changes: 7 additions & 5 deletions kb/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ def find_orphans(wiki_dir: Path) -> list[Path]:

if not wiki_dir.exists():
return []
backlink_sources = [p for p in wiki_dir.rglob("*.md") if not p.is_symlink()]
all_md = [p for p in backlink_sources if p.name != "_index.md"]
from kb.fsutil import iter_articles

backlink_sources = list(iter_articles(wiki_dir))
all_md = backlink_sources

def identidade(path: Path) -> str:
return path.relative_to(wiki_dir).with_suffix("").as_posix()
Expand All @@ -69,11 +71,11 @@ def find_by_age(wiki_dir: Path, days: int) -> list[Path]:
"""Retorna artigos com mtime anterior ao cutoff de dias."""
if not wiki_dir.exists():
return []
from kb.fsutil import iter_articles

cutoff = time.time() - (days * 86400)
result = []
for p in wiki_dir.rglob("*.md"):
if p.is_symlink():
continue
for p in iter_articles(wiki_dir):
try:
if p.stat().st_mtime < cutoff:
result.append(p)
Expand Down
63 changes: 63 additions & 0 deletions kb/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"dedup scan | dedup apply [--no-commit|--commit]\n\n"
"topics normalize [--apply] [--no-commit|--commit] | topics assign [--apply] [--limit INT] [--no-commit|--commit]\n\n"
"archive [--stale] [--older-than INT] [--dry-run]\n\n"
"regroup scan | regroup apply --book <slug> [--no-commit|--commit]\n\n"
"compile (alvo) [--workers/-j INT] [--allow-sensitive] [--no-commit|--commit]"
" [--no-update-index]\n\n"
"qa <pergunta> [--file-back/-f] [--to-wiki] [--depth INT] [--no-traverse]"
Expand All @@ -53,6 +54,7 @@
manifest_app = typer.Typer(help="Proveniência artigo→fonte no manifest (backfill)")
dedup_app = typer.Typer(help="Duplicatas de ingestão (scan/apply)")
topics_app = typer.Typer(help="Topics do frontmatter (normalize/assign)")
regroup_app = typer.Typer(help="Reagrupamento de capítulos por livro (scan/apply)")
app.add_typer(jobs_app, name="jobs")
app.add_typer(discovery_app, name="discovery")
app.add_typer(handoff_app, name="handoff")
Expand All @@ -61,6 +63,67 @@
app.add_typer(manifest_app, name="manifest")
app.add_typer(dedup_app, name="dedup")
app.add_typer(topics_app, name="topics")
app.add_typer(regroup_app, name="regroup")


def _regroup_plan():
from kb.config import WIKI_DIR
from kb.regroup import plan_regroup
from kb.state import load_manifest

return plan_regroup(WIKI_DIR, load_manifest())


@regroup_app.command("scan")
def regroup_scan():
"""Plano de reagrupamento por livro (dry-run; não altera nada)."""
from kb.config import WIKI_DIR

plan = _regroup_plan()
for slug in sorted(plan.groups):
artigos = plan.groups[slug]
typer.echo(f"[{slug}] {len(artigos)} artigo(s) — {plan.book_names[slug][:60]}")
for origem, destino in artigos:
typer.echo(f" {origem.relative_to(WIKI_DIR)} → {destino.relative_to(WIKI_DIR)}")
typer.echo(
f"{sum(len(v) for v in plan.groups.values())} artigo(s) em {len(plan.groups)} livro(s); "
f"{len(plan.unresolved)} unresolved (permanecem na wiki)"
)
for artigo in plan.unresolved:
typer.echo(f"unresolved {artigo.relative_to(WIKI_DIR)}")


@regroup_app.command("apply")
def regroup_apply(
book: str = typer.Option(..., "--book", help="Slug do livro a mover (um por commit)"),
no_commit: bool = typer.Option(True, "--no-commit/--commit"),
):
"""Move os artigos de UM livro para _chapters/ (commit por livro)."""
from kb.config import MANIFEST_PATH, WIKI_DIR
from kb.regroup import apply_book

plan = _regroup_plan()
if book not in plan.groups:
typer.echo(f"livro desconhecido no plano: {book}", err=True)
raise typer.Exit(2)
log = apply_book(WIKI_DIR, plan, book)
moved = [entry for entry in log if entry["action"] == "moved"]
for entry in log:
if entry["action"] == "moved":
typer.echo(f"movido: {entry['dest']}")
else:
typer.echo(f"erro: {entry['source']} — {entry.get('detail', '')}", err=True)
typer.echo(f"{len(moved)} arquivo(s) movidos para _chapters/{book}/")
if moved and not no_commit:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: A regroup that reports an error can still create a successful per-book commit containing only the moves that happened before the failure. The commit should be skipped whenever log contains an error, so the failed operation remains recoverable and is not presented as an applied book regroup.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At kb/cli.py, line 117:

<comment>A regroup that reports an error can still create a successful per-book commit containing only the moves that happened before the failure. The commit should be skipped whenever `log` contains an error, so the failed operation remains recoverable and is not presented as an applied book regroup.</comment>

<file context>
@@ -61,6 +63,67 @@
+        else:
+            typer.echo(f"erro: {entry['source']} — {entry.get('detail', '')}", err=True)
+    typer.echo(f"{len(moved)} arquivo(s) movidos para _chapters/{book}/")
+    if moved and not no_commit:
+        from kb.git import commit
+
</file context>
Suggested change
if moved and not no_commit:
if moved and not no_commit and not any(
entry["action"] == "error" for entry in log
):

from kb.git import commit

paths = [Path(entry["source"]) for entry in moved]
paths += [Path(entry["dest"]) for entry in moved]
paths += [Path(entry["backup"]) for entry in moved if "backup" in entry]
paths += [WIKI_DIR / "_index.md", MANIFEST_PATH]
commit(f"chore(corpus): reagrupa {book} em _chapters/", paths)
if any(entry["action"] == "error" for entry in log):
raise typer.Exit(1)


@topics_app.command("normalize")
Expand Down
Loading
Loading