Compare commits
17 commits
task/incid
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6d35ba034 | ||
|
|
308ae6a9f4 | ||
|
|
efd9ad9443 | ||
|
|
2df4f1dbc3 | ||
|
|
e20845ae1b | ||
|
|
d8ff94ca1b | ||
|
|
17e7cb0aea | ||
|
|
a97cec0819 | ||
|
|
1dca438015 | ||
|
|
40d78ce60d | ||
|
|
9a86843ddd | ||
|
|
89f75c34b0 | ||
|
|
419df70e29 | ||
|
|
3af6752461 | ||
|
|
03441a16c0 | ||
|
|
f1559991c1 | ||
|
|
99790438b6 |
|
|
@ -1,18 +1,34 @@
|
||||||
---
|
---
|
||||||
name: kb-publish
|
name: kb-publish
|
||||||
description: Reminds the operator to publish the KB site after changes to kb/**/*.md. Trigger whenever this session's own edits, or a merge/diff visible in git, touch files under kb/.
|
description: At session close, asks the operator whether to publish the KB site if kb/**/*.md changed this session. Trigger whenever the session is wrapping up (save-session, "kończymy sesję", natural end of task) and this session's own edits, or a merge/diff visible in git, touched files under kb/.
|
||||||
---
|
---
|
||||||
|
|
||||||
## What this skill does
|
## What this skill does
|
||||||
|
|
||||||
At the end of your response, if this session touched `kb/**/*.md` — either
|
Trigger point: **session close**, not every response. Session close means the
|
||||||
through your own edits or through a merge/diff you observed in git — append
|
operator signals they're wrapping up — e.g. invoking `save-session`, saying
|
||||||
exactly one reminder sentence:
|
something like "kończymy sesję" / "koniec na dziś" / "wrap up", or the task
|
||||||
|
reaching its natural end with no further work queued. Don't act on this skill
|
||||||
|
mid-session just because a `kb/` file was touched.
|
||||||
|
|
||||||
> Zmiany w kb/ — pamiętaj odpalić `bash scripts/kb/publish.sh` żeby opublikować.
|
At that moment, if this session touched `kb/**/*.md` — either through your
|
||||||
|
own edits or through a merge/diff you observed in git — **ask the operator
|
||||||
|
directly**, as a question requiring a yes/no answer, not a passive reminder:
|
||||||
|
|
||||||
One sentence, no more. Don't repeat it more than once per response, and don't
|
> Sesja dotknęła kb/. Zregenerować i zasugerować publikację (`bash scripts/kb/publish.sh`)?
|
||||||
add it if nothing under `kb/` changed.
|
|
||||||
|
- If the operator confirms (any affirmative reply): print the ready-to-paste
|
||||||
|
command block so they can copy it straight into their own shell:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/kb/publish.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not run it yourself — see below.
|
||||||
|
- If the operator declines: drop it, nothing more to do.
|
||||||
|
- If the operator doesn't respond or ignores the question (moves on to
|
||||||
|
something else, ends the conversation): do **not** ask again in this
|
||||||
|
session. One ask per session, max.
|
||||||
|
|
||||||
## What this skill does NOT do
|
## What this skill does NOT do
|
||||||
|
|
||||||
|
|
@ -23,4 +39,4 @@ production — that is out of scope for a worktree agent and requires an
|
||||||
explicit, direct instruction from the operator in the current turn.
|
explicit, direct instruction from the operator in the current turn.
|
||||||
|
|
||||||
If the operator explicitly asks you to run it, that instruction stands on its
|
If the operator explicitly asks you to run it, that instruction stands on its
|
||||||
own — this skill only governs the passive reminder, not that request.
|
own — this skill only governs the session-close question, not that request.
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ Agents must never execute destructive actions (restarts, deploys, config changes
|
||||||
|
|
||||||
## Event System
|
## Event System
|
||||||
|
|
||||||
Events are append-only JSON lines at `/opt/homelab/events/YYYY-MM-DD/<node>/events.jsonl`.
|
Events are append-only, one JSON file per event, flat under `/opt/homelab/events/<node>/evt-<node>-<unixts>-<type>[-<service>].json`.
|
||||||
|
|
||||||
Emit via `scripts/lib/events.sh` (shell) or `scripts/lib/events.py` (Python).
|
Emit via `scripts/lib/events.sh` (shell) or `scripts/lib/events.py` (Python).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,3 +69,139 @@ Brak zmian w repo.
|
||||||
### Narrative
|
### Narrative
|
||||||
|
|
||||||
> _user-provided summary_
|
> _user-provided summary_
|
||||||
|
|
||||||
|
## Session 23:00
|
||||||
|
|
||||||
|
Deploy control-plane (observer + supervisor) na VPS: stale-resolve 24h +
|
||||||
|
flagi `resolve-requests` (commit `71a7af5`), unikalny `action_id`
|
||||||
|
`container_restart` z bare-id fallbackiem (commit `91db682`), usunięcie
|
||||||
|
gokapi z desired state (commit `74ff3ee`) — zmerdowane do `master` jako
|
||||||
|
`f155999` przez operatora tuż przed sesją. SUPERVISED — checkpoint A po
|
||||||
|
weryfikacji deployu, checkpoint B po teście ścieżki flagi.
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
|
||||||
|
```
|
||||||
|
(brak commitów kodu tej sesji — wyłącznie deploy + test na produkcji;
|
||||||
|
log sesji poniżej dopisany bez pusha)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Files changed
|
||||||
|
|
||||||
|
Brak zmian w repo poza niniejszym logiem.
|
||||||
|
|
||||||
|
### Deploys / operacje na nodach
|
||||||
|
|
||||||
|
**KROK 0 — sanity:** `git pull` na `~/homelab-codex-ws` (SOLARIA, główny
|
||||||
|
checkout) — już aktualny na `03441a1` (na wierzchu mergu `f155999`).
|
||||||
|
`git status`/`diff HEAD` czyste. Wcześniej w tej samej sesji (przed
|
||||||
|
mergem) `git log -1` pokazywał `4fa10f0` — merge jeszcze nie istniał;
|
||||||
|
zatrzymano się i poczekano na operatora zamiast mergować samodzielnie
|
||||||
|
(worktree-aware: merge to wyłącznie krok człowieka).
|
||||||
|
|
||||||
|
**KROK 1 — deploy control-plane (checkpoint A, zatwierdzony):**
|
||||||
|
- Rollback tagi: `control-plane-{executor,observer,operator-ui,supervisor}
|
||||||
|
:rollback-pre-resolvefix` — ten sam wzorzec co `:rollback-pre-dispatchfix`
|
||||||
|
z 08-06.
|
||||||
|
- `git pull origin master` na VPS (`003f83d` → `03441a1`, fast-forward),
|
||||||
|
`docker compose up -d --build --force-recreate`.
|
||||||
|
- `deploy-local.sh`'s auto-chown krok padł: brak TTY dla hasła sudo,
|
||||||
|
`/opt/homelab/backups` i `/opt/homelab/events/solaria/*` są `oskar:oskar`
|
||||||
|
zamiast `aerbot:aerbot` (1000). Sprawdzone: `actions/`, `world/`,
|
||||||
|
`state/`, `config/` (realna ścieżka zapisu control-plane) już poprawnie
|
||||||
|
`aerbot:aerbot 775` — ominięto self-heal, `docker compose` odpalony
|
||||||
|
bezpośrednio bez sudo. Mismatch na `backups/`/`events/solaria/*`
|
||||||
|
pozostawiony nietknięty (follow-up niżej).
|
||||||
|
- Weryfikacja: 4/4 kontenery `healthy`, 0 linii error/traceback/exception
|
||||||
|
od restartu. sha256 `observer.py` (mount `/repo`, żywy) i `supervisor.py`
|
||||||
|
(wypieczony `/app/src`, wymaga `--build`) == repo HEAD, potwierdzone
|
||||||
|
osobno przez `docker exec` w obu kontenerach.
|
||||||
|
- Po 3 cyklach reconcile: `active_incidents: 0`, `world/resolve-requests/`
|
||||||
|
utworzony przez observera (pusty).
|
||||||
|
- `redeploy-vps-gokapi` (pending od 07-09) auto-cancelled po pierwszym
|
||||||
|
cyklu: `cancelled_reason: "service_removed_from_desired_state"` — bez
|
||||||
|
ponownego wygenerowania. `pending/` pozostał czysty (tylko niezwiązane
|
||||||
|
alerty HA z piha).
|
||||||
|
|
||||||
|
**KROK 2 — test ścieżki flagi na LUSTRO (checkpoint B, zatwierdzony):**
|
||||||
|
- `docker stop node-exporter` na LUSTRO → observer otworzył
|
||||||
|
`inc-1787777894-lustro-node-exporter`.
|
||||||
|
- Flaga: `touch world/resolve-requests/<id>` przez zwykłego SSH
|
||||||
|
usera **odrzucony permission denied** — katalog `755 aerbot:aerbot`,
|
||||||
|
brak zapisu grupowego mimo że `oskar` jest w grupie `aerbot`. Obejście:
|
||||||
|
`docker exec control-plane-observer touch ...` (proces w kontenerze
|
||||||
|
działa jako uid 1000 = właściciel katalogu). Follow-up niżej.
|
||||||
|
- Resolve w **1.01 s** od touch (limit ≤10s), `resolved_reason:
|
||||||
|
manual_operator`, flaga skasowana, `service.incident_id` wyczyszczony,
|
||||||
|
log INFO `"Manually resolving incident ... via resolve-request flag"`.
|
||||||
|
- Drift trwał dalej (kontener wciąż stopped) → supervisor wygenerował
|
||||||
|
`container-restart-lustro-node-exporter-1787777887` — **nowy format
|
||||||
|
id z COMMIT 2 potwierdzony na produkcji** — oraz równolegle
|
||||||
|
`redeploy-lustro-node-exporter` (bare id, ścieżka `unhealthy_service`
|
||||||
|
po wyczyszczeniu `incident_id`).
|
||||||
|
- Za decyzją operatora: `POST /action/mutate` na `127.0.0.1:18180`
|
||||||
|
(ten sam endpoint co UI/Telegram) — zatwierdzono restart, odrzucono
|
||||||
|
redeploy jako nadmiarowy.
|
||||||
|
- Executor zdispatchował realnie do LUSTRO; node-agent wykonał
|
||||||
|
`docker restart node-exporter` (log: `"Restarted container
|
||||||
|
'node-exporter' for action container-restart-lustro-node-exporter
|
||||||
|
-1787777887"`), akcja `completed`.
|
||||||
|
- Drift utrzymał się jeszcze chwilę po zatwierdzeniu → drugi, nowy
|
||||||
|
incydent (`inc-1787777955-...`) i druga, odrębna pending akcja
|
||||||
|
(`...-1787777950`, inny suffix `started_at`) — dokładnie oczekiwane
|
||||||
|
zachowanie "różny id przy nowym incydencie". Po powrocie zdrowia
|
||||||
|
auto-cancelled: `cancelled_reason: "drift_resolved_auto"`, bez
|
||||||
|
interwencji.
|
||||||
|
- Stan końcowy: `node-exporter` na LUSTRO `Up`, oba incydenty
|
||||||
|
`resolved`, `active_incidents: 0`, `pending/` czysty, 4/4 kontenery
|
||||||
|
control-plane nadal `healthy`, 0 error-ish linii w logach.
|
||||||
|
|
||||||
|
### Follow-upy
|
||||||
|
|
||||||
|
- **pytest env zepsuty na SOLARII**: `~/.local/bin/pytest` (brak
|
||||||
|
`_pytest`) i `homelab-codex-ws/.venv` (brak `pytest` w ogóle) oba
|
||||||
|
niedziałające; działa wyłącznie `/home/oskar/anaconda3/bin/pytest`
|
||||||
|
(7.4.4). Użyty do pełnego runu przed force-pushem poprawki COMMIT 2
|
||||||
|
(184 passed control-plane, 70 passed node-agent).
|
||||||
|
- **`world/resolve-requests/` permissions**: `755 aerbot:aerbot` zamiast
|
||||||
|
konwencji `775` używanej w `actions/`/`world/`/`state/`/`config/`.
|
||||||
|
Blokuje operatora SSH przed bezpośrednim `touch` flagi resolve —
|
||||||
|
manualna ścieżka z 71a7af5 ("operator drops a file") w praktyni wymaga
|
||||||
|
`docker exec`. Poprawić `chmod 775` / mode przy `os.makedirs` w
|
||||||
|
observer.py.
|
||||||
|
- **`backups/` i `events/solaria/*` ownership**: `oskar:oskar` zamiast
|
||||||
|
`aerbot:aerbot` — nie blokuje funkcjonalnie (czytelne dla "other"), ale
|
||||||
|
psuje self-heal chown w `deploy-local.sh` (próbuje rekurencyjnego sudo
|
||||||
|
chown całego `/opt/homelab` bez TTY/hasła). Do ręcznego wyczyszczenia
|
||||||
|
z hasłem sudo albo do zmiany self-heal na scoped (tylko katalogi
|
||||||
|
control-plane realnie potrzebuje) zamiast całego drzewa.
|
||||||
|
- **CLAUDE.md doc drift**: opisuje `events/YYYY-MM-DD/<node>/events.jsonl`,
|
||||||
|
rzeczywisty layout na VPS to płaskie `events/<node>/evt-*.json` (jeden
|
||||||
|
plik na zdarzenie, bez partycjonowania po dacie). Do poprawienia przy
|
||||||
|
najbliższej okazji.
|
||||||
|
- `redeploy-vps-gokapi` — zamknięty tym deployem (auto-cancelled), nie
|
||||||
|
wymaga już dalszego follow-upu z sesji 16:00.
|
||||||
|
|
||||||
|
### Narrative
|
||||||
|
|
||||||
|
> _user-provided summary_
|
||||||
|
|
||||||
|
## Session 23:06
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
|
||||||
|
```
|
||||||
|
(brak commitów — sesja bez żadnej pracy poza natychmiastowym zamknięciem)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Files changed
|
||||||
|
|
||||||
|
Brak zmian w repo.
|
||||||
|
|
||||||
|
### Deploys
|
||||||
|
|
||||||
|
None recorded
|
||||||
|
|
||||||
|
### Narrative
|
||||||
|
|
||||||
|
> _user-provided summary_
|
||||||
|
|
|
||||||
59
docs/sessions/2026-08-27-kb-faza5-etap1.md
Normal file
59
docs/sessions/2026-08-27-kb-faza5-etap1.md
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
---
|
||||||
|
okf: "0.1"
|
||||||
|
type: session-log
|
||||||
|
visibility: private
|
||||||
|
status: active
|
||||||
|
updated: 2026-08-27
|
||||||
|
links:
|
||||||
|
- ../../kb/phases/kb-m5-faza5-wiki.md
|
||||||
|
- ../../kb/phases/kb-m5-faza3.md
|
||||||
|
- ../../kb/audits/wiki-kompilat-recon-2026-08-26.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Sesja 2026-08-27 — Faza 5: decyzje + Etap 1 + rekonсyliacja (ZAMKNIĘTY)
|
||||||
|
|
||||||
|
## Timeline
|
||||||
|
|
||||||
|
- **Sanity** — automat mailowy zdrowy po nocy (last_success świeży, gmail
|
||||||
|
max(ts) bieżący).
|
||||||
|
- **Formalizacja decyzji** (2df4f1d i wcześniejsze) — pakiet (a)-(h) audytu
|
||||||
|
`wiki-kompilat-recon-2026-08-26.md` ZATWIERDZONY w całości; inwariant 7
|
||||||
|
dopisany do faza3 §8.1 (izolacja retrievalu kompilacji od `source='wiki'`);
|
||||||
|
`check_okf.py` łapie bajty kontrolne (nowy check #12 + testy) — 4 NUL-e
|
||||||
|
usunięte z samego audytu.
|
||||||
|
- **Etap 1** (`task/wiki-etap1`, autonomiczny run CC ~25 min) — bootstrap +
|
||||||
|
3 strony proof (`sprawy/fll-2025-26`, `podmioty/mbank`,
|
||||||
|
`osoby/pawel-cesar-sanjuan-szklarz`). 40 sources + 47 przypisów
|
||||||
|
zweryfikowanych byte-for-byte, zero degradacji. Alias-resolution: 6 er
|
||||||
|
życia Pawła spójnie + 2 fałszywe pozytywy pod `paweld2.eu` wykryte i
|
||||||
|
udokumentowane jako wynik negatywny (kluczowy test mechanizmu PASS).
|
||||||
|
- **ODKRYCIE** — kb-wiki istniało na Forgejo od 2026-07-21 (5 stron: `pzu`,
|
||||||
|
`warta`, `ubezpieczenie-auto`, `fll-2025-26`, `wspólnota`) — proof
|
||||||
|
wykonany w fazie 3 (wątek "Kontynuacja wątku o KB", decyzja D7),
|
||||||
|
nieodnotowany w `kb/` ani w audycie. Recon 08-26 błędnie stwierdził
|
||||||
|
"remote nie istnieje" — sprawdził repo/pilota/bazę, nie odpytał Forgejo
|
||||||
|
bezpośrednio.
|
||||||
|
- **LEKCJA 1**: fakty wykonania muszą lądować w repo (lekcja 6 narty27 w
|
||||||
|
praktyce — zgubiliśmy całe repo na 5 tygodni).
|
||||||
|
- **LEKCJA 2**: recon zasobów zewnętrznych odpytuje źródło wprost, nie
|
||||||
|
wnioskuje z planów.
|
||||||
|
- **Rekonсyliacja** — porównanie dwóch niezależnych kompilacji `fll-2025-26`
|
||||||
|
(lipiec paperless-only vs sierpień paperless+gmail): komplementarne, zero
|
||||||
|
sprzeczności w faktach wspólnych; konwergencja metodologiczna (obie sesje
|
||||||
|
ten sam chunk 277/278, obie odmówiły potwierdzenia nieczytelnego OCR).
|
||||||
|
Scalenie: 21 envelope, 38 par sources, 45 przypisów re-zweryfikowanych;
|
||||||
|
naprawiony wadliwy lipcowy przypis; konwencja "Brak danych w KB"
|
||||||
|
sformalizowana. Potem mbank+paweł przeniesione do kanonicznego repo,
|
||||||
|
konwencje (d)-(f) scalone, walidator w repo. Stan końcowy kb-wiki@Forgejo
|
||||||
|
`a540a99`: 7 stron, lint 74/74 sources + 150/150 inline zielono.
|
||||||
|
- **kb-wiki remote** przepięty HTTPS→SSH (port 222).
|
||||||
|
- **Kandydat Etapu 2** wykryty SQL-em: `podmioty/future-minds` (organizator
|
||||||
|
FLL, 8+ dokumentów).
|
||||||
|
- **Follow-upy bez zmian**: rotacja sekretów (kb-postgres + `TG_TOKEN` —
|
||||||
|
NADAL WISI), fix UID SEARCH, PDF-y, charset, R1/R2 z incydentu 26.08.
|
||||||
|
|
||||||
|
## Stan Fazy 5
|
||||||
|
|
||||||
|
- Etap 1 ✓ (2026-08-27). Next: Etap 2 — skala do 17+1 encji, lint w kodzie,
|
||||||
|
skan charset/NUL, decyzja o integracji `source='wiki'` w retrievalu
|
||||||
|
(inwariant 7 obowiązuje).
|
||||||
23
docs/sessions/2026-08-27.md
Normal file
23
docs/sessions/2026-08-27.md
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
## Session 13:49
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
a97cec0 merge: task/drobne-fixy (resolve-requests 775, prune out of health-monitor, events doc, redeploy action_id)
|
||||||
|
1dca438 fix(supervisor): apply started_at suffix to redeploy action_id too
|
||||||
|
40d78ce docs: fix events layout drift in CLAUDE.md
|
||||||
|
9a86843 fix(monitor): remove unfiltered docker container prune from health-monitor.sh
|
||||||
|
89f75c3 fix(observer): make world/resolve-requests/ group-writable
|
||||||
|
|
||||||
|
### Files changed
|
||||||
|
CLAUDE.md | 2 +-
|
||||||
|
scripts/monitor/health-monitor.sh | 110 +++------------------
|
||||||
|
scripts/observer/observer.py | 12 +++
|
||||||
|
services/control-plane/src/supervisor.py | 40 ++++----
|
||||||
|
.../control-plane/tests/test_incident_lifecycle.py | 17 ++++
|
||||||
|
.../tests/test_supervisor_action_id_uniqueness.py | 50 +++++++++-
|
||||||
|
6 files changed, 109 insertions(+), 122 deletions(-)
|
||||||
|
|
||||||
|
### Deploys
|
||||||
|
- control-plane → VPS: tagged 4/4 images `:rollback-pre-drobnefixy`, `git pull` (03441a1→a97cec0) + `docker compose up -d --build --force-recreate` (direct, no deploy-local.sh, per 26.08 precedent). Result: 4/4 healthy, zero error/traceback in logs since restart, sha256 of observer.py and supervisor.py match HEAD, `world/resolve-requests/` mode 775 confirmed, incidents.json stable (md5 unchanged) over 3 observer cycles. 30s permission test: flag `test-perms-123` dropped via plain `ssh` as `oskar` (no docker exec) was picked up and deleted in ~5s with `WARNING - Resolve-request flag for unknown incident test-perms-123 — removing flag` — 775 confirmed working in practice.
|
||||||
|
|
||||||
|
### Narrative
|
||||||
|
> _user-provided summary_
|
||||||
856
kb/audits/wiki-kompilat-recon-2026-08-26.md
Normal file
856
kb/audits/wiki-kompilat-recon-2026-08-26.md
Normal file
|
|
@ -0,0 +1,856 @@
|
||||||
|
---
|
||||||
|
okf: "0.1"
|
||||||
|
type: audit
|
||||||
|
visibility: private
|
||||||
|
status: active
|
||||||
|
updated: 2026-08-27
|
||||||
|
as_of: 2026-08-26
|
||||||
|
links:
|
||||||
|
- ../phases/kb-m5-faza3.md
|
||||||
|
- ../phases/kb-m5-faza-mailowa.md
|
||||||
|
- ../services/narty27.md
|
||||||
|
- ../services/kb-query.md
|
||||||
|
- ../services/kb-site.md
|
||||||
|
- ../subsystems/kb-mail-pillar.md
|
||||||
|
- ../subsystems/kb-overview.md
|
||||||
|
- ../subsystems/kb-documents-pillar.md
|
||||||
|
- ../subsystems/recon-multiagent.md
|
||||||
|
- ../decisions/architektura-2026-07-28.md
|
||||||
|
- ../decisions/backlog-aktywne.md
|
||||||
|
- ../incidents/2026-08-26-mail-sync-20-dni-ciszy.md
|
||||||
|
- ../audits/mail-sync-2026-08-06.md
|
||||||
|
- ../runbooks/mail-sync-run.md
|
||||||
|
- ../services/job-mail-imap-sync.md
|
||||||
|
- ../services/pkg-kb-retrieval.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Recon — wiki-kompilat (faza 5), 2026-08-26
|
||||||
|
|
||||||
|
Read-only recon. Zakres: przygotowanie fazy 5 subsystemu B (KB) — syntezy wiki nad
|
||||||
|
warstwą dowodową RAG/pgvector. Zlecenie: zbadać 9 punktów (lokalizacja, bootstrap
|
||||||
|
encji, tryb kompilatora, format linku dowodowego, anty-propagacja, integracja z
|
||||||
|
kb-query, zależności/kolizje, plan iteracji, decyzje operatora) i wynieść wniosek
|
||||||
|
per punkt. **Nic nie zostało zaimplementowane, zdeployowane ani skommitowane poza
|
||||||
|
tym dokumentem.**
|
||||||
|
|
||||||
|
Dowody: repo (worktree `task/wiki-kompilat-recon`, cięty z mastera @ `4fa10f0`),
|
||||||
|
pilot `~/narty-2027/saalbach-kb` na SOLARII (lokalny odczyt), żywa baza `kb` na
|
||||||
|
PIHA przez `ssh piha 'docker exec kb-postgres psql ...'` (SELECT-y, zero zapisów),
|
||||||
|
`systemctl status` na PIHA dla stanu timerów.
|
||||||
|
|
||||||
|
**Znalezisko nadrzędne, zanim cokolwiek innego:** to nie jest recon od zera.
|
||||||
|
`kb/phases/kb-m5-faza3.md` §8 zawiera **już zatwierdzony przez operatora szkic
|
||||||
|
architektoniczny** wiki-kompilatu (decyzja 2026-07-15, opisana jako „NIE podlega
|
||||||
|
zmianie") plus rozwinięcie wykonawcze (§8.2, 2026-07-17) z konkretnymi
|
||||||
|
rozstrzygnięciami: osobne repo `kb-wiki`, struktura katalogów, format frontmattera
|
||||||
|
z blokiem `sources`, inline-przypisy `[^envelope_id#chunk_id]`, trzypoziomowa
|
||||||
|
kaskada retrievalu `wiki → summary → chunk`, kompilacja przez CC/API, lint
|
||||||
|
okresowy, pętla zwrotna. Zadanie tego reconu — tak jak je czytam — to **zweryfikować
|
||||||
|
te ustalenia w świetle tego, co się wydarzyło od 07-17** (pilot narty27, cała faza
|
||||||
|
mailowa, incydent 20 dni ciszy), dociągnąć luki, których tamten szkic nie
|
||||||
|
rozstrzygał (chunk-id vs re-chunking, self-citation, konkretna lista encji z
|
||||||
|
prawdziwych danych), i złożyć to w plan pierwszego etapu. Poniżej cytuję zamiast
|
||||||
|
odtwarzać, i mówię wprost, gdzie się zgadzam, a gdzie proponuję dociągnięcie.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Stan korpusu na dziś (2026-08-26) — czy blokada z faza3 jest zdjęta
|
||||||
|
|
||||||
|
`kb/phases/kb-m5-faza3.md:628`, cytat ze szkicu operatora: *„Pełna wiki po fazie
|
||||||
|
mailowej (przyrostówka) — wcześniej kompilat byłby fotografią przeszłości."*
|
||||||
|
`kb/audits/mail-sync-2026-08-06.md` §7 nazywa to *jedyną znalezioną jawną
|
||||||
|
zależność „X czeka na przyrostówkę" w całym KB*.
|
||||||
|
|
||||||
|
Zmierzone dziś na żywej bazie:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT source, max(ts)::date AS latest, count(*) FROM envelope GROUP BY 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
| source | latest | count |
|
||||||
|
|---|---|---|
|
||||||
|
| gmail | **2026-08-26** | 227 280 |
|
||||||
|
| fastmail | **2026-08-26** | 105 |
|
||||||
|
| paperless | 2026-07-13 | 191 |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT count(*) FROM document_chunk WHERE embedding IS NULL AND excluded_reason IS NULL;
|
||||||
|
-- → 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Korpus mailowy sięga **dziś**, nie 2026-06-19 jak w audycie 08-06 — incydent
|
||||||
|
[20 dni ciszy](../incidents/2026-08-26-mail-sync-20-dni-ciszy.md) naprawiony tego
|
||||||
|
samego dnia co to zlecenie domknął przyrostówkę: kursor dogonił IMAP, 548
|
||||||
|
zaległych kopert wchłonięte, zero błędów. `kb-mail-sync.timer` (co godzinę) i
|
||||||
|
`kb-ingest.timer` (co 2h, embed) oba `active (waiting)` na PIHA, żywe od
|
||||||
|
2026-08-06, bez przerwy odkąd monitoring realnie działa (od tego samego
|
||||||
|
incydentu). Embed backlog = **0** — nic nie czeka na wektor.
|
||||||
|
|
||||||
|
**Wniosek: blokada z faza3 jest zdjęta dziś, nie wcześniej.** To jest właściwy
|
||||||
|
moment na ten recon — zrobiony tydzień wcześniej trafiłby na korpus wciąż
|
||||||
|
urwany, a wiki-kompilat skompilowałby na trwałe fotografię z czerwca. Warto
|
||||||
|
odnotować kruchość tego stanu: przyrostówka działa dzięki jednemu `chown` i
|
||||||
|
jednemu `/-/reload`, oba ręczne, oba bez automatyzacji (R1/R2 z incydentu —
|
||||||
|
poprawna własność katalogów przy `sudo`-runach i reload reguł Prometheusa w
|
||||||
|
deployu — zostają otwarte). Faza 5 dziedziczy to ryzyko: gdyby przyrostówka
|
||||||
|
znów ucichła cicho, wiki-kompilat kompilowałby dalej z coraz starszej wody, bez
|
||||||
|
sygnału. Patrz §7 niżej (interakcja z monitoringiem `KbMailSyncStale`).
|
||||||
|
|
||||||
|
**191 kopert paperless** nie rosną od 2026-07-13 — to osobny fakt: paperless nie
|
||||||
|
ma przyrostówki (dokumenty trafiają tam ręcznie), niezwiązany z blokadą mailową.
|
||||||
|
Nie wstrzymuje fazy 5.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Punkt 1 — Lokalizacja wiki: `kb/wiki/` w repo vs osobne repo
|
||||||
|
|
||||||
|
### Co już zdecydowano
|
||||||
|
|
||||||
|
`kb-m5-faza3.md` §8.2: *„Repozytorium (rozstrzygnięcie punktu 6 szkicu): osobne
|
||||||
|
repo `kb-wiki` — decyzja 7, uzasadnienie w §2. Klon roboczy na PIHA:
|
||||||
|
`/opt/homelab/data/kb-wiki/`."* — i dalej: *„D7 — rekomendacja: osobne
|
||||||
|
`kb-wiki`"* w podsumowaniu (§12). To jest już podjęta decyzja, nie propozycja.
|
||||||
|
|
||||||
|
### Weryfikacja na wzorcu pilota
|
||||||
|
|
||||||
|
narty27 (`~/narty-2027/saalbach-kb`) **potwierdza ten wzorzec empirycznie**:
|
||||||
|
`kb/services/narty27.md` — *„Content is personal and lives outside the repo… It
|
||||||
|
is never committed — not to this repo, not to any other."* Źródło leży w osobnym
|
||||||
|
lokalnym gicie na SOLARII, poza `homelab-codex`, infra (`services/narty27`)
|
||||||
|
zostaje w `homelab-codex`. Dokładnie ten sam split, jaki faza3 projektuje dla
|
||||||
|
`kb-wiki`: **treść osobno, infrastruktura hostująca w repo.**
|
||||||
|
|
||||||
|
### Rekomendacja: potwierdzam D7 (osobne repo), z jednym doprecyzowaniem
|
||||||
|
|
||||||
|
Zgadzam się z decyzją bez zmian co do kierunku. Powody, które faza3 już podała
|
||||||
|
i które podtrzymuję:
|
||||||
|
|
||||||
|
1. **Cykl życia inny niż kodu infry.** Commity do `kb-wiki` będą częste,
|
||||||
|
drobne, generowane maszynowo (`compile: <strona> ← <envelope_ids>`) —
|
||||||
|
zalałyby historię `homelab-codex-ws`, gdzie commituje operator ręcznie i
|
||||||
|
rzadko (`CLAUDE.md`: *„Primary control node — only node where commits are
|
||||||
|
made"*).
|
||||||
|
2. **Widoczność `visibility` per strona nie mapuje się na model `homelab-codex`.**
|
||||||
|
`check_okf.py` tego repo ma zamkniętą listę `TYPES` i regułę `POINTER_GLOBS`/
|
||||||
|
`EXCLUDE_DIRS` dopasowaną do tego, co tu jest — dodanie setek stron-encji
|
||||||
|
(firmy, osoby, sprawy) do `kb/` zaszumiłoby indeks tego repo i pomieszałoby
|
||||||
|
dwa różne reżimy publikacji (kb-site publikuje `visibility: public` z `kb/`;
|
||||||
|
`kb-wiki` potrzebuje **własnego**, bo część stron — dane osobowe, umowy,
|
||||||
|
ludzie — nigdy nie powinna nawet teoretycznie wpaść w tę samą ścieżkę co
|
||||||
|
dokumentacja infrastruktury).
|
||||||
|
3. **Rozmiar.** Korpus mailowy to 227k+ kopert; nawet skromna kompilacja
|
||||||
|
(setki stron) to inny rząd wielkości commitów niż `homelab-codex-ws` widział
|
||||||
|
kiedykolwiek. Osobne repo izoluje ten wzrost od repo, które operator
|
||||||
|
przegląda i na którym pracują agenty infrastrukturalne.
|
||||||
|
|
||||||
|
**Doprecyzowanie względem faza3:** szkic nie rozstrzyga *dostępu CC w
|
||||||
|
worktree*. `homelab-codex-ws` ma wzorzec `agent.sh new` → worktree per task,
|
||||||
|
każdy dostaje pełne repo `homelab-codex-ws` sklonowane z gita. `kb-wiki` **nie
|
||||||
|
jest** tym repo — sesja kompilująca stronę potrzebuje **dwóch** klonów
|
||||||
|
jednocześnie: `homelab-codex-ws` (żeby czytać `packages/kb-retrieval`, DSN,
|
||||||
|
`.env` na PIHA) i `kb-wiki` (żeby pisać stronę). To nie jest problem dla
|
||||||
|
worktree pojedynczej sesji CC odpalonej ręcznie na PIHA/SOLARII z dwoma
|
||||||
|
katalogami obok siebie — **jest** problemem dla wzorca `scripts/dev/agent.sh
|
||||||
|
new`, który zna tylko jedno repo. Rekomendacja: kompilacja wiki **nie** idzie
|
||||||
|
przez `agent.sh`-owe worktree `homelab-codex-ws` — to osobny tryb pracy
|
||||||
|
(sesja CC z dwoma katalogami roboczymi, jak dziś narty27 na SOLARII), do
|
||||||
|
zapisania w `kb-wiki/_meta/conventions.md`, nie w tym repo.
|
||||||
|
|
||||||
|
### Publish.sh i public/private per strona
|
||||||
|
|
||||||
|
`kb-site` (`services/kb-site/`) publikuje wyłącznie `kb/**/*.md` z
|
||||||
|
`visibility: public`, fail-closed, przez token w URL (`kb-e2a24af3.okit.pl`).
|
||||||
|
`kb-wiki` leżący poza `kb/` **nie wejdzie** do tego pipeline'u automatycznie —
|
||||||
|
i to dobrze, bo strony wiki (dane o ludziach, umowach, finansach) mają zupełnie
|
||||||
|
inny profil ryzyka publikacji niż dokumentacja infrastruktury. Jeśli/kiedy
|
||||||
|
pojawi się potrzeba publicznej (lub choćby LAN-owej) wystawki `kb-wiki`, to
|
||||||
|
osobny generator + osobny serwis (wzorzec `narty27`: nginx + named volume,
|
||||||
|
`exposure: private` domyślnie), **nie** rozszerzenie `gen_pages.py`. Na tym
|
||||||
|
etapie (proof-of-concept, 3–10 stron) nie ma potrzeby stawiać hostingu w ogóle
|
||||||
|
— `git log`/`cat` na PIHA wystarczy do przeglądu.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Punkt 2 — Bootstrap: analiza korpusu i lista encji zalążkowych
|
||||||
|
|
||||||
|
### Zapytania i wyniki (żywa baza, 2026-08-26)
|
||||||
|
|
||||||
|
**Wolumen per rok/źródło** (`envelope`, pełne dane w §1 wyżej dla świeżości;
|
||||||
|
poniżej rozkład historyczny):
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT source, date_trunc('year', ts) AS yr, count(*) FROM envelope GROUP BY 1,2 ORDER BY 1,2;
|
||||||
|
```
|
||||||
|
|
||||||
|
Szczyt 2011–2012 (~23k/rok — dawny pracodawcy przez `outbox.pl`), potem
|
||||||
|
stabilne 9–13k/rok 2013–2025. 2 559 kopert `ts`=epoch 1970 (daty nieodzyskane,
|
||||||
|
opisane w `kb-overview.md` backlog).
|
||||||
|
|
||||||
|
**Top nadawcy wśród kopert, które realnie mają zaembedowany, nie-wykluczony
|
||||||
|
chunk** (czyli faktycznie trafiają w wyniki `/search` — to jest właściwe sito
|
||||||
|
pod encje, nie surowy wolumen, który zdominowany jest przez newslettery
|
||||||
|
wykluczone z indeksu):
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT h->'from'->>'name', h->'from'->>'address', count(DISTINCT e.id),
|
||||||
|
min(e.ts)::date, max(e.ts)::date
|
||||||
|
FROM envelope e
|
||||||
|
JOIN jsonb_array_elements(e.entities) h ON h->>'type' = 'headers'
|
||||||
|
WHERE EXISTS (SELECT 1 FROM document_chunk c
|
||||||
|
WHERE c.envelope_id = e.id AND c.embedding IS NOT NULL AND c.excluded_reason IS NULL)
|
||||||
|
GROUP BY 1,2 ORDER BY 3 DESC LIMIT 40;
|
||||||
|
```
|
||||||
|
|
||||||
|
| Nadawca | Adres | Koperty | Zakres |
|
||||||
|
|---|---|---|---|
|
||||||
|
| oskar kapala | oskar.kapala@gmail.com | 10 021 | 2005–2020 |
|
||||||
|
| Oskar Kapala | oskar.kapala@outbox.pl | 6 399 | (epoch)–2014 |
|
||||||
|
| Kasia Lorenc-Kapala | katalia@gmail.com | 3 385 | 2007–2026 |
|
||||||
|
| Oskar Kapala | oskar.kapala@gmail.com | 2 910 | 2010–2026 |
|
||||||
|
| System Synergia | robot2@robot.librus.pl | 2 818 | 2018–2026 |
|
||||||
|
| Allegro | powiadomienia@allegro.pl | 2 170 | 2008–2026 |
|
||||||
|
| Tomasz Anuszewski | tomasz.anuszewski@outbox.pl | 1 913 | 2011–2012 |
|
||||||
|
| Dziennik Bankier.pl (×2 adresy) | …bankier.pl | 1 736 + 1 676 | 2007–2014 |
|
||||||
|
| RSW_TECH_TEAM | rsw_tech_team@outbox.pl | 1 628 | 2011–2012 |
|
||||||
|
| Groupon | info@news.groupon.pl | 1 447 | 2011–2012 |
|
||||||
|
| Miron Mironiuk | m@cosmose.co | 1 126 | 2015–2016 |
|
||||||
|
| kontakt@mbank.pl | | 1 099 | 2005–2026 |
|
||||||
|
| Jan Boboli | jan.boboli@outbox.pl | 969 | 2011–2013 |
|
||||||
|
| Paweł Cesar Sanjuan Szklarz | paweld2@gmail.com, pawel@cosmose.co | 757+555+418+376 | 2005–2024 |
|
||||||
|
| InPost | info@paczkomaty.pl | 722 | 2017–2026 |
|
||||||
|
| Bogumil Jakubiak | B.Jakubiak@icm.edu.pl | 686 | 2004–2009 |
|
||||||
|
| sOKratis Liliana Banaszak | liliana.banaszak@sokratis.pl | 679 | 2009–2026 |
|
||||||
|
| Strava | update.strava.com / strava.com | 591+395 | 2017–2026 |
|
||||||
|
|
||||||
|
**Domeny** (§2, po odsianiu marketingu/newsletterów po heurystyce
|
||||||
|
`noreply|notification|robot|mailer`): `outbox.pl` (39,4k — domena prywatna
|
||||||
|
operatora, alias `oskar.kapala@outbox.pl` + koledzy z tej samej domeny =
|
||||||
|
prawdopodobnie dawny pracodawca/hosting), `gmail.com`, `cosmose.co` (2 982,
|
||||||
|
firma — Miron Mironiuk + Paweł Szklarz), `icm.edu.pl`/`students.mimuw.edu.pl`
|
||||||
|
(uczelnia/ICM UW), `mbank.pl`/`bankier.pl` (finanse), `sokratis.pl`.
|
||||||
|
|
||||||
|
**Paperless — 191 kopert, w większości streszczone** (`document_summary`,
|
||||||
|
162 `claude-haiku-4-5` + 155 `gemma3:12b`, prawie 1:1 pokrycie). Próbka
|
||||||
|
streszczeń pokazuje wyraźne klastry tematyczne:
|
||||||
|
|
||||||
|
| Klaster | Liczba (próbka 40 najnowszych) | Przykład |
|
||||||
|
|---|---|---|
|
||||||
|
| FIRST LEGO League / Fundacja Future Minds | 8 | zgody na wizerunek, karty wyników, harmonogramy |
|
||||||
|
| Regulaminy OLX (pakiety, ochrona kupującego, ads) | 5 | umowa serwisowa |
|
||||||
|
| Revolut (krypto, regulaminy, cennik) | 4 | umowa finansowa |
|
||||||
|
| Spółdzielnia Mieszkaniowa „Przy Metrze" | 2 | RODO, dane kontaktowe |
|
||||||
|
| Ubezpieczenia (PZU OC, itp.) | 2 | polisa |
|
||||||
|
| Wynajem auta (Dollar, Madryt) | 2 | rezerwacja |
|
||||||
|
| Faktura (ZigBee, Oskar Kapała Rozwiązania IT — działalność gospodarcza) | 1 | dowód zakupu |
|
||||||
|
|
||||||
|
### Kryterium kolejności
|
||||||
|
|
||||||
|
Trzy sygnały, w tej kolejności ważności:
|
||||||
|
|
||||||
|
1. **Obecność w indeksowanym (nie-wykluczonym) korpusie** — encja musi mieć
|
||||||
|
realne pokrycie w `document_chunk` z wektorem, inaczej strona kompiluje się
|
||||||
|
z pustki. To jedyny twardy filtr (zastosowany w zapytaniu wyżej).
|
||||||
|
2. **Wielowątkowość źródeł** — encje, które łączą paperless (dokument formalny:
|
||||||
|
umowa, polisa, regulamin) *i* gmail (korespondencja o tej samej sprawie),
|
||||||
|
są lepszymi kandydatami na `sprawy`/`umowy` niż encje widoczne tylko w
|
||||||
|
jednym źródle — bo demonstrują dokładnie to, co ma udowodnić wiki-kompilat
|
||||||
|
(Karpathy-wzorzec: strona syntezuje, nie kopiuje jednego dokumentu).
|
||||||
|
3. **Rozstrzygalność, nie wolumen.** `outbox.pl`/`Tomasz Anuszewski`/`Jan
|
||||||
|
Boboli`/`RSW_TECH_TEAM` mają duży wolumen (2011–2012), ale to zamknięty w
|
||||||
|
czasie epizod (prawdopodobnie dawny pracodawca) — dobry kandydat na
|
||||||
|
`podmiot`, zły na pierwszy PoC, bo wymaga ustalenia kontekstu (kim była ta
|
||||||
|
firma), którego recon nie ma. Odłożone na drugą turę, nie na pierwszą.
|
||||||
|
|
||||||
|
### Proponowana lista 10–20 encji zalążkowych
|
||||||
|
|
||||||
|
| # | Strona | Typ | Uzasadnienie (z danych) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | `fll-2025-26` | sprawa | 8/40 streszczeń paperless; żywa sprawa (2026), krzyżuje szkołę + fundację + dziecko |
|
||||||
|
| 2 | `future-minds` (fundacja) | podmiot | organizator FLL, NIP w danych, kontakt jawny w treści dokumentów |
|
||||||
|
| 3 | `spoldzielnia-przy-metrze` | podmiot | nieruchomość, RODO + dane kontaktowe, aktywna (bieżąca korespondencja o pożarówce 2026) |
|
||||||
|
| 4 | `revolut` | podmiot | 4 dokumenty regulaminowe w próbce, konto aktywne (kontekst kryptowalut) |
|
||||||
|
| 5 | `mbank` | podmiot | 1 099 kopert 2005–2026, ciągłość 20 lat, kontakt@mbank.pl + dziennik bankier.pl (powiązana marka) |
|
||||||
|
| 6 | `pzu` | podmiot | kandydat wymieniony wprost w szkicu operatora (§8.1 przykład), polisa OC w paperless |
|
||||||
|
| 7 | `cosmose` (firma) | podmiot | 2 982 kopert, dwóch nazwanych korespondentów (Mironiuk, Szklarz) — test cross-osoba w jednym podmiocie |
|
||||||
|
| 8 | `miron-mironiuk` | osoba | 1 126 kopert, jeden zidentyfikowany adres, powiązanie z `cosmose` |
|
||||||
|
| 9 | `pawel-cesar-sanjuan-szklarz` | osoba | 4 warianty adresu/nazwy (757+555+418+376 kopert) — dobry test entity resolution na aliasach tej samej osoby |
|
||||||
|
| 10 | `librus-synergia` | temat | 2 818 kopert 2018–2026, szkoła dzieci, ciągły strumień — test strony-tematu o dużym, jednorodnym źródle |
|
||||||
|
| 11 | `inpost-paczkomaty` | temat | 722+681 (dwa nadawcy tej samej usługi) — test scalania dwóch adresów jednej usługi w jedną stronę |
|
||||||
|
| 12 | `strava` | temat | 591+395, dwa adresy tej samej usługi, 2017–2026 ciągłe |
|
||||||
|
| 13 | `bogumil-jakubiak` | osoba | ICM UW, 2004–2009 — zamknięty epizod, test strony `status: archived` |
|
||||||
|
| 14 | `wynajem-samochodow` (Dollar/Madryt) | temat | rezerwacja 2026, dwa powiązane dokumenty paperless (potwierdzenie różnymi kanałami tej samej rezerwacji) — test dedupu treści |
|
||||||
|
| 15 | `dzialalnosc-gospodarcza` (Oskar Kapała Rozwiązania IT) | podmiot | faktura w paperless, własna działalność — inny rejestr niż korespondencja prywatna |
|
||||||
|
| 16 | `sokratis` | podmiot | 679 kopert 2009–2026, ciągłość 17 lat, nazwany kontakt (Liliana Banaszak) |
|
||||||
|
| 17 | `allegro` | temat | 2 170 kopert 2008–2026 — najdłuższa ciągła relacja handlowa w korpusie |
|
||||||
|
| 18–20 | (druga tura, po pierwszej weryfikacji) | — | `outbox.pl`/dawny pracodawca po ustaleniu kontekstu; `PZU`↔`mbank` cross-link jako pierwszy test grafu `sprawy` łączących dwa `podmioty` |
|
||||||
|
|
||||||
|
17 pozycji zamiast równych 20 — reszta świadomie zostawiona na drugą turę
|
||||||
|
zamiast dopełniana sztucznie (Bogumil Jakubiak/ICM to jedyny archiwalny/
|
||||||
|
zamknięty przypadek na liście — reszta to żywe encje; warto mieć chociaż jeden
|
||||||
|
przykład `status: archived` w PoC, żeby lint (inwariant 3) miał co sprawdzać
|
||||||
|
na obu stanach od pierwszego dnia).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Punkt 3 — Tryb kompilatora: batch / pipeline / hybryda
|
||||||
|
|
||||||
|
### Co już zdecydowano
|
||||||
|
|
||||||
|
Faza3 §8.2, inwariant 2: *„Kompilację i lint robi CC/zewnętrzne API… Sesja
|
||||||
|
CC/API dostaje wyniki retrievalu dla encji… pisze/aktualizuje stronę,
|
||||||
|
commituje do kb-wiki."* To opisuje **batch przez sesje CC** — potwierdzone też
|
||||||
|
przez operatora w zleceniu tego reconu („Utrzymanie: CC/API — lokalny model za
|
||||||
|
słaby — rozstrzygnięte"). Nie podważam tego kierunku; on jest już przesądzony.
|
||||||
|
Pytanie, które faza3 zostawia otwarte, to **rytm**: czy kompilacja jest
|
||||||
|
wyłącznie ręcznie wyzwalanym batchem, czy dostaje automatyczny trigger, i jak
|
||||||
|
zamyka się pętla zwrotna (inwariant 4: *„dobre odpowiedzi z zapytań wracają do
|
||||||
|
wiki jako nowe strony"*).
|
||||||
|
|
||||||
|
### Trzy tryby, ocenione pod kątem tego, co ta faza faktycznie robi
|
||||||
|
|
||||||
|
**(A) Czysty batch (sesja CC, ręcznie wyzwalana).** Operator albo agent
|
||||||
|
odpala sesję z listą encji, sesja robi N zapytań do `hybrid_retrieve`/
|
||||||
|
`cascade_retrieve`, pisze/aktualizuje strony, commituje. Koszt: **przewidywalny
|
||||||
|
i widoczny w moment wywołania** — tyle, ile ta jedna sesja zużyje tokenów API.
|
||||||
|
Częstotliwość: kiedy operator zdecyduje (np. raz na kilka sesji roboczych KB).
|
||||||
|
|
||||||
|
**(B) Job z pipeline'em eskalacji (per-query, automatyczny).** Każde
|
||||||
|
zapytanie do `kb-query` /search, które trafia próg (`dist`, heurystyki),
|
||||||
|
eskaluje do API i **automatycznie** zapisuje/aktualizuje stronę wiki z
|
||||||
|
odpowiedzi. To bezpośrednio realizuje pętlę zwrotną (inwariant 4) bez udziału
|
||||||
|
człowieka w każdym pojedynczym zapisie.
|
||||||
|
|
||||||
|
**(C) Hybryda: bootstrap batch, utrzymanie pipeline.** Pierwsza fala stron
|
||||||
|
(PoC, potem szersza kompilacja korpusu) idzie trybem (A) — bo trzeba pokryć
|
||||||
|
setki/tysiące encji z pustego stanu, co jest z natury wsadowe i wymaga
|
||||||
|
przeglądu jakości. Po ustabilizowaniu wiki, dopisywanie/aktualizacja idzie
|
||||||
|
trybem (B), zdarzeniowo, przy okazji zapytań `/search`, które i tak przechodzą
|
||||||
|
przez `kb-query`.
|
||||||
|
|
||||||
|
### Rekomendacja: (C), z (B) odłożonym poza zakres tej fazy
|
||||||
|
|
||||||
|
Uzasadnienie:
|
||||||
|
|
||||||
|
1. **Skala nie pozwala na (B) od startu.** `kb-query` dziś **nie robi syntezy
|
||||||
|
odpowiedzi** — to jawnie zapisane w `kb/services/kb-query.md` (*„This is a
|
||||||
|
search API, not chat: no answer synthesis over results, that's phase 5"*).
|
||||||
|
Automatyczny zapis strony z „dobrej odpowiedzi" wymaga najpierw tej
|
||||||
|
syntezy (LLM nad wynikami retrievalu) — a to jest osobny, jeszcze
|
||||||
|
niezbudowany komponent, w planie `kb-m5-faza-mailowa.md` §11 zapisany jako
|
||||||
|
*„Synteza odpowiedzi + polityka eskalacji | faza 5 | po fazie 3/4"*.
|
||||||
|
Innymi słowy: (B) zakłada istnienie warstwy, którą dopiero ta faza ma
|
||||||
|
zbudować. Zaczynanie od (B) to budowanie pipeline'u nad nieistniejącym
|
||||||
|
sygnałem.
|
||||||
|
2. **Bootstrap wymaga przeglądu, nie automatyzacji.** 17 stron z §3 to
|
||||||
|
pierwsza próba entity resolution na korpusie, który nigdy wcześniej nie był
|
||||||
|
tak czytany — pierwsze przebiegi *będą* mylić się (aliasy tej samej osoby
|
||||||
|
pod różnymi adresami, jak `paweld2@gmail.com` vs `pawel@cosmose.co`, patrz
|
||||||
|
#9). Batch z operatorem czytającym diff przed commitem jest tu właściwym
|
||||||
|
trybem; automatyczny zapis bez przeglądu ryzykuje właśnie to, co inwariant
|
||||||
|
3 (lint) ma łapać *po fakcie* — lepiej łapać *przed* pierwszym zapisem.
|
||||||
|
3. **Koszt.** Batch: koszt jednej sesji, znany z góry (faza3 §11 szacuje
|
||||||
|
kompilację 3–5 stron proof jako „pomijalna" wobec ~1.5–4 USD pilota
|
||||||
|
streszczeń). Pipeline per-query: koszt proporcjonalny do ruchu na
|
||||||
|
`kb-query`, dziś zerowego (serwis bez UI logowania, brak ruchu
|
||||||
|
produkcyjnego opisanego w żadnym audycie) — budowanie licznika kosztów pod
|
||||||
|
ruch, którego jeszcze nie ma, jest przedwczesne.
|
||||||
|
|
||||||
|
**Wniosek: w tej fazie — wyłącznie (A)/(C)-batch.** Pipeline eskalacji (B) to
|
||||||
|
konsekwencja *syntezy odpowiedzi*, która sama jest poza zakresem tego reconu
|
||||||
|
(`kb-m5-faza-mailowa.md` §11) — wpisuję go do §8 (plan iteracji) jako **osobny,
|
||||||
|
późniejszy etap**, nie część pierwszego kroku.
|
||||||
|
|
||||||
|
### Trigger i częstotliwość dla batcha
|
||||||
|
|
||||||
|
Rekomendacja: **ręczny**, przywiązany do sesji roboczych KB, nie do ticku
|
||||||
|
ingestu. Uzasadnienie: `kb-ingest`/`kb-mail-sync` tykają bez nadzoru
|
||||||
|
(systemd timer) właśnie dlatego, że są deterministyczne i tanie (lokalny
|
||||||
|
embed). Kompilacja wiki zużywa API i wymaga jakościowego osądu przy każdym
|
||||||
|
zapisie na tym etapie — sprzeczne z „bez nadzoru". Automatyczny trigger „po
|
||||||
|
ticku ingestu" dodałby niekontrolowany koszt API do procesu, który dziś nie ma
|
||||||
|
żadnego budżetu ani limitu (`packages/kb-retrieval`/`kb-query` nie mają
|
||||||
|
pojęcia o kosztach API — nigdzie w repo nie ma licznika wydatków Anthropica).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Punkt 4 — Format linku dowodowego i odporność na re-chunking
|
||||||
|
|
||||||
|
### Co już zdecydowano
|
||||||
|
|
||||||
|
Faza3 §8.2 daje **dokładną składnię**, cytuję w całości bo to bezpośrednia
|
||||||
|
odpowiedź na pytanie zlecenia:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
sources:
|
||||||
|
- envelope_id: paperless:119
|
||||||
|
chunks: [4211, 4213] # document_chunk.id
|
||||||
|
- envelope_id: "CABtrY-...@mail.gmail.com"
|
||||||
|
chunks: [] # koperta bez chunków (np. sam manifest/headers)
|
||||||
|
---
|
||||||
|
|
||||||
|
Treść… składka 1 234 zł/rok [^paperless:119#4212].
|
||||||
|
```
|
||||||
|
|
||||||
|
Dwa poziomy: `sources:` we frontmatterze (agregat, dla lintu — sprawdza czy
|
||||||
|
*strona* ma dowody) i inline `[^envelope_id#chunk_id]` przy każdym fakcie
|
||||||
|
liczbowym (dla spot-checku — sprawdza czy *zdanie* ma dowód). To jest już
|
||||||
|
odpowiedź na „envelope_id vs chunk-id": **oba, na dwóch poziomach
|
||||||
|
granularności**, nie albo-albo.
|
||||||
|
|
||||||
|
### Luka, którą faza3 zostawia: `document_chunk.id` (bigint PK) nie jest stabilny
|
||||||
|
|
||||||
|
To jest realny problem, którego szkic nie adresuje wprost. `document_chunk.id`
|
||||||
|
to `bigint GENERATED … nextval(...)` — PK generowany przy insercie. Re-chunking
|
||||||
|
(zmiana `chunk_size`/`chunk_overlap`, zmiana modelu embeddingu, poprawka
|
||||||
|
chunkera) w obecnym pipeline'u (`jobs/mail-body-ingest`, `kb/phases/kb-m5-faza3.md`
|
||||||
|
§1.6, Decyzja 1) **usuwa i wstawia na nowo** wiersze `document_chunk` dla
|
||||||
|
przetwarzanej koperty — nowe `id`. Strona wiki napisana dziś z przypisem
|
||||||
|
`[^paperless:119#4212]` po re-chunkowaniu paperless wskazuje na `id`, który już
|
||||||
|
nie istnieje.
|
||||||
|
|
||||||
|
Sprawdziłem: nie ma w repo żadnego mechanizmu "kotwiczenia" chunka niezależnego
|
||||||
|
od PK (np. hash treści, offset w dokumencie źródłowym). `chunk_index` (kolumna
|
||||||
|
w `document_chunk`) jest stabilniejszy semantycznie (n-ty fragment tej koperty)
|
||||||
|
ale też **nie** jest gwarantowany stabilny — zmiana `chunk_size` przesuwa
|
||||||
|
granice i renumeruje wszystkie chunki tej koperty.
|
||||||
|
|
||||||
|
### Ocena: to nie jest luka krytyczna, bo lint już ją łapie — ale warto dociągnąć degradację
|
||||||
|
|
||||||
|
Inwariant 3 (lint) faza3 §8.2 już projektuje mechanizm wykrywania: *„spot-check
|
||||||
|
N losowych przypisów `[^...]` vs treść chunka w bazie"*. Martwy `chunk_id` po
|
||||||
|
re-chunkingu to dokładnie to, co ten spot-check złapie — `SELECT text FROM
|
||||||
|
document_chunk WHERE id = 4212` zwróci zero wierszy, lint zgłasza to jako
|
||||||
|
znalezisko w `_meta/lint-reports/`. **Mechanizm wykrywający już istnieje w
|
||||||
|
projekcie**, więc rekomendacja to nie „zbuduj nowy system kotwiczenia" tylko
|
||||||
|
**doprecyzowanie zachowania przy trafieniu martwego linku**:
|
||||||
|
|
||||||
|
**Rekomendacja — dwupoziomowy fallback, nie twardy błąd:**
|
||||||
|
|
||||||
|
1. Link **prymarny** to `[^envelope_id#chunk_id]` — precyzyjny, ale kruchy.
|
||||||
|
2. Gdy `chunk_id` nie istnieje (po re-chunkingu), **fallback do
|
||||||
|
`envelope_id`** — dowód wskazuje na kopertę jako całość ("ten fakt pochodzi
|
||||||
|
z tej wiadomości", zamiast "z tego dokładnie fragmentu"). Zdegradowane, ale
|
||||||
|
wciąż audytowalne, bo `envelope_id` **jest** stabilny (`envelope.id` to
|
||||||
|
Message-ID albo `sha256-…`, nigdy nie zmienia się po insercie —
|
||||||
|
`kb-mail-pillar.md` §3, kontrakt zamrożony).
|
||||||
|
3. Lint raportuje **liczbę zdegradowanych przypisów** jako osobną metrykę w
|
||||||
|
`_meta/lint-reports/` (nie tylko listę zerwanych linków) — to jest wczesny
|
||||||
|
sygnał "re-chunking coś ruszył, strony trzeba przejrzeć", zanim ktokolwiek
|
||||||
|
zauważy to ręcznie.
|
||||||
|
4. **Re-chunking = trigger do przeglądu, nie do automatycznej naprawy.**
|
||||||
|
Automatyczne przepisanie `chunk_id` na nowy najbliższy semantycznie chunk
|
||||||
|
wymagałoby ponownego embeddingu i porównania — kosztowne i ryzykowne (może
|
||||||
|
przypisać fakt do złego fragmentu po cichu). Degradacja do `envelope_id` +
|
||||||
|
ręczny przegląd przy najbliższej kompilacji tej strony jest tańsza i
|
||||||
|
bezpieczniejsza.
|
||||||
|
|
||||||
|
Konsekwencja praktyczna: re-chunking całego korpusu mailowego (227k kopert)
|
||||||
|
jest zdarzeniem rzadkim i świadomym (widziane już raz — Decyzja 1 fazy3,
|
||||||
|
`UNIQUE+model`), nie czymś co dzieje się przy okazji. Skala degradacji przy
|
||||||
|
takim zdarzeniu jest więc *skokowa i rzadka*, nie ciągła — fallback +
|
||||||
|
metryka w locie wystarczą, nie trzeba temu poświęcać osobnej infrastruktury.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Punkt 5 — Anty-propagacja: datowanie, sprzeczności, aktualizacja
|
||||||
|
|
||||||
|
### Co już zdecydowano
|
||||||
|
|
||||||
|
Frontmatter niesie `compiled_at`/`updated_at` (datowanie na poziomie strony).
|
||||||
|
Inwariant 3: lint okresowy łapie *„sprzeczności między stronami (fakty o tej
|
||||||
|
samej encji)"*. Inwariant 4: pętla zwrotna dopisuje nowe strony/aktualizacje z
|
||||||
|
dobrych odpowiedzi zapytań.
|
||||||
|
|
||||||
|
### Luki, które faza3 nie precyzuje i które zlecenie wprost pyta
|
||||||
|
|
||||||
|
**(1) Sekcja „niepewne/sprzeczne" na poziomie strony.** Dziś nic w
|
||||||
|
projektowanym frontmatterze/strukturze nie ma miejsca na "wiem, że dwa źródła
|
||||||
|
mówią co innego, i to jest w rękopisie widoczne". Lint wykrywa sprzeczność
|
||||||
|
*między stronami* — ale co z sprzecznością *wewnątrz* jednej kompilacji (np.
|
||||||
|
paperless:119 podaje jedną kwotę składki, wcześniejszy mail podaje inną —
|
||||||
|
podwyżka, błąd, czy różne polisy)?
|
||||||
|
|
||||||
|
**Rekomendacja:** sekcja `## Niepewne / sprzeczne` jako **konwencja treści**
|
||||||
|
strony (nie pole frontmattera — to jest proza, nie metadane), umieszczana
|
||||||
|
zawsze na końcu strony, gdy występuje. Format:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Niepewne / sprzeczne
|
||||||
|
|
||||||
|
- Składka OC: [^paperless:24#N] podaje kwotę roczną bez liczby, [^<msg-id>#M]
|
||||||
|
(mail 2025-03) wspomina "1 450 zł" — nie jest jasne, czy to ta sama polisa
|
||||||
|
czy poprzedni rok. **Nie rozstrzygane automatycznie** — do potwierdzenia
|
||||||
|
przy następnej kompilacji tej strony.
|
||||||
|
```
|
||||||
|
|
||||||
|
Lint (spot-check) traktuje tę sekcję jako **oczekiwaną**, nie jako defekt —
|
||||||
|
strona bez tej sekcji nie jest "lepsza", strona z niewykrytą sprzecznością w
|
||||||
|
treści głównej (fakt podany jako pewny, gdy dowody się rozjeżdżają) jest
|
||||||
|
gorsza. To jest różnica między *ukrywaniem* niepewności (zły kompilat) a jej
|
||||||
|
*nazwaniem* (dobry kompilat) — i to jest właśnie mechanizm, który ma
|
||||||
|
zapobiegać propagacji błędu: LLM kompilujący wprost pisze "nie wiem", zamiast
|
||||||
|
zgadywać i zamrażać zgadywankę jako fakt.
|
||||||
|
|
||||||
|
**(2) Polityka aktualizacji przy nowym, sprzecznym dowodzie.** Scenariusz ze
|
||||||
|
zlecenia: nowy mail przeczy istniejącej stronie. Dwie opcje:
|
||||||
|
|
||||||
|
- **Nadpisać** stronę nowym stanem (traktować najnowszy dowód jako
|
||||||
|
prawdziwy) — ryzyko: jeśli nowy mail jest błędny/nieaktualny/o innej
|
||||||
|
sprawie, kasuje się poprawną wcześniejszą treść bez śladu.
|
||||||
|
- **Dopisać do „Niepewne/sprzeczne"** i **nie nadpisywać** faktu w treści
|
||||||
|
głównej, dopóki sprzeczność nie zostanie rozstrzygnięta (przez kolejną
|
||||||
|
kompilację z jeszcze jednym dowodem, albo ręcznie przez operatora).
|
||||||
|
|
||||||
|
**Rekomendacja: druga opcja, z jednym wyjątkiem.** Domyślnie *dopisz, nie
|
||||||
|
nadpisuj* — bo strona ma być audytowalnym destylatem, nie najświeższą
|
||||||
|
migawką, a "nowszy mail" nie zawsze znaczy "poprawniejszy" (może być o innej
|
||||||
|
polisie, literówka nadawcy, spam). Wyjątek: gdy nowy dowód jest **tego samego
|
||||||
|
typu i wprost aktualizuje poprzedni** (np. "nowy cennik od 1.09" jawnie
|
||||||
|
zastępujący poprzedni, nie sprzeczny z nim) — to nie jest sprzeczność, to
|
||||||
|
aktualizacja, i idzie do treści głównej z nową datą `updated_at`, a stary fakt
|
||||||
|
przechodzi do sekcji „Historia" (jeśli strona jej potrzebuje) albo po prostu
|
||||||
|
znika, zastąpiony — to jest zwykła kompilacja, nie przypadek antypropagacji.
|
||||||
|
Rozróżnienie "to jest aktualizacja" vs "to jest sprzeczność" robi **sesja
|
||||||
|
kompilująca (CC/API)**, nie automat — zgodnie z inwariantem 2 (kompilacja to
|
||||||
|
zadanie dla modelu, nie dla reguły).
|
||||||
|
|
||||||
|
**(3) Datowanie twierdzeń w treści, nie tylko w frontmatterze.** Rekomendacja
|
||||||
|
dodatkowa: każdy fakt z konkretną datą ważności (cena, status sprawy, "aktualnie
|
||||||
|
w toku") niesie datę **przy fakcie**, nie tylko w `updated_at` strony — bo
|
||||||
|
strona może zbierać fakty z różnych momentów (mail z 2019 o starej umowie +
|
||||||
|
mail z 2026 o nowej), a jedna data na całą stronę zaciera to rozróżnienie.
|
||||||
|
Przykład: *„Składka 1 234 zł/rok (stan na 07.2026) [^paperless:119#4212]."*
|
||||||
|
To jest rozszerzenie konwencji przypisu z §5, nie nowy mechanizm.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Punkt 6 — Integracja z kb-query: retrieval, ryzyko sprzężenia
|
||||||
|
|
||||||
|
### Co już zdecydowano
|
||||||
|
|
||||||
|
Inwariant 5 (faza3 §8.1): *„Wiki wchodzi do retrievalu jako dodatkowe źródło…
|
||||||
|
kaskada: wiki → summary → chunk."* §8.2 rozwija: strona wiki = koperta
|
||||||
|
`source='wiki'`, `id='wiki:<ścieżka>'`, chunkowana i embedowana tym samym
|
||||||
|
jobem, **bez nowego schematu bazy** — reużywa istniejący kontrakt koperty.
|
||||||
|
|
||||||
|
### Co to znaczy konkretnie w kodzie dzisiejszego `packages/kb-retrieval`
|
||||||
|
|
||||||
|
Sprawdziłem `retrieval.py`: dziś są trzy tory (`flat`/`cascade`/`hybrid`), z
|
||||||
|
`cascade_retrieve` jako **dwupoziomowym** (`document_summary` → `document_chunk`)
|
||||||
|
i `hybrid_retrieve` jako cascade + równoległy skan `summaryless_sources`
|
||||||
|
(`gmail`, `fastmail`) scalany po `dist`. **Nie ma dziś trzeciego poziomu.**
|
||||||
|
Realizacja inwariantu 5 wymaga:
|
||||||
|
|
||||||
|
1. Nowej tabeli/kolekcji `document_summary`-podobnej dla wiki, **albo**
|
||||||
|
traktowania stron wiki jak paperless — envelope z summary (skoro strony
|
||||||
|
są krótkie, 1–3 chunki, `document_summary` dla całej strony ma sens
|
||||||
|
semantyczny inaczej niż dla 187k-wierszowego newslettera).
|
||||||
|
2. Nowej funkcji `wiki_retrieve` w `kb_retrieval/retrieval.py`, wołanej
|
||||||
|
**przed** `cascade_retrieve`, z wynikiem scalanym analogicznie do
|
||||||
|
`hybrid_retrieve` (ten sam embedder, ten sam cosine space — merge to sort,
|
||||||
|
nie renormalizacja, dokładnie jak dziś).
|
||||||
|
3. Zmiany w `search.py`/`app/main.py` `kb-query`, żeby nowy tryb (`mode`)
|
||||||
|
albo rozszerzenie `hybrid` obejmował wiki — i w `app/links.py`
|
||||||
|
(`build_result`) żeby wynik ze źródła `wiki` miał sensowny `link`
|
||||||
|
(`raw_ref` do pliku w `kb-wiki`, nie URL — bo `kb-wiki` nie jest hostowane
|
||||||
|
publicznie, patrz §2).
|
||||||
|
|
||||||
|
To jest **realna zmiana kodu** w serwisie produkcyjnym (`kb-query`), nie tylko
|
||||||
|
w nowym repo `kb-wiki` — warto to mieć jawnie w planie iteracji (§8), bo
|
||||||
|
inwariant 5 bez tego pozostaje deklaracją, nie działaniem.
|
||||||
|
|
||||||
|
### Ryzyko sprzężenia (self-citation / citogenesis) — luka w szkicu faza3
|
||||||
|
|
||||||
|
Zlecenie pyta wprost o to ryzyko i faza3 **go nie adresuje**. Mechanizm
|
||||||
|
ryzyka: kompilacja strony X czyta wyniki retrievalu (inwariant 2) → jeśli
|
||||||
|
retrieval w tym momencie już przeszukuje `source='wiki'` (inwariant 5, w pełni
|
||||||
|
wdrożony) → strona X może dostać jako "dowód" **inną stronę wiki**, która sama
|
||||||
|
została skompilowana z niepewnych/błędnych przesłanek → błąd się utrwala i
|
||||||
|
**wzmacnia** zamiast zanikać, bo kolejna kompilacja cytuje już nie surowy
|
||||||
|
mail, tylko wcześniejszą (błędną) syntezę, z pozorem niezależnego
|
||||||
|
potwierdzenia. To jest dokładnie mechanizm Wikipedia-citogenesis, przeniesiony
|
||||||
|
na kompilator jednoosobowy.
|
||||||
|
|
||||||
|
**Rekomendacja — rozdzielić dwie ścieżki retrievalu, które dziś inwariant 5
|
||||||
|
zlewa w jedną:**
|
||||||
|
|
||||||
|
1. **Retrieval na potrzeby `/search` (użytkownik, faza 5 synteza
|
||||||
|
odpowiedzi)** — tu wiki **powinna** wchodzić do kaskady (inwariant 5 ma
|
||||||
|
rację: skompilowana wiedza jest cenniejsza niż surowy chunk dla kogoś, kto
|
||||||
|
pyta "co wiem o PZU").
|
||||||
|
2. **Retrieval na potrzeby *kompilacji* nowej/aktualizowanej strony (sesja
|
||||||
|
CC/API pisząca `kb-wiki`)** — tu retrieval **musi** wykluczać
|
||||||
|
`source='wiki'` (parametr `exclude_sources=('wiki',)` w
|
||||||
|
`cascade_retrieve`/`hybrid_retrieve`, jedna linia zmiany analogiczna do
|
||||||
|
dzisiejszego `summaryless_sources`). Kompilacja zawsze czyta wyłącznie
|
||||||
|
warstwę dowodową (mail, paperless — evidence layer z ustaleń wyjściowych
|
||||||
|
operatora), nigdy inne strony wiki jako źródło faktów. Strony **mogą**
|
||||||
|
linkować się nawzajem przez `[[...]]` (graf, nawigacja) — ale to nie jest
|
||||||
|
to samo co "cytowanie jako dowód faktu"; link `[[pzu]]` w treści strony
|
||||||
|
`polisa-oc-auto` to odsyłacz do powiązanej strony, nie przypis źródłowy —
|
||||||
|
przypisy źródłowe (`[^...]`) zawsze wskazują `envelope_id`, nigdy
|
||||||
|
`wiki:...`.
|
||||||
|
|
||||||
|
Ten podział jest tani do wymuszenia (jeden parametr) i eliminuje ryzyko przy
|
||||||
|
źródle, zamiast polegać wyłącznie na lincie żeby je złapać po fakcie.
|
||||||
|
Rekomendacja: dopisać to jako **inwariant 7** przy zatwierdzaniu planu, nie
|
||||||
|
jako implementacyjny szczegół — to jest decyzja architektoniczna tej samej
|
||||||
|
wagi co inwarianty 1–6 z faza3 §8.1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Punkt 7 — Zależności i kolizje
|
||||||
|
|
||||||
|
### Multiagent recon / dyspozytor
|
||||||
|
|
||||||
|
Sprawdzone bezpośrednio w `kb/subsystems/recon-multiagent.md` i potwierdzone
|
||||||
|
przez `kb/audits/mail-sync-2026-08-06.md` §5 (który zrobił dokładnie to samo
|
||||||
|
ćwiczenie dla przyrostówki): **dyspozytor subsystemu B nie istnieje jeszcze w
|
||||||
|
kodzie** — jest zdefiniowany jako *osobny projekt* w
|
||||||
|
`kb/decisions/architektura-2026-07-28.md` (*„B — do-the-work: dyspozytor zadań
|
||||||
|
(agent)… Osobny wysiłek, osobny projekt"*), planowany na PIHA. `kb-query`
|
||||||
|
występuje w reconie multiagentowym wyłącznie jako wiersz inwentarza, poza
|
||||||
|
zakresem control-plane/Telegram.
|
||||||
|
|
||||||
|
**Wniosek: zero zależności blokujących w żadną stronę**, z tym samym
|
||||||
|
zastrzeżeniem, jakie mail-sync-recon już zanotował dla siebie: nowy automat
|
||||||
|
(tu: ewentualny periodyczny lint albo — gdyby kiedyś powstał — pipeline
|
||||||
|
eskalacji z §4) **dokłada się do „shadow set"** bytów poza GitOps-ową detekcją
|
||||||
|
dryfu (`kb-ingest`, `stability-agent`, teraz `kb-mail-sync`, potencjalnie
|
||||||
|
`kb-wiki`-lint). Nie blokuje nic dzisiaj; warto, żeby trafiło świadomie do
|
||||||
|
otwartego pytania nr 5 tamtego reconu, a nie przez przeoczenie — dokładnie tak
|
||||||
|
samo jak zanotował to mail-sync-recon dla siebie.
|
||||||
|
|
||||||
|
**Telegram/approval flow** (`CLAUDE.md` „Action approval flow") dotyczy
|
||||||
|
wyłącznie akcji `container_restart`/`redeploy`/`disk_cleanup`/`alert_only` z
|
||||||
|
control-plane. Kompilacja wiki nie generuje żadnej z tych akcji — nie ma i nie
|
||||||
|
powinno być punktu styku. Jedyny sensowny alert, gdyby faza 5 dostała
|
||||||
|
automatyzację (§4 tryb B, poza zakresem tej fazy), byłby analogiczny do
|
||||||
|
`KbMailSyncStale`: "kompilator/lint nie chodzi", nie coś wymagające approvalu
|
||||||
|
operatora.
|
||||||
|
|
||||||
|
### kb-site publish
|
||||||
|
|
||||||
|
Rozstrzygnięte w §2: `kb-wiki` leży poza `kb/**/*.md`, więc `gen_pages.py`/
|
||||||
|
`publish.sh` **nie widzą go w ogóle** — zero kolizji, bo zero styku. Gdyby w
|
||||||
|
przyszłości ktoś położył treść `kb-wiki` wewnątrz `kb/` (odwrotnie od
|
||||||
|
rekomendacji §2), *wtedy* powstałaby realna kolizja: `gen_pages.py --check`
|
||||||
|
skanuje wygenerowany output pod kątem wycieków (IP, tokeny, ścieżki hosta) —
|
||||||
|
ale nie skanuje pod kątem **danych osobowych** (PESEL, adresy, numery umów),
|
||||||
|
bo nie taki był jego cel. To jest dodatkowy, mocny argument za §2 (osobne
|
||||||
|
repo): trzymanie wiki-encji poza `kb/` eliminuje całą tę klasę ryzyka
|
||||||
|
strukturalnie, zamiast polegać na tym, żeby nikt nigdy nie oznaczył strony o
|
||||||
|
PZU jako `visibility: public` przez pomyłkę.
|
||||||
|
|
||||||
|
### Follow-upy fazy mailowej — czy brudne chunki psują kompilację
|
||||||
|
|
||||||
|
Z `kb/incidents/2026-08-26-mail-sync-20-dni-ciszy.md` §6 (`Nie w zakresie tej
|
||||||
|
naprawy`, bez zmian od 08-06): **charset/mojibake w co najmniej 12 kopertach**
|
||||||
|
oraz **NUL w nagłówkach `jsonb`** pozostają otwarte. Oceniam wpływ na
|
||||||
|
wiki-kompilat:
|
||||||
|
|
||||||
|
- **Skala jest znikoma** — 12 kopert na 227 576 (≈0,005%). Statystycznie
|
||||||
|
prawdopodobieństwo, że pierwsza fala 17 stron z §3 natrafi akurat na jedną
|
||||||
|
z tych 12, jest bliskie zeru (żadna z zidentyfikowanych encji §3 nie
|
||||||
|
pochodzi z okresu/nadawcy powiązanego ze znanymi przypadkami mojibake wg
|
||||||
|
audytu 08-06).
|
||||||
|
- **Mechanizm szkody, gdyby trafiła:** kompilująca sesja CC dostaje chunk z
|
||||||
|
zepsutym kodowaniem jako "dowód" — najgorszy scenariusz to nie błędny fakt
|
||||||
|
(model raczej rozpozna nieczytelny tekst i pominie/zaznaczy niepewność, patrz
|
||||||
|
§6 wyżej), tylko **marnotrawstwo** — chunk wygląda jak szum, model go
|
||||||
|
ignoruje, dowód dla realnego faktu ginie z pola widzenia.
|
||||||
|
- **NUL w `entities` jsonb jest poważniejszy strukturalnie** — jeśli sesja
|
||||||
|
kompilująca odpytuje `entities` bezpośrednio (nie tylko `document_chunk.text`),
|
||||||
|
NUL bajt w jsonb może wywrócić parser po stronie klienta (Pythonowy
|
||||||
|
`json`/`asyncpg` różnie reaguje na `` w zależności od ścieżki). To
|
||||||
|
**nie zostało zweryfikowane w tym reconie** (nie testowałem odczytu takiego
|
||||||
|
wiersza) — flaguję jako `[do weryfikacji]`, nie jako potwierdzony problem.
|
||||||
|
|
||||||
|
**Rekomendacja: nie blokować pierwszego etapu na R1–R3 z §6 incydentu**
|
||||||
|
(charset/NUL) — skala nie uzasadnia opóźnienia PoC. Warto natomiast, żeby
|
||||||
|
lint (inwariant 3) przy pierwszym uruchomieniu zrobił **jednorazowy skan**
|
||||||
|
`document_chunk`/`envelope.entities` pod kątem znaków zastępczych
|
||||||
|
(`<60>`) i `` w źródłach, które faktycznie zasiliły PoC (17 stron
|
||||||
|
§3) — tanie zabezpieczenie, żeby nie odkryć problemu dopiero przy spot-checku
|
||||||
|
tygodnie później.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Punkt 8 — Plan iteracji
|
||||||
|
|
||||||
|
Wzorzec: faza mailowa (`kb-m5-faza-mailowa.md` §10 „Plan implementacji" +
|
||||||
|
§12 kryterium ukończenia per krok) i mail-sync-recon (§6 „Podsumowanie i zakres
|
||||||
|
pracy" — tabela kroków z rodzajem zmiany). Stosuję ten sam format: kroki z
|
||||||
|
zależnościami, DoD per krok, pierwszy etap ograniczony do 1–2 sesji.
|
||||||
|
|
||||||
|
### Etap 0 — Zamknięcie decyzji (ten dokument + zatwierdzenie, 0 sesji kodu)
|
||||||
|
|
||||||
|
Operator przyklepuje §11 (Decyzje) poniżej. Bez tego nic dalej nie rusza —
|
||||||
|
identyczny wzorzec jak Decyzje (a)-(g) w mail-sync-recon, które **zatwierdzono
|
||||||
|
w całości** przed implementacją (`kb-m5-faza-mailowa.md` nagłówek statusu).
|
||||||
|
|
||||||
|
### Etap 1 — Szkielet `kb-wiki` + 3 strony proof (**cel: 1 sesja**)
|
||||||
|
|
||||||
|
> **Wykonany 2026-08-27 — z zastrzeżeniem: ten audyt nie wykrył, że proof-of-concept
|
||||||
|
> (5 stron) już istniał od 2026-07-21 na Forgejo (`oskar/kb-wiki`, poza zestawem
|
||||||
|
> dowodów tego reconu). Pełne rozliczenie i rekoncyliacja: `kb/phases/kb-m5-faza5-wiki.md`.**
|
||||||
|
|
||||||
|
Zakres, celowo mniejszy niż faza3 §8.2 (3–5 stron) i mniejszy niż lista z §3
|
||||||
|
(17 encji) — pierwszy etap ma zweryfikować *mechanizm*, nie pokryć korpus:
|
||||||
|
|
||||||
|
| # | Element | DoD |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | Repo `kb-wiki` (git, poza `homelab-codex-ws`), struktura katalogów wg faza3 §8.2, `_meta/conventions.md` (format frontmattera + inline-przypisów z §5, w tym reguła fallback z §5, sekcja „Niepewne/sprzeczne" z §6, rozdział retrievalu z §7 inwariant 7) | Repo istnieje, `conventions.md` kompletny, czytelny bez kontekstu tej sesji |
|
||||||
|
| 2 | Walidator `kb-wiki/check_okf.py` — wariant lekki, wzorowany na `~/narty-2027/saalbach-kb/check_okf.py` (frontmatter parsowalny + `type` niepuste), rozszerzony o sprawdzenie bloku `sources` (niepusty, każdy `envelope_id` istnieje w bazie — może wymagać `KB_DSN`) | `python3 check_okf.py` PASS na repo z krokiem 3 |
|
||||||
|
| 3 | **3 strony proof**, kompilowane ręcznie sesją CC z wyników `hybrid_retrieve`/`cascade_retrieve` na encjach #1 (`fll-2025-26`), #5 (`mbank`), #9 (`pawel-cesar-sanjuan-szklarz`) z §3 — świadomie zróżnicowane typy (sprawa/podmiot/osoba-z-aliasami) i źródła (paperless/gmail-długi-ciąg/gmail-multi-adres) | Każda strona: frontmatter zgodny z konwencją, ≥1 przypis inline, `sources:` niepuste, przechodzi walidator |
|
||||||
|
| 4 | Ręczny spot-check (inwariant 3, wersja manualna — pełny lint to Etap 2) — operator albo agent czyta 3 strony vs źródłowe chunki, potwierdza brak halucynacji | Zapisany w `_meta/lint-reports/2026-08-XX.md` — pierwszy raport, nawet jeśli ręczny |
|
||||||
|
|
||||||
|
**Dlaczego te trzy, nie 17:** `pawel-cesar-sanjuan-szklarz` (#9) testuje
|
||||||
|
entity resolution na aliasach — jeśli to zawiedzie, cała reszta listy z §3
|
||||||
|
(gdzie kilka pozycji ma podobny problem, np. `cosmose`↔#7/#8) wymaga
|
||||||
|
przeprojektowania konwencji *zanim* zainwestuje się w kolejne 14 stron.
|
||||||
|
Trzy strony, świadomie różne, są tańszym testem niż siedemnaście podobnych.
|
||||||
|
|
||||||
|
### Etap 2 — Lint automatyczny + rozszerzenie do pełnej listy §3 (**2. sesja**)
|
||||||
|
|
||||||
|
Zależy od Etapu 1 (mechanizm zweryfikowany). Zakres:
|
||||||
|
|
||||||
|
1. `kb-wiki/lint.py` — implementacja inwariantu 3 w kodzie (nie ręcznie):
|
||||||
|
linki `[[...]]` do nieistniejących stron, strony-sieroty, spot-check N
|
||||||
|
losowych przypisów (zapytanie do `document_chunk` po `id`, fallback do
|
||||||
|
`envelope_id` z §5 gdy `id` martwy — z metryką degradacji), sprzeczności
|
||||||
|
między stronami (heurystyka: te same encje w `sources:` różnych stron z
|
||||||
|
rozbieżnymi liczbami przy tym samym typie faktu — pełna detekcja
|
||||||
|
semantyczna to zadanie dla CC, nie dla skryptu; skrypt robi tylko
|
||||||
|
pre-filtr kandydatów).
|
||||||
|
2. Rozszerzenie o pozostałe encje z §3 (do 17), plus jednorazowy skan pod
|
||||||
|
kątem `<60>`/`` (§8 „follow-upy fazy mailowej") na źródłach, które
|
||||||
|
faktycznie zasiliły te strony.
|
||||||
|
3. **DoD etapu**: `lint.py` uruchamiany ręcznie (nie timer — zgodnie z §4,
|
||||||
|
automatyzacja poza zakresem), raport w `_meta/lint-reports/`, zero
|
||||||
|
krytycznych znalezisk nierozwiązanych (martwe linki, sieroty) — degradacje
|
||||||
|
chunk-id i sekcje „niepewne" to oczekiwany, nie błędny stan.
|
||||||
|
|
||||||
|
### Poza zakresem pierwszych dwóch etapów (świadomie odłożone)
|
||||||
|
|
||||||
|
| Temat | Gdzie wraca |
|
||||||
|
|---|---|
|
||||||
|
| Integracja `source='wiki'` w `packages/kb-retrieval` (§7, inwariant 5 + 7) | Etap 3 — wymaga działającego korpusu wiki (≥Etap 2) zanim ma sens dodawać go do retrievalu produkcyjnego |
|
||||||
|
| Synteza odpowiedzi (`/search` → odpowiedź w naturalnym języku) | Osobna faza, `kb-m5-faza-mailowa.md` §11 już to tak kotwiczy |
|
||||||
|
| Pipeline eskalacji per-query (tryb B z §4) | Po syntezie odpowiedzi — bez niej nie ma sygnału do eskalacji |
|
||||||
|
| Automatyzacja lintu (timer) | Po Etapie 2, gdy wiadomo ile realnie kosztuje jeden przebieg |
|
||||||
|
| Ewentualny hosting/publikacja `kb-wiki` | Nie zaplanowane — patrz §2, dziś brak potrzeby |
|
||||||
|
| Druga tura encji (#18-20 z §3, `outbox.pl`/dawny pracodawca) | Po ustaleniu kontekstu firmy — osobne mini-śledztwo |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Punkt 9 — Decyzje operatora
|
||||||
|
|
||||||
|
> **Status: (a)-(h) ZATWIERDZONE przez operatora w całości 2026-08-27, bez
|
||||||
|
> zmian** — wzorzec identyczny jak decyzje (a)-(g) w `mail-sync-2026-08-06.md`
|
||||||
|
> (zatwierdzone 2026-08-06 w całości, patrz `kb-m5-faza-mailowa.md` nagłówek
|
||||||
|
> statusu).
|
||||||
|
|
||||||
|
Wzorzec (a)-(g) z `kb/audits/mail-sync-2026-08-06.md` §4.
|
||||||
|
|
||||||
|
### (a) Wersja OKF dla `kb-wiki`: pin v0.1 (jak pilot) vs start od razu na v0.2
|
||||||
|
|
||||||
|
**Rekomendacja: pin v0.1, z migracją zaplanowaną, nie natychmiastową.**
|
||||||
|
|
||||||
|
- Pilot narty27 **potwierdził w praktyce**, że pinowanie v0.1 działa i że
|
||||||
|
zmiana jest tania: `docs/sessions/2026-07-31-kb-f4-final-narty27.md` —
|
||||||
|
*„OKF v0.1 działa w praktyce, a pinowanie wersji okazało się słuszne — spec
|
||||||
|
ewoluuje (v0.2: `timestamp` → `generated: {by, at}`, provenance
|
||||||
|
first-class). Przy fazie 5 rozważyć start od razu na v0.2 albo pin v0.1 z
|
||||||
|
zaplanowaną migracją."* To jest bezpośrednia rekomendacja z pilota, cytowana
|
||||||
|
w zleceniu tego reconu.
|
||||||
|
- **Ale**: `kb-wiki` ma silniejszą potrzebę provenance niż narty27 (który jest
|
||||||
|
planowaniem wyjazdu, nie audytowalnym kompilatem nad wrażliwym korpusem).
|
||||||
|
Provenance first-class w v0.2 (`generated: {by, at}`) pokrywa się częściowo
|
||||||
|
z tym, co faza3 §8.2 już projektuje osobno (`compiled_by`/`compiled_at` w
|
||||||
|
`sources`-owym frontmatterze) — czyli **potrzeba, którą v0.2 adresuje,
|
||||||
|
została już zaadresowana lokalną konwencją**, niezależnie od wersji OKF.
|
||||||
|
To osłabia argument "musimy mieć v0.2 od razu", bo pole `compiled_by`
|
||||||
|
robi tę samą robotę.
|
||||||
|
- Decydujący argument za v0.1: **`homelab-codex` samo jest dziś na v0.1**
|
||||||
|
(`scripts/kb/check_okf.py`, `PINNED_OKF = "0.1"`). Gdyby `kb-wiki` (osobne
|
||||||
|
repo, ale koncepcyjnie ta sama rodzina dokumentów co `kb/`) startował na
|
||||||
|
v0.2, powstałyby **dwie różne wersje specyfikacji w tym samym ekosystemie
|
||||||
|
narzędzi** — dwa parsery, dwie reguły walidacji, mylące przy przenoszeniu
|
||||||
|
wzorców między repo (dokładnie to, co uzasadnia rekomendację pilota "pin +
|
||||||
|
zaplanowana migracja" zamiast "zawsze najnowsza wersja"). Spójność z resztą
|
||||||
|
ekosystemu waży więcej niż wyprzedzenie o jedną wersję specyfikacji, którą
|
||||||
|
i tak trzeba będzie kiedyś zmigrować w `kb/` też.
|
||||||
|
- **Migracja zaplanowana**: gdy `homelab-codex`/`kb/` migruje na v0.2 (osobna
|
||||||
|
decyzja, poza zakresem tego reconu), `kb-wiki` migruje w tym samym oknie —
|
||||||
|
jedna zamiana pola (`timestamp`→`generated: {by,at}`), zgodnie z oceną
|
||||||
|
pilota że to tania zmiana.
|
||||||
|
|
||||||
|
### (b) Repo `kb-wiki` — potwierdzenie D7 z faza3
|
||||||
|
|
||||||
|
**Rekomendacja: potwierdzić bez zmian** (§2). Jedyne doprecyzowanie: kompilacja
|
||||||
|
nie idzie przez `agent.sh`-owy worktree — osobny tryb pracy z dwoma klonami
|
||||||
|
obok siebie, do zapisania w `kb-wiki/_meta/conventions.md`.
|
||||||
|
|
||||||
|
### (c) Tryb kompilatora — potwierdzenie batch/hybryda, pipeline eskalacji poza zakres
|
||||||
|
|
||||||
|
**Rekomendacja:** (§4) batch (CC/API, ręczny trigger) dla bootstrapu i
|
||||||
|
utrzymania w tej fazie; pipeline eskalacji per-query (tryb B) świadomie
|
||||||
|
odłożony do momentu, gdy istnieje synteza odpowiedzi w `kb-query` — bez niej
|
||||||
|
nie ma sygnału, który miałby eskalować.
|
||||||
|
|
||||||
|
### (d) Fallback chunk-id → envelope_id przy re-chunkingu
|
||||||
|
|
||||||
|
**Rekomendacja:** (§5) dwupoziomowy link z degradacją do `envelope_id`, lint
|
||||||
|
raportuje liczbę zdegradowanych przypisów jako osobną metrykę, re-chunking
|
||||||
|
traktowany jako trigger do ręcznego przeglądu, nie do automatycznej naprawy.
|
||||||
|
|
||||||
|
### (e) Sekcja „Niepewne/sprzeczne" i polityka aktualizacji — dopisać do konwencji
|
||||||
|
|
||||||
|
**Rekomendacja:** (§6) sekcja jako konwencja treści (nie pole frontmattera),
|
||||||
|
domyślna polityka „dopisz, nie nadpisuj" przy sprzecznym nowym dowodzie,
|
||||||
|
wyjątek dla jawnych aktualizacji tego samego faktu, rozróżnienie robi sesja
|
||||||
|
kompilująca.
|
||||||
|
|
||||||
|
### (f) Inwariant 7 — rozdział retrievalu kompilacji od retrievalu zapytań
|
||||||
|
|
||||||
|
**Rekomendacja:** (§7) dopisać do inwariantów faza3 §8.1 jako inwariant 7:
|
||||||
|
kompilacja strony wiki **nigdy** nie czyta `source='wiki'` jako dowód
|
||||||
|
(`exclude_sources=('wiki',)`), tylko `/search` (warstwa użytkownika, po
|
||||||
|
zbudowaniu syntezy odpowiedzi) widzi wiki w kaskadzie. Mitygacja
|
||||||
|
self-citation/citogenesis przy źródle, nie tylko przez lint po fakcie.
|
||||||
|
|
||||||
|
### (g) Zakres i kolejność Etapu 1 — 3 strony proof, nie 17
|
||||||
|
|
||||||
|
**Rekomendacja:** (§9) Etap 1 = mechanizm (3 strony celowo zróżnicowane:
|
||||||
|
sprawa/podmiot/osoba-z-aliasami), Etap 2 = skala (do 17) + lint w kodzie.
|
||||||
|
Rozszerzenie listy encji (#18-20, `outbox.pl`) odłożone do osobnego
|
||||||
|
mini-śledztwa kontekstu firmy.
|
||||||
|
|
||||||
|
### (h) Follow-upy fazy mailowej (charset/NUL) — nie blokują, jednorazowy skan przy Etapie 2
|
||||||
|
|
||||||
|
**Rekomendacja:** (§8) nie opóźniać PoC na R1–R3 z incydentu 20-dni-ciszy;
|
||||||
|
dopisać jednorazowy skan `<60>`/`` do zakresu Etapu 2 (lint), nie jako
|
||||||
|
osobny, wcześniejszy krok.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Podsumowanie
|
||||||
|
|
||||||
|
Korpus mailowy dogonił dziś — blokada z `kb-m5-faza3.md` §8.1 jest zdjęta
|
||||||
|
(§1). Architektura wiki-kompilatu jest w dużej mierze **już zaprojektowana i
|
||||||
|
zatwierdzona** (`kb-m5-faza3.md` §8, decyzja 2026-07-15/17) — ten recon
|
||||||
|
weryfikuje ją na aktualnym stanie repo, dociąga trzy realne luki, których
|
||||||
|
tamten szkic nie adresował (**chunk-id vs re-chunking** §5, **self-citation
|
||||||
|
przy retrievalu kompilacyjnym** §7 — nowy inwariant 7, **sekcja
|
||||||
|
niepewne/sprzeczne** §6), i daje 17-pozycyjną listę encji zalążkowych
|
||||||
|
wyprowadzoną z realnych SELECT-ów na żywym korpusie zamiast z trzech
|
||||||
|
przykładów w szkicu (§3). Rekomendowany pierwszy etap to **3 strony proof w
|
||||||
|
1 sesji** (świadomie mniej niż faza3 §8.2 sugerowała, bo pierwszy test ma
|
||||||
|
zweryfikować mechanizm entity-resolution na trudnym przypadku — aliasach —
|
||||||
|
zanim zainwestuje się w skalę), z jawnym odłożeniem pipeline'u eskalacji i
|
||||||
|
integracji retrievalu produkcyjnego do kolejnych etapów.
|
||||||
|
|
||||||
|
Decyzje: (a)-(h) w §10 — **ZATWIERDZONE przez operatora w całości 2026-08-27,
|
||||||
|
bez zmian.**
|
||||||
|
|
@ -3,7 +3,7 @@ okf: "0.1"
|
||||||
type: phase
|
type: phase
|
||||||
visibility: private
|
visibility: private
|
||||||
status: active
|
status: active
|
||||||
updated: 2026-07-17
|
updated: 2026-08-27
|
||||||
links: []
|
links: []
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -628,6 +628,17 @@ Adapter i embed są już idempotentne — nowość to wyłącznie orkiestracja i
|
||||||
> półprodukt kompilacji) i PO filtrze śmieciowych chunków. Pełna wiki po fazie mailowej
|
> półprodukt kompilacji) i PO filtrze śmieciowych chunków. Pełna wiki po fazie mailowej
|
||||||
> (przyrostówka) — wcześniej kompilat byłby fotografią przeszłości.
|
> (przyrostówka) — wcześniej kompilat byłby fotografią przeszłości.
|
||||||
|
|
||||||
|
**Inwariant 7 (dodany 2026-08-27 — rozszerzenie, nie zmiana inwariantów 1–6
|
||||||
|
powyżej, które pozostają NIE podlega zmianie):** decyzja (f),
|
||||||
|
`kb/audits/wiki-kompilat-recon-2026-08-26.md` §10, zatwierdzona przez operatora
|
||||||
|
w całości 2026-08-27. Kompilacja strony wiki **nigdy** nie czyta `source='wiki'`
|
||||||
|
jako dowodu (`exclude_sources=('wiki',)` w `cascade_retrieve`/`hybrid_retrieve`) —
|
||||||
|
retrieval na potrzeby kompilacji zawsze wyklucza wiki, czyta wyłącznie warstwę
|
||||||
|
dowodową (mail, paperless). Tylko `/search` (warstwa użytkownika, po zbudowaniu
|
||||||
|
syntezy odpowiedzi — inwariant 5, faza 5) widzi wiki w kaskadzie. Mitygacja
|
||||||
|
self-citation/citogenesis przy źródle retrievalu, nie tylko przez lint (inwariant
|
||||||
|
3) po fakcie — patrz audyt §7 dla pełnego rozumowania.
|
||||||
|
|
||||||
### 8.2 Rozwinięcie wykonawcze (szczegóły, decyzje architektoniczne bez zmian)
|
### 8.2 Rozwinięcie wykonawcze (szczegóły, decyzje architektoniczne bez zmian)
|
||||||
|
|
||||||
**Repozytorium** (rozstrzygnięcie punktu 6 szkicu): osobne repo `kb-wiki` — decyzja 7,
|
**Repozytorium** (rozstrzygnięcie punktu 6 szkicu): osobne repo `kb-wiki` — decyzja 7,
|
||||||
|
|
|
||||||
322
kb/phases/kb-m5-faza5-wiki.md
Normal file
322
kb/phases/kb-m5-faza5-wiki.md
Normal file
|
|
@ -0,0 +1,322 @@
|
||||||
|
---
|
||||||
|
okf: "0.1"
|
||||||
|
type: phase
|
||||||
|
visibility: private
|
||||||
|
status: active
|
||||||
|
updated: 2026-08-27
|
||||||
|
links:
|
||||||
|
- ../audits/wiki-kompilat-recon-2026-08-26.md
|
||||||
|
- kb-m5-faza3.md
|
||||||
|
- kb-m5-faza-mailowa.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Moduł 5, faza 5 — wiki-kompilat (Etap 1: bootstrap + proof, i **odkrycie rozjazdu**)
|
||||||
|
|
||||||
|
> **Status: Etap 1 wykonany 2026-08-27 — ale w cieniu ważniejszego znaleziska: proof-of-concept
|
||||||
|
> wiki-kompilatu (repo `kb-wiki`, 5 stron) już istniał od **2026-07-21**, na Forgejo
|
||||||
|
> (`https://forgejo.kapala.org/oskar/kb-wiki`), i `kb/audits/wiki-kompilat-recon-2026-08-26.md`
|
||||||
|
> tego nie wykrył. Ta sesja (2026-08-27) zbudowała **niezależny, równoległy** lokalny bootstrap
|
||||||
|
> (`~/kb-wiki-etap1-sesja-2026-08-27/`, 3 strony) nie wiedząc o repo z lipca — dowiedziała się
|
||||||
|
> dopiero przy Części C tego zadania, kiedy próba udokumentowania "remote nie istnieje" (cytat z
|
||||||
|
> polecenia sesji, zgodny z audytem) skłoniła do sprawdzenia Forgejo wprost. **Nic nie zostało
|
||||||
|
> wypchnięte do prawdziwego zdalnego repo** — ta sesja tylko czytała je (`git clone`, pull-only).
|
||||||
|
>
|
||||||
|
> **Rekoncyliacja domknięta 2026-08-27 (operator zdecydował: opcja 1 z §4 —
|
||||||
|
> scalić).** `kb-wiki` ma teraz **7 stron**: 5 lipcowych + `fll-2025-26`
|
||||||
|
> scalone (dwie niezależne kompilacje tej samej encji) + `mbank`/
|
||||||
|
> `pawel-cesar-sanjuan-szklarz` przeniesione bez kolizji. Cztery commity na
|
||||||
|
> `master` **lokalnie, nie pushnięte** — czekają na review operatora przed
|
||||||
|
> pushem do Forgejo. Pełne rozliczenie: §6.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Jak doszło do odkrycia (żeby przyszła sesja nie powtórzyła tej samej luki)
|
||||||
|
|
||||||
|
`kb/audits/wiki-kompilat-recon-2026-08-26.md` (2026-08-26, decyzje a-h zatwierdzone
|
||||||
|
2026-08-27) stwierdza wprost w nagłówku: *„Nic nie zostało zaimplementowane,
|
||||||
|
zdeployowane ani skommitowane poza tym dokumentem."* Dowody audytu: worktree
|
||||||
|
`homelab-codex-ws` cięty z mastera, pilot `~/narty-2027/saalbach-kb` (lokalny
|
||||||
|
odczyt), żywa baza `kb` na PIHA (`ssh piha 'docker exec kb-postgres psql...'`),
|
||||||
|
`systemctl status` na PIHA. **Forgejo nie było w tym zestawie dowodów** — audyt nie
|
||||||
|
odpytał API/repo Forgejo, mimo że `CLAUDE.md` i `inventory/topology.yaml` jasno
|
||||||
|
wskazują `git_provider: forgejo` jako miejsce, gdzie `kb-wiki` miało w ogóle
|
||||||
|
powstać (decyzja D7 faza3 §8.2: *„osobne repo kb-wiki"*, bez wskazania konkretnego
|
||||||
|
hosta w chwili pisania szkicu — Forgejo to naturalna, ale niejawna implikacja).
|
||||||
|
|
||||||
|
Zlecenie tej sesji (2026-08-27, Etap 1) powtórzyło za audytem: *„REMOTE NIE
|
||||||
|
ISTNIEJE — nie konfiguruj, nie pushuj; operator założy repo na Forgejo później."*
|
||||||
|
To zdanie okazało się **faktycznie nieprawdziwe** — sprawdzone dopiero przy pisaniu
|
||||||
|
tego dokumentu (Część C zlecenia), zapytaniem GET do `http://100.108.208.3:3000/
|
||||||
|
api/v1/repos/search?q=kb-wiki` (read-only, bez uwierzytelnienia, `private: false`
|
||||||
|
więc odpowiedź jawna) i następnie `git clone` (pull-only, potwierdzone uprawnieniami
|
||||||
|
API: `"push": false, "pull": true`) do katalogu tymczasowego poza tym repo.
|
||||||
|
|
||||||
|
**Wniosek na przyszłość:** *„repo nie istnieje"* w planie/audycie nie jest
|
||||||
|
wystarczające bez sprawdzenia bezpośrednio w systemie, który je hostuje
|
||||||
|
(`inventory/topology.yaml` → `git_provider: forgejo` → Forgejo API/`git ls-remote`),
|
||||||
|
nie tylko w obrębie `homelab-codex-ws` i lokalnych klonów znanych z wcześniejszych
|
||||||
|
sesji. Ten sam błąd metodologiczny mógłby się powtórzyć przy dowolnym innym „osobnym
|
||||||
|
repo" planowanym w dokumentacji, ale nigdy nie zweryfikowanym wprost u hosta.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Co faktycznie istnieje w `oskar/kb-wiki` (Forgejo, od 2026-07-21)
|
||||||
|
|
||||||
|
Stan wg `git clone https://forgejo.kapala.org/oskar/kb-wiki.git` (read-only,
|
||||||
|
2026-08-27), 3 commity na `master`:
|
||||||
|
|
||||||
|
```
|
||||||
|
156925c struktura: katalogi, konwencje, README
|
||||||
|
42c7731 compile: pzu, warta, ubezpieczenie-auto-ga431ja-2026, fll-2025-26, wspolnota-mieszkaniowa-targowa-2a ← kaskada retrievalu kb-postgres@PIHA
|
||||||
|
c52f867 lint: pierwszy raport ręczny 2026-07-21
|
||||||
|
```
|
||||||
|
|
||||||
|
Struktura: `README.md`, `INDEX.md` (wielka litera — inaczej niż plik zarezerwowany
|
||||||
|
OKF `index.md`, patrz §3 niżej), `_meta/conventions.md`, `_meta/lint-reports/
|
||||||
|
lint-report-2026-07-21.md`, 5 stron-encji:
|
||||||
|
|
||||||
|
| Strona | Typ | Skrót |
|
||||||
|
|---|---|---|
|
||||||
|
| `podmioty/pzu.md` | podmiot | PZU — OWU Auto/OC, karty produktu, oferta T1531938417 |
|
||||||
|
| `podmioty/warta.md` | podmiot | WARTA — OWU AC Standard/Komfort/Moje Auto |
|
||||||
|
| `sprawy/ubezpieczenie-auto-ga431ja-2026.md` | sprawa | Porównanie ofert PZU/WARTA/InterRisk, Mazda 6 (GA431JA), 06.2026 |
|
||||||
|
| `sprawy/fll-2025-26.md` | sprawa | FLL Challenge UNEARTHED, drużyna #1100, Warszawa II |
|
||||||
|
| `sprawy/wspolnota-mieszkaniowa-targowa-2a.md` | sprawa | Wspólnota Mieszkaniowa Targowa 2A (Pruszków) — zebranie roczne 2026 |
|
||||||
|
|
||||||
|
`osoby/`, `umowy/`, `tematy/` istnieją jako katalogi puste (`.gitkeep`) — **zero
|
||||||
|
stron typu `osoba`** w lipcowym proof-of-concept. To jest jedyna luka pokrycia
|
||||||
|
typów, którą Etap 1 tej sesji (§2) faktycznie domyka, nie duplikuje.
|
||||||
|
|
||||||
|
Jakość: wysoka, metodologicznie zgodna z tym, co audyt 08-26 dopiero rekomendował
|
||||||
|
(sekcja „Niepewne/sprzeczne" jako wzorzec — tam nazwana wprost inaczej, ale ta sama
|
||||||
|
funkcja; degradacja przy uszkodzonym OCR zamiast ślepego zaufania streszczeniu;
|
||||||
|
84/86 wpisów `sources[].chunks` i 73/73 przypisów inline zweryfikowanych wprost w
|
||||||
|
bazie, udokumentowane w `_meta/lint-reports/lint-report-2026-07-21.md`). Pełna
|
||||||
|
treść: `_meta/conventions.md` tamtego repo (różni się od konwencji tej sesji w
|
||||||
|
kilku miejscach nieistotnych merytorycznie — casing `INDEX.md`/`index.md`, typ
|
||||||
|
`meta` vs `temat` dla `conventions.md` — patrz §3).
|
||||||
|
|
||||||
|
**`kb-m5-faza4.md` (linia 17, `docs/sessions/2026-07-21.md`) już to dokumentowały
|
||||||
|
poprawnie** — ten fakt był w repo `homelab-codex-ws` cały czas, czytelny `git log`/
|
||||||
|
`grep -r kb-wiki`. Audyt 08-26 po prostu tego nie sprawdził (§0 wyżej).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Co ta sesja (2026-08-27) faktycznie zrobiła — Etap 1 wg zlecenia, **lokalnie, bez wiedzy o §1**
|
||||||
|
|
||||||
|
Zbudowany od zera, niezależnie: `~/kb-wiki-etap1-sesja-2026-08-27/` (przemianowany
|
||||||
|
z `~/kb-wiki/` po odkryciu §1 — żeby nie kolidować ścieżką z przyszłym `git clone`
|
||||||
|
prawdziwego repo). Git lokalny, **6 commitów, nigdy nie pushowany do żadnego
|
||||||
|
remote** (żaden remote nie był skonfigurowany — zgodnie z literą pierwotnego
|
||||||
|
zlecenia, które okazało się oparte na błędnym założeniu z §0).
|
||||||
|
|
||||||
|
3 strony proof, celowo zróżnicowane typy (jak żądał audyt §9 Etap 1):
|
||||||
|
|
||||||
|
| Strona | Typ | Pokrycie względem §1 |
|
||||||
|
|---|---|---|
|
||||||
|
| `sprawy/fll-2025-26.md` | sprawa | **Duplikat tej samej encji** co lipcowe repo — skompilowany niezależnie, z częściowo innym zestawem chunków (uzupełniające, nie identyczne `sources`). Wysoka zgodność wniosków — patrz §4. |
|
||||||
|
| `podmioty/mbank.md` | podmiot | **Nowe pokrycie** — nie istnieje w lipcowym repo (tam tylko PZU/WARTA, ubezpieczyciele) |
|
||||||
|
| `osoby/pawel-cesar-sanjuan-szklarz.md` | osoba | **Domyka lukę** — lipcowe repo ma katalog `osoby/` pusty; to pierwsza strona typu `osoba` w całym projekcie, jedyny dotąd test entity-resolution na aliasach |
|
||||||
|
|
||||||
|
Pełna treść stron, `_meta/conventions.md` tej sesji (napisane niezależnie od
|
||||||
|
lipcowego — patrz różnice §3), `check_okf.py` (walidator, **którego lipcowe repo
|
||||||
|
nie ma jako skomitowanego pliku** — tamta sesja weryfikowała `sources`/przypisy
|
||||||
|
ad hoc, bez zostawienia narzędzia), i `_meta/lint-reports/2026-08-27.md` — wszystko
|
||||||
|
w `~/kb-wiki-etap1-sesja-2026-08-27/`, 6 commitów (`git log --oneline`):
|
||||||
|
struktura+konwencje, walidator, 3× `compile:`, 1× `lint:`.
|
||||||
|
|
||||||
|
DoD audytu §9 Etap 1 (tabela, 4 punkty) — spełnione **lokalnie**, w tym repo
|
||||||
|
równoległym: repo istnieje z konwencjami, walidator działa (zielono, 40 par
|
||||||
|
`sources` + 47 inline przypisów zweryfikowanych wprost w `KB_DSN`), 3 strony
|
||||||
|
proof spełniają inwarianty 1-2, raport lintu ręcznego zapisany.
|
||||||
|
|
||||||
|
**Weryfikacja lint:**
|
||||||
|
```
|
||||||
|
$ python3 check_okf.py # w ~/kb-wiki-etap1-sesja-2026-08-27/
|
||||||
|
ZGODNE z OKF v0.1 (kb-wiki): [...] wszystkie 40 przypisów zweryfikowane w bazie.
|
||||||
|
```
|
||||||
|
|
||||||
|
**`scripts/kb/check_okf.py` z `homelab-codex-ws` uruchomiony na tym samym repo —
|
||||||
|
zgodnie z rozjazdem przewidzianym w zleceniu:**
|
||||||
|
```
|
||||||
|
$ python3 scripts/kb/check_okf.py ~/kb-wiki-etap1-sesja-2026-08-27
|
||||||
|
Zakres: kb, docs/sessions
|
||||||
|
Sprawdzono plików .md: 0
|
||||||
|
BRAK plików w zakresie — nie ma czego walidować.
|
||||||
|
```
|
||||||
|
`SCOPE = ("kb", "docs/sessions")` w tamtym skrypcie jest wpisany na sztywno
|
||||||
|
względem `root` — `kb-wiki` (żadna z dwóch kopii, lipcowa ani ta) nie ma
|
||||||
|
podkatalogu `kb/` ani `docs/sessions/`, więc walidator zawsze zwróci "brak
|
||||||
|
plików" dla tego layoutu. Nie hackowany — to jest realny, ustrukturalny rozjazd
|
||||||
|
(nie błąd w `kb-wiki`), odnotowany tu jako follow-up: albo `scripts/kb/
|
||||||
|
check_okf.py` dostaje parametr `--scope`, albo `kb-wiki` (dowolna wersja) trzyma
|
||||||
|
własny walidator na stałe (rekomendacja tej sesji — już częściowo zrobione, patrz
|
||||||
|
`check_okf.py` w repo tej sesji).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Różnice konwencji: lipiec vs ta sesja (nieistotne merytorycznie, warto ujednolicić)
|
||||||
|
|
||||||
|
| Aspekt | Lipiec (`oskar/kb-wiki`, prawdziwe repo) | Ta sesja (lokalne) | Rekomendacja |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Plik nawigacyjny root | `INDEX.md` (duża litera), osobny od OKF-owego `index.md` (nie istnieje w ogóle w lipcowym repo — brak pliku `index.md`) | `index.md` (mała litera, OKF §11 reserved-file, pełni tę samą rolę) | Do rozstrzygnięcia przez operatora — lipcowe repo **nie ma pliku `index.md`** w ogóle, więc formalnie nie przechodzi `check_okf.py` tej sesji (`brak bloku frontmattera` nie wystąpi, bo pliku nie ma, ale reguła "root index.md wymagany" by to złapała, gdyby uruchomić walidator tej sesji na lipcowym repo — nie testowane, bo nie nasze repo do modyfikacji) |
|
||||||
|
| `type` dla `_meta/conventions.md` | `meta` (dodatkowa wartość w słowniku typów) | `temat` (nadużycie istniejącego typu, bo `TYPES` tej sesji nie miało `meta`) | Lipcowe podejście czystsze — warto przyjąć `meta` jako 6. typ przy rekoncyliacji |
|
||||||
|
| Frontmatter `sources` obowiązkowe niepuste | Nie wymuszone narzędziem (ręczna dyscyplina) | Wymuszone w `check_okf.py` (poza `_meta/`) | Zachować regułę tej sesji przy rekoncyliacji — tańsze niż poleganie na dyscyplinie |
|
||||||
|
| Walidator jako plik w repo | Brak (weryfikacja ad hoc, nieskomitowana) | `check_okf.py`, skomitowany, wielokrotnego użytku | Przenieść `check_okf.py` tej sesji do prawdziwego repo przy rekoncyliacji |
|
||||||
|
| Remote skonfigurowany | Tak (Forgejo, `master`) | Nie (lokalny, celowo) | — |
|
||||||
|
|
||||||
|
Żadna z różnic nie jest błędem — obie konwencje są spójne z faza3 §8.2 i audytem
|
||||||
|
08-26 w rzeczach, które się nakładają (format `[^envelope_id#chunk_id]`, fallback,
|
||||||
|
sekcja niepewności, CC/API-only, izolacja retrievalu kompilacji od `source='wiki'`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Rekoncyliacja — do operatora, nie rozstrzygane przez tę sesję
|
||||||
|
|
||||||
|
To jest decyzja o realnych, już opublikowanych danych osobowych (finanse,
|
||||||
|
ubezpieczenia, dziecko na turnieju) w prawdziwym repo — powyżej progu, przy którym
|
||||||
|
ta sesja podejmuje decyzje sama. Opcje, bez rekomendacji wiążącej:
|
||||||
|
|
||||||
|
1. **Scalić nowe strony tej sesji do prawdziwego repo.** `podmioty/mbank.md` i
|
||||||
|
`osoby/pawel-cesar-sanjuan-szklarz.md` nie kolidują z niczym w lipcowym repo —
|
||||||
|
czysty dodatek. `sprawy/fll-2025-26.md` **koliduje** (dwie różne kompilacje tej
|
||||||
|
samej encji) — wymaga porównania treść-po-treści przez operatora albo kolejną
|
||||||
|
sesję CC, nie automatycznego scalenia.
|
||||||
|
2. **Porównanie dwóch niezależnych kompilacji `fll-2025-26` jako test jakości
|
||||||
|
kompilatora.** Obie wersje (lipcowa i ta) zgadzają się merytorycznie w
|
||||||
|
kluczowych punktach: wynik 195 pkt jako jedyny czytelny wprost z chunka
|
||||||
|
(`paperless:119#277`/`#278` — obie sesje trafiły w ten sam chunk niezależnie),
|
||||||
|
pozostałe dwa wyniki (165/230) tylko ze streszczenia, nominacja do Nagrody
|
||||||
|
Sędziów niepotwierdzona wprost w chunku, imiona z formularzy zgód celowo
|
||||||
|
pominięte, nazwisko trenera nieczytelne OCR. Rozbieżność: ta sesja znalazła i
|
||||||
|
zacytowała sprzeczność liczebności drużyny (10 vs 8 osób, z korespondencji
|
||||||
|
gmail — lipcowa wersja jej nie ma, bo nie sięgnęła po te same koperty gmail).
|
||||||
|
Lipcowa wersja znalazła niejednoznaczność sezonu SUBMERGED vs UNEARTHED
|
||||||
|
(`paperless:6`) i konkretne imiona dzieci z nazw plików (metadane, nie treść
|
||||||
|
dokumentu) — ta sesja pominęła `paperless:6` całkowicie i nie sięgnęła po
|
||||||
|
`entities[type=filename]`. **Dwie niezależne kompilacje tej samej encji nie są
|
||||||
|
sprzeczne, są komplementarne** — dobry sygnał, że metoda jest powtarzalna, zły
|
||||||
|
sygnał, że jedna kompilacja pomija realne dowody, które druga znalazła.
|
||||||
|
3. **Zostawić oba repo osobno, jawnie zarchiwizować to jako dwa niezależne
|
||||||
|
przebiegi tej samej fazy.** Najbezpieczniejsze, ale traci szansę na scalenie
|
||||||
|
uzupełniających się dowodów z punktu 2.
|
||||||
|
|
||||||
|
Krok operatora, niezależnie od wyboru z (1)-(3): **`~/kb-wiki-etap1-sesja-2026-08-27/`
|
||||||
|
istnieje lokalnie, nigdy nie pushowany** — do usunięcia/scalenia/zachowania wg
|
||||||
|
decyzji, nie automatycznie.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Poza zakresem tej notatki (Etap 2, jeśli operator potwierdzi kontynuację)
|
||||||
|
|
||||||
|
Zależnie od wyniku rekoncyliacji z §4:
|
||||||
|
|
||||||
|
- Skala do pełnej listy 17 encji z audytu §3 (już częściowo pokryta lipcowym repo
|
||||||
|
inną listą — PZU/WARTA/wspólnota/FLL nie pokrywają się z 17-pozycyjną listą
|
||||||
|
audytu poza `fll-2025-26`; wymaga ponownego review, które z 17 są już zrobione
|
||||||
|
pod innymi nazwami).
|
||||||
|
- Lint automatyczny w kodzie (`kb-wiki/lint.py`, inwariant 3 pełny) — żadna z
|
||||||
|
dwóch kopii tego nie ma, obie robiły lint ręcznie.
|
||||||
|
- Jednorazowy skan `<60>`/`\x00` na źródłach, które faktycznie zasiliły PoC (decyzja
|
||||||
|
h) — nie wykonany w żadnej z dwóch kompilacji; oba proof-of-concept **napotkały**
|
||||||
|
uszkodzony OCR organicznie (paperless:119/120 w obu wersjach fll-2025-26,
|
||||||
|
mbank.md ta sesja) bez systematycznego skanu.
|
||||||
|
- Integracja `source='wiki'` w `packages/kb-retrieval` (inwariant 5+7) — poza
|
||||||
|
zakresem obu przebiegów.
|
||||||
|
- Rozjazd `scripts/kb/check_okf.py` (SCOPE sztywny) — patrz §2, nie naprawiony
|
||||||
|
celowo (instrukcja zlecenia: nie hackować).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Rekoncyliacja domknięta (2026-08-27, decyzja operatora: opcja 1 z §4)
|
||||||
|
|
||||||
|
Operator zdecydował scalić — bez rozstrzygania punktu 3 z §4 (dwa osobne
|
||||||
|
repo) i bez samego jedynie archiwizowania punktu 2 (test jakości
|
||||||
|
kompilatora, wykonany przy okazji scalenia `fll-2025-26`, patrz niżej).
|
||||||
|
Wykonano w klonie roboczym `~/kb-wiki` (repo prawdziwe, `master`), **cztery
|
||||||
|
commity, lokalnie, świadomie nie pushnięte** — review operatora przed
|
||||||
|
pushem do Forgejo jest krokiem następnym, nie tej sesji.
|
||||||
|
|
||||||
|
### 6.1 `fll-2025-26` — scalenie dwóch niezależnych kompilacji
|
||||||
|
|
||||||
|
Wykonane **przed** tą sesją reconciliation (`kb-wiki` commit `c4127b9`,
|
||||||
|
autor: operator lub wcześniejsza sesja — poza zakresem tego zadania, ta
|
||||||
|
sesja zastała je już na `master` i pushnięte: *"Push zrobiony"*, cytat
|
||||||
|
zlecenia). Wynik zgodny z analizą §4 punkt 2: zero sprzecznych twierdzeń,
|
||||||
|
czysto komplementarne pokrycie (lipiec: 14 kopert paperless; sierpień
|
||||||
|
dodał 6 kopert gmail + `paperless:183`) — 21 kopert finalnie (15 paperless +
|
||||||
|
6 gmail), 38/38 par `sources` + 45/45 przypisów inline zweryfikowanych w
|
||||||
|
bazie przy tamtym scaleniu. Ta sesja **nie modyfikowała** tej strony —
|
||||||
|
tylko zweryfikowała ją ponownie w ramach pełnego lintu repo (§6.4).
|
||||||
|
|
||||||
|
### 6.2 `mbank` i `pawel-cesar-sanjuan-szklarz` — przeniesione bez kolizji
|
||||||
|
|
||||||
|
Zgodnie z §4 punkt 1 ("nie kolidują z niczym w lipcowym repo — czysty
|
||||||
|
dodatek"): skopiowane z `~/kb-wiki-etap1-sesja-2026-08-27/` do `~/kb-wiki`,
|
||||||
|
linki `[[...]]` przepisane ze składni sesyjnej (`[[../katalog/plik]]`) na
|
||||||
|
rzeczywistą konwencję repo (`[[plik]]`, bez katalogu). Treść merytoryczna
|
||||||
|
nietknięta. `INDEX.md` dopisany o oba wpisy. `sprawy/fll-2025-26.md`
|
||||||
|
przestało być sierotą w grafie `[[...]]` (link przychodzący z obu nowych
|
||||||
|
stron, sekcja „Powiązane" — decyzja sesyjna sprzed przeniesienia, nie tej
|
||||||
|
rekoncyliacji: „sprawdzone wprost, zero wspólnego dowodu", link mimo braku
|
||||||
|
dowodu).
|
||||||
|
|
||||||
|
### 6.3 Konwencje: `check_okf.py` przeniesiony, decyzje (d)-(f) scalone
|
||||||
|
|
||||||
|
`check_okf.py` (walidator sesyjny) przeniesiony do `~/kb-wiki` — lipcowy
|
||||||
|
proof-of-concept nie zostawił żadnego skomitowanego narzędzia (§3 tabela,
|
||||||
|
rekomendacja "Przenieść `check_okf.py` tej sesji do prawdziwego repo przy
|
||||||
|
rekoncyliacji" — wykonana dosłownie). Zaadaptowany do rzeczywistego layoutu
|
||||||
|
tego repo: `TYPES` rozszerzone o `meta` (repo miało 6. typ od lipca, sesyjny
|
||||||
|
walidator znał tylko 5), `README.md`/`INDEX.md`/`_meta/lint-reports/*.md`
|
||||||
|
wyjęte z wymogu frontmattera stron-konceptów (istniały bez niego od
|
||||||
|
2026-07-21 — dokumentacja/raporty, nie skompilowane encje).
|
||||||
|
|
||||||
|
Decyzje (d)-(f) audytu 08-26, wypracowane w `_meta/conventions.md` sesji
|
||||||
|
(§5/§7/§8 tamtego pliku), scalone do `_meta/conventions.md` prawdziwego repo
|
||||||
|
— **bez duplikowania** tego, co repo już miało z lipca/wcześniejszej sesji
|
||||||
|
(6 typów wliczając `meta`, sekcja „Brak danych w KB" — obie już scalone,
|
||||||
|
commity `156925c`/`3bd2441`, przed tym zadaniem):
|
||||||
|
|
||||||
|
- **(d) fallback envelope-only** — `[^envelope_id]` bez `#chunk_id`, gdy
|
||||||
|
dowód nie ma konkretnego chunka albo chunk zniknął po re-chunkingu.
|
||||||
|
- **(e) sekcja „Niepewne / sprzeczne"** jako nazwana sekcja końca strony
|
||||||
|
(już używana de facto na żywych stronach, teraz sformalizowana pisemnie) +
|
||||||
|
polityka aktualizacji „dopisz, nie nadpisuj" przy nowym sprzecznym dowodzie.
|
||||||
|
- **(f) inwariant 7** — izolacja retrievalu kompilacji od `source='wiki'`
|
||||||
|
(mitygacja self-citation/citogenesis), CC/API-only jako wykonawca.
|
||||||
|
|
||||||
|
Różnica konwencji z §3 tabeli dotycząca casingu pliku nawigacyjnego
|
||||||
|
(`INDEX.md` vs OKF-owy `index.md`) **pozostaje nierozstrzygnięta** — zgodnie
|
||||||
|
z §3, do operatora, poza zakresem tej rekoncyliacji. `check_okf.py`
|
||||||
|
pragmatycznie traktuje oba pliki root (`README.md`, `INDEX.md`) jako
|
||||||
|
wyjęte z wymogu frontmattera, bez przesądzania, który plik "wygrywa".
|
||||||
|
|
||||||
|
### 6.4 Lint na całości — stan końcowy: 7 stron, zero błędów
|
||||||
|
|
||||||
|
`python3 check_okf.py` w `~/kb-wiki`: **ZGODNE**, 74/74 par `sources`
|
||||||
|
(wszystkie 6 stron-konceptów poza `_meta/`) zweryfikowanych w `KB_DSN`
|
||||||
|
(SELECT-only). Osobny, pełny przebieg (skrypt jednorazowy, nieskomitowany —
|
||||||
|
`check_okf.py` nie parsuje treści Markdown) po **wszystkich** przypisach
|
||||||
|
inline `[^envelope_id#chunk_id]` na **wszystkich** 7 stronach: **150/150**
|
||||||
|
zweryfikowanych, 0 zdegradowanych do fallback. Zero wiszących linków
|
||||||
|
`[[...]]`, zero błędów przypisania chunk→envelope. Pełny raport:
|
||||||
|
`_meta/lint-reports/2026-08-27-rekoncyliacja.md` w `kb-wiki`.
|
||||||
|
|
||||||
|
| Metryka | Wynik |
|
||||||
|
|---|---|
|
||||||
|
| Stron w repo (po rekoncyliacji) | 7 (5 lipiec + `fll-2025-26` scalone + 2 przeniesione) |
|
||||||
|
| `sources:` (agregat frontmatter) zweryfikowane w bazie | 74/74 |
|
||||||
|
| Przypisy inline zweryfikowane w bazie | 150/150 |
|
||||||
|
| Wiszące linki `[[...]]` | 0 |
|
||||||
|
| Przypisy zdegradowane (fallback envelope-only) | 0 |
|
||||||
|
| Commity rekoncyliacji na `kb-wiki:master` | 4, lokalne, **nie pushnięte** |
|
||||||
|
|
||||||
|
### 6.5 Co zostało poza zakresem tej rekoncyliacji
|
||||||
|
|
||||||
|
- `~/kb-wiki-etap1-sesja-2026-08-27/` — pozostawione nietknięte na żądanie
|
||||||
|
operatora ("skasuję sam po weryfikacji"), nie usuwane przez tę sesję.
|
||||||
|
- Push `kb-wiki:master` do Forgejo — czeka na review operatora.
|
||||||
|
- Wszystko z §5 (skala do 17 encji, lint automatyczny w kodzie, skan
|
||||||
|
`<60>`/`\x00` na źródłach, integracja `source='wiki'` w retrievalu, rozjazd
|
||||||
|
`scripts/kb/check_okf.py`) — nadal otwarte, nie ruszone tą rekoncyliacją.
|
||||||
|
|
@ -18,6 +18,8 @@ reguły tego repo:
|
||||||
10. Wpisy `contradicts` wyglądające jak ścieżka .md też muszą istnieć;
|
10. Wpisy `contradicts` wyglądające jak ścieżka .md też muszą istnieć;
|
||||||
pozostałe wpisy to wolny tekst.
|
pozostałe wpisy to wolny tekst.
|
||||||
11. `stub` — o ile obecne — musi być boolem.
|
11. `stub` — o ile obecne — musi być boolem.
|
||||||
|
12. Brak bajtów kontrolnych poza \t \n \r w całym pliku (łapie wklejki NUL/inne
|
||||||
|
binarne śmieci wklejone z zewnętrznych źródeł, np. wyników SELECT-ów).
|
||||||
|
|
||||||
Zakres domyślny: kb/ oraz docs/sessions/, z wyłączeniem README-wskaźników
|
Zakres domyślny: kb/ oraz docs/sessions/, z wyłączeniem README-wskaźników
|
||||||
(POINTER_GLOBS) — te są nawigacją do kb-doca, nie dokumentami KB, i celowo nie
|
(POINTER_GLOBS) — te są nawigacją do kb-doca, nie dokumentami KB, i celowo nie
|
||||||
|
|
@ -71,6 +73,7 @@ POINTER_GLOBS = (
|
||||||
EXCLUDE_DIRS = ("build",)
|
EXCLUDE_DIRS = ("build",)
|
||||||
|
|
||||||
DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
||||||
|
CONTROL_CHAR_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]")
|
||||||
|
|
||||||
|
|
||||||
def is_pointer(rel: str) -> bool:
|
def is_pointer(rel: str) -> bool:
|
||||||
|
|
@ -232,6 +235,17 @@ def check_file(path: Path, root: Path) -> list[str]:
|
||||||
f"{rel}: `contradicts` wskazuje na nieistniejący plik: {item}"
|
f"{rel}: `contradicts` wskazuje na nieistniejący plik: {item}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
seen_lines: set[int] = set()
|
||||||
|
for m in CONTROL_CHAR_RE.finditer(text):
|
||||||
|
line_no = text.count("\n", 0, m.start()) + 1
|
||||||
|
if line_no in seen_lines:
|
||||||
|
continue
|
||||||
|
seen_lines.add(line_no)
|
||||||
|
errors.append(
|
||||||
|
f"{rel}: bajt kontrolny {ord(m.group()):#04x} w linii {line_no} "
|
||||||
|
"(dozwolone tylko \\t \\n \\r)"
|
||||||
|
)
|
||||||
|
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
62
scripts/kb/tests/test_check_okf.py
Normal file
62
scripts/kb/tests/test_check_okf.py
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
"""Tests for check_okf.py — the OKF v0.1 frontmatter validator.
|
||||||
|
|
||||||
|
Covers the control-character check added 2026-08-27: a file that otherwise has
|
||||||
|
valid frontmatter must still fail if its body contains a NUL byte or other
|
||||||
|
control byte outside \\t \\n \\r (the class of bug found in
|
||||||
|
kb/audits/wiki-kompilat-recon-2026-08-26.md — raw bytes pasted in from a psql
|
||||||
|
SELECT over the mail corpus).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
import check_okf # noqa: E402
|
||||||
|
|
||||||
|
VALID_FRONTMATTER = """---
|
||||||
|
okf: "0.1"
|
||||||
|
type: decision
|
||||||
|
visibility: private
|
||||||
|
status: active
|
||||||
|
updated: 2026-08-27
|
||||||
|
links: []
|
||||||
|
---
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _write(tmp_path: Path, body: str) -> Path:
|
||||||
|
p = tmp_path / "doc.md"
|
||||||
|
p.write_text(VALID_FRONTMATTER + body, encoding="utf-8")
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def test_clean_file_has_no_control_char_errors(tmp_path):
|
||||||
|
path = _write(tmp_path, "# Title\n\nZwykła treść z \t tabulatorem i \r\n końcem linii.\n")
|
||||||
|
errors = check_okf.check_file(path, tmp_path)
|
||||||
|
assert errors == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_nul_byte_in_body_is_flagged(tmp_path):
|
||||||
|
path = _write(tmp_path, "# Title\n\nZawiera bajt NUL: \x00 w tym miejscu.\n")
|
||||||
|
errors = check_okf.check_file(path, tmp_path)
|
||||||
|
assert any("bajt kontrolny" in e and "0x0" in e for e in errors)
|
||||||
|
|
||||||
|
|
||||||
|
def test_control_char_error_reports_correct_line(tmp_path):
|
||||||
|
body = "linia 1\nlinia 2\nzepsuta \x00 linia 3\nlinia 4\n"
|
||||||
|
path = _write(tmp_path, body)
|
||||||
|
errors = check_okf.check_file(path, tmp_path)
|
||||||
|
control_errors = [e for e in errors if "bajt kontrolny" in e]
|
||||||
|
assert len(control_errors) == 1
|
||||||
|
# Frontmatter occupies 7 lines before the body starts.
|
||||||
|
frontmatter_lines = VALID_FRONTMATTER.count("\n")
|
||||||
|
expected_line = frontmatter_lines + 3
|
||||||
|
assert f"w linii {expected_line}" in control_errors[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiple_control_chars_same_line_reported_once(tmp_path):
|
||||||
|
path = _write(tmp_path, "para \x00 z dwoma \x00 bajtami na tej samej linii\n")
|
||||||
|
errors = check_okf.check_file(path, tmp_path)
|
||||||
|
control_errors = [e for e in errors if "bajt kontrolny" in e]
|
||||||
|
assert len(control_errors) == 1
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# health-monitor.sh - Homelab node health monitor and safe disk cleanup
|
# health-monitor.sh - Homelab node health monitor
|
||||||
#
|
#
|
||||||
# Designed to run standalone on the host (cron or direct) or to be called by
|
# Designed to run standalone on the host (cron or direct) or to be called by
|
||||||
# the node-agent Python daemon. All cleanup decisions follow the conservative
|
# the node-agent Python daemon.
|
||||||
# policy agreed in the design review:
|
|
||||||
#
|
#
|
||||||
# lte_node (chelsty-infra, chelsty-ha) : NO cleanup at all
|
# Docker cleanup (image/container/build-cache prune) does NOT live here.
|
||||||
# sd_card (piha, saturn) : dangling images + stopped containers,
|
# node_agent.py (R1) is the sole owner of that cleanup, with a filtered
|
||||||
# rate-limited to once per 24 h
|
# container prune that respects restart policy and compose ownership —
|
||||||
# ai_node (solaria) : dangling images + stopped containers
|
# see kb/incidents/2026-07-30-ollama-solaria-vanish.md. This script used to
|
||||||
# + build cache (NEVER -a)
|
# carry its own unfiltered `docker container prune -f` (never wired into
|
||||||
# standard (vps) : dangling images + stopped containers
|
# cron/systemd on any node, verified 2026-08-06); it was removed rather than
|
||||||
# + build cache
|
# backporting the R1 filter here too, to avoid two independent copies of
|
||||||
|
# cleanup logic drifting apart.
|
||||||
#
|
#
|
||||||
# VPS additionally rotates control-plane filesystem artefacts:
|
# VPS additionally rotates control-plane filesystem artefacts:
|
||||||
# actions/completed + failed > 7 days
|
# actions/completed + failed > 7 days
|
||||||
|
|
@ -43,15 +43,6 @@ DISK_CRIT_PCT=85
|
||||||
MEM_WARN_PCT=85
|
MEM_WARN_PCT=85
|
||||||
MEM_CRIT_PCT=95
|
MEM_CRIT_PCT=95
|
||||||
|
|
||||||
# Rate-limit file for SD-card nodes (max one Docker cleanup per 24 h)
|
|
||||||
CLEANUP_LOCK="${STATE_DIR}/last-docker-cleanup"
|
|
||||||
CLEANUP_INTERVAL=86400 # seconds
|
|
||||||
|
|
||||||
# Node classifications
|
|
||||||
LTE_NODES="chelsty-infra chelsty-ha"
|
|
||||||
SD_CARD_NODES="piha saturn"
|
|
||||||
AI_NODES="solaria"
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Helpers
|
# Helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -60,20 +51,6 @@ log() { echo "$(date -u +%H:%M:%S) [INFO] $*"; }
|
||||||
warn() { echo "$(date -u +%H:%M:%S) [WARN] $*" >&2; }
|
warn() { echo "$(date -u +%H:%M:%S) [WARN] $*" >&2; }
|
||||||
err() { echo "$(date -u +%H:%M:%S) [ERROR] $*" >&2; }
|
err() { echo "$(date -u +%H:%M:%S) [ERROR] $*" >&2; }
|
||||||
|
|
||||||
contains() {
|
|
||||||
local word="$1"; shift
|
|
||||||
for w in "$@"; do [[ "$w" == "$word" ]] && return 0; done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
get_node_type() {
|
|
||||||
# shellcheck disable=SC2086
|
|
||||||
if contains "$NODE_NAME" $LTE_NODES; then echo "lte_node"; return; fi
|
|
||||||
if contains "$NODE_NAME" $SD_CARD_NODES; then echo "sd_card"; return; fi
|
|
||||||
if contains "$NODE_NAME" $AI_NODES; then echo "ai_node"; return; fi
|
|
||||||
echo "standard"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Event emission
|
# Event emission
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -195,69 +172,6 @@ check_containers() {
|
||||||
--format "{{.Names}}" 2>/dev/null || true)
|
--format "{{.Names}}" 2>/dev/null || true)
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Safe Docker cleanup (per policy)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
_sd_card_rate_ok() {
|
|
||||||
if [[ -f "${CLEANUP_LOCK}" ]]; then
|
|
||||||
local last_ts elapsed
|
|
||||||
last_ts=$(cat "${CLEANUP_LOCK}" 2>/dev/null || echo 0)
|
|
||||||
elapsed=$(( TIMESTAMP - last_ts ))
|
|
||||||
if [[ "${elapsed}" -lt "${CLEANUP_INTERVAL}" ]]; then
|
|
||||||
log "Docker cleanup skipped: last run ${elapsed}s ago (limit ${CLEANUP_INTERVAL}s)"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
_mark_cleanup_done() {
|
|
||||||
echo "${TIMESTAMP}" > "${CLEANUP_LOCK}"
|
|
||||||
}
|
|
||||||
|
|
||||||
run_safe_cleanup() {
|
|
||||||
command -v docker &>/dev/null || return
|
|
||||||
local node_type
|
|
||||||
node_type=$(get_node_type)
|
|
||||||
|
|
||||||
case "${node_type}" in
|
|
||||||
lte_node)
|
|
||||||
# NO cleanup on LTE nodes. Any docker operation risks triggering
|
|
||||||
# a pull over a metered/intermittent connection.
|
|
||||||
log "Skipping Docker cleanup: LTE node (${NODE_NAME})"
|
|
||||||
;;
|
|
||||||
|
|
||||||
sd_card)
|
|
||||||
# Dangling images + stopped containers only.
|
|
||||||
# Rate-limited to once per 24 hours to protect SD card write endurance.
|
|
||||||
_sd_card_rate_ok || return
|
|
||||||
log "Running rate-limited Docker cleanup (SD card node)"
|
|
||||||
docker image prune -f >/dev/null 2>&1 || true
|
|
||||||
docker container prune -f >/dev/null 2>&1 || true
|
|
||||||
_mark_cleanup_done
|
|
||||||
;;
|
|
||||||
|
|
||||||
ai_node)
|
|
||||||
# Dangling images + stopped containers + build cache.
|
|
||||||
# NEVER docker image prune -a (would remove Ollama runtime images,
|
|
||||||
# requiring a multi-hour re-pull of model weights).
|
|
||||||
log "Running AI-node Docker cleanup (dangling images + containers + build cache)"
|
|
||||||
docker image prune -f >/dev/null 2>&1 || true
|
|
||||||
docker container prune -f >/dev/null 2>&1 || true
|
|
||||||
docker builder prune -f >/dev/null 2>&1 || true
|
|
||||||
;;
|
|
||||||
|
|
||||||
standard)
|
|
||||||
# VPS and other standard nodes: full safe cleanup.
|
|
||||||
log "Running standard Docker cleanup"
|
|
||||||
docker image prune -f >/dev/null 2>&1 || true
|
|
||||||
docker container prune -f >/dev/null 2>&1 || true
|
|
||||||
docker builder prune -f >/dev/null 2>&1 || true
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# VPS-specific: control-plane filesystem rotation
|
# VPS-specific: control-plane filesystem rotation
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -315,15 +229,13 @@ except Exception:
|
||||||
|
|
||||||
mkdir -p "${EVENTS_DIR}/${NODE_NAME}" "${STATE_DIR}"
|
mkdir -p "${EVENTS_DIR}/${NODE_NAME}" "${STATE_DIR}"
|
||||||
|
|
||||||
log "Health check starting on ${NODE_NAME} (type=$(get_node_type))"
|
log "Health check starting on ${NODE_NAME}"
|
||||||
|
|
||||||
disk_pct=$(check_disk || echo 0)
|
disk_pct=$(check_disk || echo 0)
|
||||||
mem_pct=$(check_memory || echo 0)
|
mem_pct=$(check_memory || echo 0)
|
||||||
cpu_pct=$(check_cpu || echo 0)
|
cpu_pct=$(check_cpu || echo 0)
|
||||||
check_containers
|
check_containers
|
||||||
|
|
||||||
run_safe_cleanup
|
|
||||||
|
|
||||||
# VPS: also rotate control-plane filesystem artefacts
|
# VPS: also rotate control-plane filesystem artefacts
|
||||||
if [[ "${NODE_NAME}" == "vps" ]]; then
|
if [[ "${NODE_NAME}" == "vps" ]]; then
|
||||||
cleanup_control_plane_fs
|
cleanup_control_plane_fs
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,11 @@ FAILED_EVENTS_DIR = STATE_DIR / "observer_failed_events"
|
||||||
# "operator drops a file, the owning process consumes it" pattern the actions
|
# "operator drops a file, the owning process consumes it" pattern the actions
|
||||||
# pending/approved queue already uses.
|
# pending/approved queue already uses.
|
||||||
RESOLVE_REQUESTS_DIR = WORLD_DIR / "resolve-requests"
|
RESOLVE_REQUESTS_DIR = WORLD_DIR / "resolve-requests"
|
||||||
|
# mkdir(mode=...) is masked by the process umask, so the SSH operator (group
|
||||||
|
# aerbot) cannot drop a flag file into a dir created at the default 0o755 —
|
||||||
|
# same defect/fix as executor.py's INBOX_DIR_MODE (2026-08-06): an explicit,
|
||||||
|
# idempotent os.chmod after mkdir, applied on the same path the dir is used.
|
||||||
|
RESOLVE_REQUESTS_DIR_MODE = 0o775
|
||||||
|
|
||||||
# Time-based fallback for incidents that can never receive the service_healthy
|
# Time-based fallback for incidents that can never receive the service_healthy
|
||||||
# event _resolve_incident() waits for (service removed/renamed/decommissioned
|
# event _resolve_incident() waits for (service removed/renamed/decommissioned
|
||||||
|
|
@ -277,6 +282,13 @@ class Observer:
|
||||||
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
FAILED_EVENTS_DIR.mkdir(parents=True, exist_ok=True)
|
FAILED_EVENTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
RESOLVE_REQUESTS_DIR.mkdir(parents=True, exist_ok=True)
|
RESOLVE_REQUESTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
try:
|
||||||
|
os.chmod(RESOLVE_REQUESTS_DIR, RESOLVE_REQUESTS_DIR_MODE)
|
||||||
|
except OSError as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Could not set mode {oct(RESOLVE_REQUESTS_DIR_MODE)} on "
|
||||||
|
f"{RESOLVE_REQUESTS_DIR}: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
def _quarantine_event_file(self, file_path: str, node_dir: str, exc: Exception) -> None:
|
def _quarantine_event_file(self, file_path: str, node_dir: str, exc: Exception) -> None:
|
||||||
"""Move an unreadable/unprocessable event out of the hot path."""
|
"""Move an unreadable/unprocessable event out of the hot path."""
|
||||||
|
|
|
||||||
|
|
@ -452,13 +452,15 @@ class Supervisor:
|
||||||
|
|
||||||
# Choose action type first so we can build the ID.
|
# Choose action type first so we can build the ID.
|
||||||
#
|
#
|
||||||
# container_restart IDs carry a suffix so two DIFFERENT incidents for
|
# Both container_restart and redeploy IDs carry a suffix so two
|
||||||
# the same node+service never collide in cancelled/completed/failed
|
# DIFFERENT incidents for the same node+service never collide in
|
||||||
# (2026-08-26: a generic containers_not_running restart and a later
|
# cancelled/completed/failed (2026-08-26: a generic
|
||||||
# ha-diag-agent shadow-mode restart both used the bare
|
# containers_not_running restart and a later ha-diag-agent
|
||||||
|
# shadow-mode restart both used the bare
|
||||||
# container-restart-piha-homeassistant id and overwrote each other's
|
# container-restart-piha-homeassistant id and overwrote each other's
|
||||||
# history — worked around manually that session, see
|
# history — worked around manually that session, see
|
||||||
# docs/sessions/2026-08-26.md).
|
# docs/sessions/2026-08-26.md). redeploy carries the same latent risk
|
||||||
|
# via the same code path and got the same fix on 2026-08-27.
|
||||||
#
|
#
|
||||||
# The suffix is the triggering incident's started_at, NOT time.time()
|
# The suffix is the triggering incident's started_at, NOT time.time()
|
||||||
# at generation time: reconcile() calls _generate_recommendation on
|
# at generation time: reconcile() calls _generate_recommendation on
|
||||||
|
|
@ -482,20 +484,22 @@ class Supervisor:
|
||||||
# bare id has no incident to distinguish "same" from "different"
|
# bare id has no incident to distinguish "same" from "different"
|
||||||
# occurrences by, but it is at least stable across calls, which is
|
# occurrences by, but it is at least stable across calls, which is
|
||||||
# what idempotency here actually requires.
|
# what idempotency here actually requires.
|
||||||
if trigger_type in CONTAINER_RESTART_TRIGGERS:
|
#
|
||||||
|
# Safe to add the suffix to redeploy ids: verified no code anywhere
|
||||||
|
# else reconstructs `redeploy-{node}-{service}` for an exact-match
|
||||||
|
# lookup. _cancel_resolved_pending_actions matches on the node/service
|
||||||
|
# *fields* inside each pending file, not on the id string; executor.py,
|
||||||
|
# node_agent.py and deploy-runner.sh all treat action_id as an opaque
|
||||||
|
# string read back from the action's own JSON/marker file, never
|
||||||
|
# reconstructed from node+service.
|
||||||
|
action_prefix = "container-restart" if trigger_type in CONTAINER_RESTART_TRIGGERS else "redeploy"
|
||||||
incident_id = self.actual_state["services"].get(drift["svc_key"], {}).get("incident_id")
|
incident_id = self.actual_state["services"].get(drift["svc_key"], {}).get("incident_id")
|
||||||
incident = self.actual_state["incidents"].get(incident_id, {}) if incident_id else {}
|
incident = self.actual_state["incidents"].get(incident_id, {}) if incident_id else {}
|
||||||
started_ts = int(_parse_ts(incident.get("started_at")))
|
started_ts = int(_parse_ts(incident.get("started_at")))
|
||||||
if started_ts:
|
if started_ts:
|
||||||
action_id = f"container-restart-{node}-{service}-{started_ts}"
|
action_id = f"{action_prefix}-{node}-{service}-{started_ts}"
|
||||||
else:
|
else:
|
||||||
action_id = f"container-restart-{node}-{service}"
|
action_id = f"{action_prefix}-{node}-{service}"
|
||||||
else:
|
|
||||||
# redeploy IDs stay bare (node-service) — out of scope for this
|
|
||||||
# fix (see commit message: no observed collision here yet), and
|
|
||||||
# _cancel_resolved_pending_actions/_ha_action_recently_completed
|
|
||||||
# do not key off redeploy ids so nothing here depends on it.
|
|
||||||
action_id = f"redeploy-{node}-{service}"
|
|
||||||
|
|
||||||
# Skip if an action for this ID is already live in any active state
|
# Skip if an action for this ID is already live in any active state
|
||||||
# (pending → approved → running). This prevents re-creation after
|
# (pending → approved → running). This prevents re-creation after
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -915,3 +916,19 @@ def test_resolve_request_flag_for_already_resolved_incident_is_removed(tmp_path)
|
||||||
assert obs.world_state["incidents"][inc_id]["status"] == "resolved"
|
assert obs.world_state["incidents"][inc_id]["status"] == "resolved"
|
||||||
assert obs.world_state["incidents"][inc_id]["resolved_reason"] == "manual_operator"
|
assert obs.world_state["incidents"][inc_id]["resolved_reason"] == "manual_operator"
|
||||||
assert not flag.exists()
|
assert not flag.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_requests_dir_is_group_writable(tmp_path, monkeypatch):
|
||||||
|
"""world/resolve-requests/ must be group-writable so an SSH operator
|
||||||
|
(group aerbot, not the observer's own user) can drop a resolve flag file
|
||||||
|
without docker exec — mkdir(mode=...) alone is masked by the process
|
||||||
|
umask, same defect/fix as executor.py's INBOX_DIR_MODE (2026-08-06)."""
|
||||||
|
old_umask = os.umask(0o022)
|
||||||
|
try:
|
||||||
|
obs = _make_observer_simple(tmp_path)
|
||||||
|
finally:
|
||||||
|
os.umask(old_umask)
|
||||||
|
import observer.observer as obs_mod
|
||||||
|
|
||||||
|
mode = obs_mod.RESOLVE_REQUESTS_DIR.stat().st_mode & 0o777
|
||||||
|
assert mode == 0o775
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""action_id uniqueness for container_restart (2026-08-26 fix).
|
"""action_id uniqueness for container_restart (2026-08-26) and redeploy
|
||||||
|
(2026-08-27, same latent risk, same fix).
|
||||||
|
|
||||||
Before this fix, _generate_recommendation() built container_restart action
|
Before this fix, _generate_recommendation() built container_restart action
|
||||||
ids as the bare `container-restart-<node>-<service>` — no timestamp, no
|
ids as the bare `container-restart-<node>-<service>` — no timestamp, no
|
||||||
|
|
@ -172,10 +173,51 @@ def test_fallback_bare_id_stable_across_repeated_calls(sup, tmp_path):
|
||||||
assert pending[0].name == "container-restart-piha-paperless.json"
|
assert pending[0].name == "container-restart-piha-paperless.json"
|
||||||
|
|
||||||
|
|
||||||
def test_redeploy_action_id_stays_bare(sup, tmp_path):
|
def test_redeploy_action_id_falls_back_to_bare_without_incident(sup, tmp_path):
|
||||||
"""Non-container_restart drift (redeploy path) is out of scope for this
|
"""Non-container_restart drift (redeploy path) with no linked incident
|
||||||
fix and keeps its existing bare node-service id."""
|
record keeps the pre-fix bare node-service id, same fallback as
|
||||||
|
container_restart."""
|
||||||
drift = _drift("piha", "outline", trigger_type="service_unhealthy")
|
drift = _drift("piha", "outline", trigger_type="service_unhealthy")
|
||||||
sup._generate_recommendation(drift)
|
sup._generate_recommendation(drift)
|
||||||
|
|
||||||
assert (tmp_path / "actions" / "pending" / "redeploy-piha-outline.json").exists()
|
assert (tmp_path / "actions" / "pending" / "redeploy-piha-outline.json").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_redeploy_action_id_carries_incident_started_at_suffix(sup, tmp_path):
|
||||||
|
"""Domknięcie COMMIT 2 z task/incident-resolve-fix (2026-08-27): redeploy
|
||||||
|
ids carry the same started_at suffix as container_restart, closing the
|
||||||
|
same latent node+service collision risk."""
|
||||||
|
_seed_incident(sup, "piha", "outline", "inc-3000-piha-outline", started_at=3000)
|
||||||
|
drift = _drift("piha", "outline", trigger_type="service_unhealthy")
|
||||||
|
|
||||||
|
sup._generate_recommendation(drift)
|
||||||
|
|
||||||
|
pending = _pending(tmp_path)
|
||||||
|
assert len(pending) == 1
|
||||||
|
assert pending[0].name == "redeploy-piha-outline-3000.json"
|
||||||
|
|
||||||
|
|
||||||
|
def test_redeploy_new_incident_after_old_completed_gets_different_action_id(sup, tmp_path):
|
||||||
|
"""Same collision scenario as the container_restart case, for redeploy:
|
||||||
|
a second, later incident for the same node/service must not collide
|
||||||
|
with the first incident's already-completed action file."""
|
||||||
|
_seed_incident(sup, "piha", "outline", "inc-3000-piha-outline", started_at=3000)
|
||||||
|
drift = _drift("piha", "outline", trigger_type="service_unhealthy")
|
||||||
|
sup._generate_recommendation(drift)
|
||||||
|
|
||||||
|
first_action_path = tmp_path / "actions" / "pending" / "redeploy-piha-outline-3000.json"
|
||||||
|
assert first_action_path.exists()
|
||||||
|
|
||||||
|
completed_dir = tmp_path / "actions" / "completed"
|
||||||
|
completed_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
first_action = json.loads(first_action_path.read_text())
|
||||||
|
first_action["status"] = "completed"
|
||||||
|
(completed_dir / first_action_path.name).write_text(json.dumps(first_action))
|
||||||
|
first_action_path.unlink()
|
||||||
|
|
||||||
|
_seed_incident(sup, "piha", "outline", "inc-4000-piha-outline", started_at=4000)
|
||||||
|
sup._generate_recommendation(drift)
|
||||||
|
|
||||||
|
second_action_path = tmp_path / "actions" / "pending" / "redeploy-piha-outline-4000.json"
|
||||||
|
assert second_action_path.exists()
|
||||||
|
assert json.loads((completed_dir / "redeploy-piha-outline-3000.json").read_text())["status"] == "completed"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue