ADR 004: Package CLI Decentralizzata
Stato: Attivo Decisore: Architecture Lead Data: 2026-04-15 (sprint v0.7.0, D062-B / D063 / D064)
Contesto
La CLI originale di Zenzic era contenuta in un singolo file: src/zenzic/cli.py.
Nel corso del ciclo di rilascio v0.6.x, quel file è cresciuto fino a superare
le 2.000 righe, contenendo sei responsabilità concettualmente distinte in
un unico namespace:
| Responsabilità | Esempi |
|---|---|
| Comandi di analisi | check links, check orphans, check all |
| Ispezione del motore | inspect capabilities |
| Comandi di manutenzione | clean |
| Showcase lab | zenzic lab — 11 atti interattivi |
| Operazioni standalone | diff, score, init |
| Helper UI/output condivisi | banner, console, costruttore dell'exclusion manager |
Questo monolite creava problemi composti:
-
Rischio di importazione circolare. Con la crescita dei moduli
core/, icontributori erano tentati di importare direttamente le utility di
cli.pydal core, invertendo la direzione delle dipendenze. -
Stato UI disperso. L'oggetto Rich
consoleveniva istanziato più volte inscope di funzioni diverse, causando formattazione dell'output incoerente e race condition negli ambienti di test.
-
Fallimento dell'isolamento dei test. Ogni test che toccava un comando CLI
doveva importare l'intero
cli.py— incluso il lab showcase, il display live di Rich e tutti i sub-app Typer. Questo aumentava il tempo di avvio dei test e rendeva il mocking inaffidabile. -
Attrito per i contributori. Un nuovo contributore che aggiungeva un comando
check non aveva un chiaro segnale "dove va questo?" dalla struttura dei file.
Decisione
src/zenzic/cli.py è stato sciolto in un package src/zenzic/cli/ con la
seguente struttura modulare:
src/zenzic/cli/
__init__.py — re-export pubblici
_check.py — sub-app check: links, orphans, snippets, references, assets, all
_inspect.py — sub-app inspect: capabilities
_clean.py — sub-app clean
_lab.py — comando lab: 11 Atti (0–10), showcase interattivo
_standalone.py — comandi standalone: diff, init, score
_shared.py — helper condivisi: _build_exclusion_manager, _validate_docs_root,
_ui, console
src/zenzic/main.py è diventato il punto di ingresso Typer — un orchestratore
minimale che importa ogni sub-app e la registra sull'applicazione Typer root.
Non contiene alcuna logica di analisi.
Tre decisioni complementari sono state applicate nello stesso sprint:
-
D062-B:
src/zenzic/ui.py→src/zenzic/core/ui.py. Le primitive UI sonousate sia dalla CLI che dal Core; collocarle in
core/garantisce che il Core possa usarle senza importare dacli/, il che violerebbe la Layer Law. -
D063:
src/zenzic/lab.py→src/zenzic/cli/_lab.py. Il lab showcase èpura orchestrazione CLI — display Rich interattivi, sequenziamento degli atti, prompt utente. Appartiene al layer CLI, non adiacente al core.
-
D064 (SDK Cleansing):
run_rule()è stata estratta dacli.pye spostatain
core/rules.py. Il modulo pubblicozenzic.rulesè diventato una façade di 6 righe — retrocompatibile con qualsiasi codice di terze parti che lo importasse direttamente, assicurando che l'implementazione viva incore/.
La Layer Law (Regola R05)
Questo ADR formalizza l'invariante di direzione delle dipendenze come regola denominata:
R05 — Il Core non importa verso l'alto. I moduli in
src/zenzic/core/non devono mai importare dasrc/zenzic/cli/osrc/zenzic/main.py.
La direzione imposta è:
cli/ → core/ → models/
cli/ può importare qualsiasi cosa da core/. core/ può importare da
models/. L'inverso è permanentemente vietato. Questo garantisce che core/
possa essere usato come SDK standalone senza trascinare Typer, display live
di Rich o dipendenze I/O interattive.
Motivazione
1. Responsabilità Singola a Livello di File
Un file da 2.000 righe non è un file — è un package non dichiarato. Formalizzare
la struttura del package rende il principio di responsabilità singola visibile
nel filesystem: un contributore che cerca la logica di rilevamento degli orphan
apre _check.py, non un monolite in cui deve cercare per nome di funzione.
2. Isolamento dei Test
Dopo la suddivisione, test_cli.py può importare solo il sub-app specifico in
test. I display live Rich del lab showcase non vengono più caricati quando si
testano i check links. Il tempo di avvio per i singoli moduli di test è calato
in modo misurabile.
3. Contratto SDK
La façade zenzic.rules preserva la retrocompatibilità per qualsiasi progetto
che usasse from zenzic.rules import run_rule. Non è stato richiesto alcun
cambiamento ai percorsi di importazione per le integrazioni esistenti, nonostante
la riorganizzazione interna.
Invarianti (Non Negoziabili)
-
src/zenzic/core/non importa mai dasrc/zenzic/cli/— qualsiasi PR cheintroduce tale importazione è un candidato automatico al revert.
-
_shared.pyè l'unico posto incli/in cui l'oggetto Richconsoleviene istanziato. Tutti gli altri moduli
cli/chiamano_ui()da_shared.py. -
src/zenzic/main.pynon contiene alcuna logica di analisi — solo cablaggiodell'app Typer.
-
zenzic.rulesrimane una façade di re-export. L'implementazione vive incore/rules.py.
Conseguenze
-
I nuovi comandi CLI vengono aggiunti al modulo
cli/_*.pyappropriato, non aun monolite generico.
-
La funzione
run_rule()è importabile sia comezenzic.rules.run_rule(façadepubblica) che come
zenzic.core.rules.run_rule(diretta). Entrambi i percorsi sono stabili. -
Il lab showcase (
cli/_lab.py) può essere esteso con nuovi atti senza influiresulla superficie di test della pipeline di analisi.