Scrivere Regole Plugin
Zenzic supporta regole di linting esterne scritte in Python. Una regola plugin è
una sottoclasse di BaseRule distribuita come un normale pacchetto Python e
scoperta a runtime tramite il gruppo di entry-point zenzic.rules.
Il Contratto della Regola
Ogni regola plugin deve soddisfare tre requisiti non negoziabili. Questi vengono
verificati al momento della costruzione del motore — una regola che viola uno
qualsiasi di essi viene rifiutata con PluginContractError prima che il primo
file venga scansionato.
1. Definita a livello di modulo
La classe deve essere importabile per nome da un modulo. Le classi definite all'interno di funzioni o closure non possono essere pickled e verranno rifiutate.
# ✓ corretto — importabile come my_rules.NoDraftRule
class NoDraftRule(BaseRule): ...
# ✗ errato — non serializzabile; solleverà PluginContractError al caricamento
def make_rule():
class NoDraftRule(BaseRule): ...
return NoDraftRule()
2. Serializzabile via pickle
L'AdaptiveRuleEngine serializza le regole tramite pickle prima di
inviarle ai processi worker. Ogni attributo memorizzato su self deve essere
serializzabile.
Attributi sicuri: stringhe, numeri, pattern re.compile(), dataclass frozen,
oggetti Path, tuple di tipi sicuri.
Attributi non sicuri: file handle aperti, connessioni a database, funzioni
lambda, threading.Lock, oggetti generator, o qualsiasi oggetto che definisce
__reduce__ incorrettamente.
# ✓ regex compilata è serializzabile
class NoDraftRule(BaseRule):
_pattern = re.compile(r"(?i)\bDRAFT\b") # attributo di classe
# ✓ funziona anche come attributo di istanza impostato in __init__
class NoDraftRule(BaseRule):
def __init__(self) -> None:
self._pattern = re.compile(r"(?i)\bDRAFT\b")
3. Pura e deterministica
check() e check_vsm() devono:
- Mai aprire file, fare richieste di rete, o chiamare sottoprocessi.
- Sempre restituire lo stesso output per lo stesso input — nessuna casualità, nessuna dipendenza da stato globale mutabile.
- Non mutare i propri argomenti (
file_path,text,vsm,anchors_cache).
Una regola che scrive su un contatore globale sembrerà funzionare in
modalità sequenziale, ma produrrà risultati non deterministici e
silenziosamente errati in modalità parallela. I processi worker
ricevono ciascuno una copia pickle indipendente del motore — le mutazioni
sono locali al worker e vengono scartate al completamento. Tutto lo
stato deve essere restituito come oggetti RuleFinding.
Esempio minimale
# my_org_rules/rules.py
import re
from pathlib import Path
from zenzic.rules import BaseRule, RuleFinding
class NoInternalHostnameRule(BaseRule):
"""Segnala le occorrenze dell'hostname interno nella documentazione pubblica."""
_pattern = re.compile(r"internal\.corp\.example\.com", re.IGNORECASE)
@property
def rule_id(self) -> str:
return "MYORG-001"
def check(self, file_path: Path, text: str) -> list[RuleFinding]:
findings = []
for lineno, line in enumerate(text.splitlines(), start=1):
if self._pattern.search(line):
findings.append(
RuleFinding(
file_path=file_path,
line_no=lineno,
rule_id=self.rule_id,
message="L'hostname interno non deve apparire nella documentazione pubblica.",
severity="error",
matched_line=line,
)
)
return findings
Pacchettizzazione e registrazione
Esponi la regola tramite il gruppo di entry-point zenzic.rules nel
pyproject.toml del tuo pacchetto:
[project.entry-points."zenzic.rules"]
no-internal-hostname = "my_org_rules.rules:NoInternalHostnameRule"
Il nome dell'entry-point (no-internal-hostname) è il plugin ID che gli
utenti referenziano in zenzic.toml (vedi Abilitare i plugin
qui sotto).
Installa il tuo pacchetto insieme a Zenzic:
uv add my-org-rules # oppure: pip install my-org-rules
Dopo l'installazione, esegui zenzic plugins list per confermare che la regola venga
scoperta:
zenzic plugins list
# Installed plugin rules (2 found)
# broken-links Z001 (core) zenzic.core.rules.VSMBrokenLinkRule
# no-internal-hostname MYORG-001 (my-org-rules) my_org_rules.rules.NoInternalHostnameRule
Fast-Track: da zero a plugin in 30 secondi
Usa il comando di scaffolding per generare un pacchetto plugin pronto da modificare:
zenzic init --plugin plugin-scaffold-demo
Struttura generata:
plugin-scaffold-demo/
pyproject.toml
README.md
zenzic.toml
docs/
index.md
src/
plugin_scaffold_demo/
__init__.py
rules.py
Lo scaffold include:
- un entry-point
zenzic.rulespre-configurato inpyproject.toml - un template di classe
BaseRulea livello modulo inrules.py - una fixture docs minima, cosi
zenzic check allpassa subito
Verifica rapida:
cd plugin-scaffold-demo
uv pip install -e .
zenzic plugins list
zenzic check all
Abilitare i plugin
Le regole core (registrate sotto zenzic.rules da Zenzic stesso) sono sempre
attive. Le regole plugin esterne devono essere esplicitamente abilitate in
zenzic.toml sotto la chiave plugins:
# zenzic.toml
[build_context]
engine = "mkdocs"
plugins = ["no-internal-hostname"]
Solo i plugin elencati qui verranno caricati. L'installazione di un pacchetto
che registra regole sotto zenzic.rules senza elencarlo in plugins non ha
effetto — questo è il comportamento intenzionale del Safe Harbor: sai sempre
esattamente quali regole sono attive nel tuo progetto.
Regole VSM-aware
Le regole che devono validare i link contro la tabella di routing devono
sovrascrivere check_vsm invece di (o in aggiunta a) check. Il motore chiama
check_vsm quando una VSM e anchors_cache sono disponibili:
from collections.abc import Mapping
from zenzic.core.rules import BaseRule, RuleFinding
from zenzic.models.vsm import Route
class NoOrphanLinkRule(BaseRule):
@property
def rule_id(self) -> str:
return "MYORG-002"
def check(self, file_path, text):
return [] # nessun controllo autonomo; richiede contesto VSM
def check_vsm(self, file_path, text, vsm: Mapping[str, Route], anchors_cache):
# vsm mappa URL canonico → Route; consulta vsm[url].status
...
return [] # restituisce list[Violation]
Vedi BaseRule nella reference API per l'interfaccia completa.
Testare le regole
Usa il test helper run_rule per validare una regola con una singola chiamata —
nessuna configurazione dell'engine richiesta:
from zenzic.rules import run_rule
from my_org_rules.rules import NoInternalHostnameRule
def test_hostname_interno_rilevato():
findings = run_rule(
NoInternalHostnameRule(),
"Visita internal.corp.example.com per i dettagli.",
)
assert len(findings) == 1
assert findings[0].rule_id == "MYORG-001"
assert findings[0].severity == "error"
def test_contenuto_pulito_passa():
findings = run_rule(NoInternalHostnameRule(), "Tutto contenuto pubblico.")
assert findings == []
run_rule crea internamente un AdaptiveRuleEngine, esegue la regola e
restituisce la lista dei finding. Accetta un argomento opzionale file_path
per l'etichettatura (default: test.md).
Isolamento degli errori
Se una regola plugin solleva un'eccezione inaspettata all'interno di check()
o check_vsm(), il motore la cattura, emette un singolo finding "error" con
rule_id="RULE-ENGINE-ERROR", e continua la scansione. Un plugin difettoso non
può interrompere la scansione dell'intero albero della documentazione.
Se una regola plugin fallisce la validazione pickle anticipata al momento
del caricamento (cioè non è serializzabile), Zenzic solleva PluginContractError
immediatamente e si rifiuta di avviarsi. Correggi la regola prima di eseguire
Zenzic.
Checklist prima della pubblicazione
- Classe definita a livello di modulo (non all'interno di una funzione o lambda).
- Tutti gli attributi
self.*sono serializzabili via pickle. -
check()è pura: nessun I/O, nessun effetto collaterale, stesso output per stesso input. -
rule_idè una stringa stabile e univoca (includi un prefisso org, es."MYORG-001"). - Entry-point registrato sotto
zenzic.rulesnelpyproject.toml. - Plugin ID elencato nel
zenzic.tomldel progetto sottoplugins.
Collega la regola dal codice al flusso Sentinel in produzione:
- Registra e abilita il plugin ID nel
zenzic.tomlsottoplugins(vedi Abilitare i plugin). - Valida la regola in semantica pipeline strict:
zenzic check all --strict. Per i controlli di run, vedi Comandi CLI: Flag globali. - Se la regola è nav-aware, mappa il comportamento atteso delle Ghost Route rispetto al modello VSM: Riferimento Controlli — VSM.