job-alert-tuner
Modulo 1.2.2 del progetto Job Hunter. Analizza il source-log prodotto dalla routine job-watch e risponde a tre domande: quali ricerche portano volume, quali si sovrappongono, quali portano rumore (annunci fuori scope). L'output informa; le modifiche restano all'utente, tramite job-search-profile (1.2) per i criteri e job-alert-config (1.2.1) per riallineare gli alert.
Precondizioni di readiness
Due prerequisiti distinti, non uno. Le metriche 1-4 (sourcing) richiedono il source-log; le metriche 5-6 (staging) richiedono staging/. Sono indipendenti: se manca l'uno, calcola comunque le metriche dell'altro e dichiara quale gruppo non è calcolabile e perché. Non bloccare l'intera skill per un prerequisito che serve solo a metà dell'analisi — e non usare l'uno come surrogato dell'altro.
Prima di calcolare metriche, verifica il prerequisito minimo di questa skill: esiste almeno un file source-log/YYYY-MM.jsonl con almeno una riga. Attenzione: qui il gap NON è l'onboarding — a differenza delle altre skill funzionali, il prerequisito mancante è un passo successivo. Il source-log lo produce la routine job-watch, che dev'essere già girata almeno una volta. Se la cartella source-log/ è assente o vuota, o i file esistono ma hanno 0 righe, non procedere e non ricostruire il log da fonti alternative: fermati e spiega il gap reale con una frase specifica, es.: "Non ho ancora metriche da analizzare: il source-log lo scrive la routine job-watch, che finora non ha prodotto dati — facciamola girare almeno una volta (skill job-watch) e poi torniamo qui al tuning.". (È il "caso base" già descritto sotto in dettaglio: qui è la guardia d'ingresso esplicita, con la stessa postura — nessun crash, nessuna ricostruzione inventata.)
Contratto dati
Il source-log vive nel repo, in source-log/YYYY-MM.jsonl (un file JSONL per mese, rotazione mensile). Lo schema completo — chiavi, enum, campo intento_id, semantica "una riga = un annuncio osservato da una ricerca in una run" — è in references/source-log-schema.md: leggilo prima di ogni analisi, è il contratto che questa skill ha definito e che la routine job-watch rispetta scrivendolo a ogni run.
Caso base da gestire per primo: il log non c'è (previsto, non un errore)
Il source-log è scritto dalla routine job-watch: se manca, la routine non ha ancora girato (o l'ultima run è fallita prima di scrivere — controlla source-log/runs.jsonl, il ledger delle run). Non è un errore, è uno stato previsto. Quindi, prima di tutto:
- Guarda nella cartella
source-log/ del repo (file locali in sessione Claude Code, nessun connettore): cerca i file YYYY-MM.jsonl. Se una finestra temporale richiesta copre più mesi, i file mensili corrispondenti vanno concatenati (leggendoli tutti riga per riga).
- Nessun file / cartella vuota → spiega con calma: "il source-log non esiste ancora: lo produce la routine job-watch a ogni run. Se è vuoto, la routine non ha ancora girato (o l'ultima run è fallita — vedi
runs.jsonl). Finché non c'è almeno una run, non ci sono metriche calcolabili." Nessun crash, nessun tentativo di ricostruire il log da altre fonti (email digest, state.json): dati parziali produrrebbero metriche fuorvianti.
- File presente ma vuoto (0 righe) → stesso messaggio, più il fatto che il file esiste ma nessuna run ha ancora loggato.
- File presente ma con poche run (1-2
run_id distinti) → calcola comunque, ma dichiara che con così poche esecuzioni le metriche sono indicative, non conclusive.
Parsing (robusto per costruzione)
Usa il code tool per leggere i JSONL riga per riga (una riga = un oggetto JSON; es. pandas.read_json(path, lines=True), oppure parsing manuale riga-per-riga se una riga è malformata). Concatena i mensili quando la finestra copre più mesi. Righe malformate (JSON non valido, chiavi obbligatorie mancanti, enum sconosciuti in esito o fonte): scartale, contale, e riporta il conteggio nell'output ("N righe malformate ignorate") — non fermarti e non correggerle inventando valori. Il JSONL è robusto proprio qui: una riga rotta non compromette le altre. Se le righe malformate superano ~20% del totale, segnala che il log è probabilmente corrotto o che la routine ha deviato dal contratto: in quel caso le metriche non sono affidabili e la cosa va sistemata alla fonte.
Metriche (definizioni esatte)
Calcola sulle run disponibili (o su una finestra se l'utente la chiede, es. "ultimo mese" = il/i file YYYY-MM.jsonl corrispondenti, o filtrando run_id). Ogni metrica è raggruppabile per intento_id (D2): puoi darle sia per singola ricerca_id sia aggregate per intento, secondo cosa chiede l'utente.
- Numerosità per ricerca — per ogni
ricerca_id: righe totali, media per run, trend (prime run vs ultime). Aggregabile per intento_id. Una ricerca che porta ~0 annunci per molte run è morta o mal configurata.
- Overlap tra ricerche — per ogni coppia di
ricerca_id: quanti annuncio_id condividono nella stessa run (contando anche le righe scartato_dedup, che esistono apposta). Deriva per ogni ricerca la resa unica: quota di annunci portati SOLO da quella ricerca. Resa unica bassa + alto overlap con un'altra = candidata alla rimozione. Overlap TRA intenti (intento_id diversi che portano gli stessi annuncio_id): è un caso a sé da segnalare — non è necessariamente un errore (due intenti possono legittimamente sovrapporsi), ma se è alto vale la pena dirlo, perché significa che due intenti stanno cercando quasi la stessa cosa.
- Tasso fuori scope per ricerca — quota di righe con
esito in {scartato_lingua, scartato_livello, scartato_idoneita} sul totale della ricerca (aggregabile per intento_id). Alto fuori scope = query troppo larga (es. location che pesca annunci in lingua esclusa) — costa tempo di pipeline anche se il digest resta pulito. ⚠️ scartato_idoneita (offerte riservate alle categorie protette) va contato, ma la sua leva non è sempre la query: una keyword LinkedIn come "Java Developer" non può escludere Java Developer (Categorie Protette) a monte — se un ricerca_id ne porta molte in modo strutturale, è un fatto della piattaforma da annotare, non necessariamente una query da restringere. NON includere qui gli esiti scartato_ruolo/scartato_location: sono career_page-only e non misurano la qualità di una query (una career page non la puoi restringere, fetcha sempre tutta l'azienda) — vanno nella metrica 3-bis, altrimenti falsano il segnale "query da restringere".
3-bis. Rumore per-azienda del canale career_page (solo se il log contiene righe fonte: career_page) — per ogni azienda_fonte: quota di scartato_ruolo + scartato_location sul totale portato da quell'azienda, e conteggio delle righe incluso_* sopravvissute. È il segnale di valore cross-source: un'azienda che porta 190 righe di cui 189 scartate per ruolo/location e 1 inclusa (già vista anche su Indeed) è candidata a attiva: false in companies.yaml — non perché la query sia sbagliata (non c'è query), ma perché quella specifica career page rende poco. Distinto dalla metrica 3 proprio perché l'azione è diversa: qui si agisce sull'anagrafica aziende (job-search-profile), non sui criteri di ricerca.
- (Di contorno) quota
non_lavorato_cap per intento_id, segmentata per priorita (alta/media/bassa, campo sulle righe non_lavorato_cap da 2026-09). Il cap taglia il fondo della classifica di priorità (passo 5-0 di job-watch): se il tagliato è tutto bassa, il cap sta funzionando come previsto e alzarlo darebbe solo rumore; se compaiono media/alta in modo ricorrente su un intento, max_annunci_per_esecuzione è troppo stretto per quell'intento — raccomanda un aumento mirato (override nell'intento) o criteri più stretti. Riporta anche il numero di run consecutive in cui il cap ha morso.
Metriche 5 e 6 — input diverso: staging/, non il source-log
Le due metriche seguenti non leggono il source-log: leggono staging/*/fit.yaml e staging/*/staging.yaml. Non cercare i loro campi nel contratto del log, non ci sono. Sono qui perché rispondono alla stessa famiglia di domande ("il sistema sta rendendo?"), ma guardano a valle del sourcing invece che a monte. Valgono le stesse regole di robustezza: file illeggibile o campo assente → scartalo, contalo, dillo; non ricostruire.
Distribuzione degli score e potere discriminante (input: staging/*/fit.yaml, campo valutazione→score; in alternativa il campo score proiettato in staging.yaml, più comodo da leggere in blocco)
Distribuzione dei quattro livelli forte|buono|parziale|debole sul totale valutato, complessiva e per intento_id.
- Soglia minima: 20 valutazioni. Sotto, non calcolare: dillo e fermati su questa metrica (le altre restano valide). Una distribuzione su 8 voci non dice nulla e invita a conclusioni che i dati non reggono.
- Segnala esplicitamente le classi vuote o quasi vuote. Una scala a 4 livelli in cui un livello non viene mai assegnato su un campione ≥ 30 non sta discriminando, ed è un fatto da riportare, non da lasciare implicito in una tabella.
- Separa le valutazioni a confidenza bassa. Le voci con
confidenza: bassa (campo opzionale in staging.yaml/fit.yaml, default piena quando assente — contratto in job-watch/references/staging-schema.md) hanno uno score orientativo, prodotto senza il corpo della JD: tipicamente alert LinkedIn con solo titolo+azienda+location, e nel source-log portano esito: incluso_da_verificare. Riporta la distribuzione con e senza quelle voci, e la loro quota sul totale: mischiarle alle altre fa sembrare rappresentativo un campione che non lo è — stesso vizio di non_lavorato_cap, causa diversa. Se la quota è alta, è di per sé un risultato da dire (buona parte della coda è valutata a informazione incompleta).
- Incrocia SEMPRE con
non_lavorato_cap (metrica 4). Se il cap morde, la distribuzione osservata non è un campione rappresentativo delle offerte disponibili: le voci tagliate dal cap non sono mai state valutate. Da 2026-09 il cap taglia il fondo della classifica di priorità (passo 5-0 di job-watch), non più l'ordine di raccolta: le righe non_lavorato_cap portano il campo priorita. Leggilo: se il tagliato è tutto priorita: bassa, la distribuzione degli score è comunque ragionevolmente rappresentativa della parte utile del bacino; se compaiono media/alta, il cap è troppo stretto per quell'intento e va detto. Dichiara il limite dell'analisi in entrambi i casi, ma con questa sfumatura invece che come "campione tagliato a caso".
Umiltà causale (obbligatoria, stessa postura della metrica 3-bis). Se un livello è vuoto, ci sono almeno tre spiegazioni con azioni diverse e incompatibili, e questa metrica da sola non le distingue. Presentale come ipotesi alternative, mai come diagnosi:
- il sourcing non pesca ruoli di quel livello → si agisce sui criteri (
job-search-profile);
- la rubrica di
role-fit è tarata in modo che quel livello sia di fatto irraggiungibile → si agisce sulla rubrica;
- il cap ha tagliato le candidate migliori prima che venissero valutate → si agisce su
parametri_esecuzione, o si introduce un pre-ordinamento prima del cap.
Non scrivere mai "la tua scala è sbagliata": con un campione tagliato dal cap non è deducibile. Scrivi cosa si osserva, le tre letture possibili, e cosa servirebbe per distinguerle (tipicamente: una run senza cap, o il cap alzato per qualche giro).
Gap di conversione staging → decisione (input: staging/*/staging.yaml)
Non è una metrica di sourcing: misura quanto del lavoro prodotto dal batch resta inutilizzato. Riporta:
- quota di voci
pending sul totale (pending / approved / discarded / expired);
- età della voce
pending più vecchia (dal run_id che l'ha creata) e mediana dell'età delle pending;
- conteggio delle voci con
materials_generated: true mai promosse, cioè materiali pre-generati che nessuno ha mai letto;
- conteggio delle voci con
materials_flagged: true (gate di veridicità rosso — vanno guardate prima di essere usate).
È spesso l'informazione più azionabile che il sistema possa dare: se le pending crescono in modo monotono e nulla viene promosso o scartato, il collo di bottiglia non è il sourcing ma la revisione, e nessuna metrica di tuning delle ricerche lo risolve. Dillo esplicitamente quando i numeri lo mostrano — con la stessa franchezza (e senza colpevolizzare: è un'osservazione sul sistema, non sull'utente).
Nota per il futuro (non requisito v1): applications/<id>/application.yaml porta un intent_id; incrociarlo col log abiliterebbe una metrica candidature-per-intento (quali intenti non solo portano volume, ma convertono in candidature reali) a costo quasi zero — da tenere presente, non da implementare ora. Prerequisito reale: serve che applications/ contenga almeno ~5 esiti oltre lo stato "candidata"; finché è vuota o quasi, questa metrica non è calcolabile e non va simulata sui dati di staging (che misurano il giudizio del sistema, non l'esito del mercato).
Output (in chat)
- Una tabella riassuntiva per
ricerca_id (con la colonna intento_id, così si legge anche aggregata per intento): volume medio/run, resa unica %, fuori scope %, note.
- Le coppie con overlap rilevante, distinguendo overlap dentro lo stesso intento da overlap tra intenti diversi.
2-bis. Se hai calcolato le metriche 5 e 6: la distribuzione degli score (con il limite dovuto al cap dichiarato accanto, non in nota) e lo stato della coda di staging. Se la metrica 6 mostra una coda che cresce senza essere consumata, mettila in testa all'output, non in fondo: è il vincolo che rende poco rilevante tutto il resto.
- 2-4 raccomandazioni qualitative, nello stile del progetto (pesate, non binarie): non "elimina la ricerca X" ma "X porta il 90% di annunci già portati da Y e quasi nulla di unico: candidata alla rimozione — la decisione è tua". Ogni raccomandazione indica anche DOVE si agisce: criteri →
job-search-profile, alert sulle piattaforme → job-alert-config. Rendile azionabili: per le ricerche linkedin_alert/indeed_alert, risolvi il ricerca_id opaco in searches/alerts-registry.yaml (se presente) e cita l'etichetta leggibile (es. "Integration Engineer — Unione Europea") e, se serve, l'URL/keywords dell'alert reale da disattivare sulla piattaforma — così l'utente sa esattamente quale avviso toccare, non un id criptico.
- Le soglie usate (es. "resa unica < 15% = bassa") sono euristiche dichiarate nel testo, mai tagli automatici.
Cosa NON fare
- Non modificare i file in
searches/ né generare istruzioni alert: solo raccomandare e rimandare a 1.2 / 1.2.1.
- Non ricostruire dati mancanti da fonti alternative (digest email, state.json).
- Non presentare metriche su 1-2 run come conclusive.
- Non inventare chiavi o esiti fuori dal contratto: se il log contiene valori non previsti, è la routine che ha deviato — segnalalo, non adattare silenziosamente il contratto.
1---2name: job-alert-tuner3description: Metriche di tuning delle ricerche del sistema Job Hunter a partire dal source-log nel repo (source-log/YYYY-MM.jsonl): overlap tra ricerche e tra intenti, numerosità per ricerca, tasso di annunci fuori scope. Analizza anche lo stato dello staging: distribuzione degli score di fit e quanto lavoro del batch resta non revisionato. Usa SEMPRE questa skill quando l'utente chiede: "come stanno andando le ricerche/gli alert", "quali ricerche rendono", "ci sono alert doppi/inutili", "tuning del sourcing", "metriche della routine", "conviene togliere qualche alert", "quante offerte sono ferme in staging", "come sono distribuiti i fit", "perché non trovo mai fit forti", o vuole capire se le fonti del digest producono rumore. Produce analisi e raccomandazioni in chat: NON modifica da sola profilo o alert.4---56# job-alert-tuner78Modulo 1.2.2 del progetto Job Hunter. Analizza il `source-log` prodotto dalla routine `job-watch` e risponde a tre domande: quali ricerche portano volume, quali si sovrappongono, quali portano rumore (annunci fuori scope). L'output informa; le modifiche restano all'utente, tramite `job-search-profile` (1.2) per i criteri e `job-alert-config` (1.2.1) per riallineare gli alert.910## Precondizioni di readiness1112**Due prerequisiti distinti, non uno.** Le metriche 1-4 (sourcing) richiedono il `source-log`; le metriche 5-6 (staging) richiedono `staging/`. Sono indipendenti: se manca l'uno, calcola comunque le metriche dell'altro e dichiara quale gruppo non è calcolabile e perché. Non bloccare l'intera skill per un prerequisito che serve solo a metà dell'analisi — e non usare l'uno come surrogato dell'altro.1314Prima di calcolare metriche, verifica il prerequisito minimo di questa skill: esiste almeno un file `source-log/YYYY-MM.jsonl` con almeno una riga. **Attenzione: qui il gap NON è l'onboarding** — a differenza delle altre skill funzionali, il prerequisito mancante è un passo successivo. Il source-log lo produce la routine `job-watch`, che dev'essere già girata almeno una volta. Se la cartella `source-log/` è assente o vuota, o i file esistono ma hanno 0 righe, non procedere e non ricostruire il log da fonti alternative: fermati e spiega il gap reale con una frase specifica, es.: "Non ho ancora metriche da analizzare: il source-log lo scrive la routine `job-watch`, che finora non ha prodotto dati — facciamola girare almeno una volta (skill `job-watch`) e poi torniamo qui al tuning.". (È il "caso base" già descritto sotto in dettaglio: qui è la guardia d'ingresso esplicita, con la stessa postura — nessun crash, nessuna ricostruzione inventata.)1516## Contratto dati1718Il `source-log` vive nel repo, in `source-log/YYYY-MM.jsonl` (un file JSONL per mese, rotazione mensile). Lo schema completo — chiavi, enum, campo `intento_id`, semantica "una riga = un annuncio osservato da una ricerca in una run" — è in `references/source-log-schema.md`: **leggilo prima di ogni analisi**, è il contratto che questa skill ha definito e che la routine `job-watch` rispetta scrivendolo a ogni run.1920## Caso base da gestire per primo: il log non c'è (previsto, non un errore)2122Il `source-log` è scritto dalla routine `job-watch`: se manca, la routine non ha ancora girato (o l'ultima run è fallita prima di scrivere — controlla `source-log/runs.jsonl`, il ledger delle run). Non è un errore, è uno stato previsto. Quindi, prima di tutto:23241. Guarda nella cartella `source-log/` del repo (file locali in sessione Claude Code, nessun connettore): cerca i file `YYYY-MM.jsonl`. Se una finestra temporale richiesta copre più mesi, i file mensili corrispondenti vanno **concatenati** (leggendoli tutti riga per riga).252. **Nessun file / cartella vuota** → spiega con calma: "il source-log non esiste ancora: lo produce la routine job-watch a ogni run. Se è vuoto, la routine non ha ancora girato (o l'ultima run è fallita — vedi `runs.jsonl`). Finché non c'è almeno una run, non ci sono metriche calcolabili." Nessun crash, nessun tentativo di ricostruire il log da altre fonti (email digest, state.json): dati parziali produrrebbero metriche fuorvianti.263. **File presente ma vuoto (0 righe)** → stesso messaggio, più il fatto che il file esiste ma nessuna run ha ancora loggato.274. **File presente ma con poche run** (1-2 `run_id` distinti) → calcola comunque, ma dichiara che con così poche esecuzioni le metriche sono indicative, non conclusive.2829## Parsing (robusto per costruzione)3031Usa il code tool per leggere i JSONL **riga per riga** (una riga = un oggetto JSON; es. `pandas.read_json(path, lines=True)`, oppure parsing manuale riga-per-riga se una riga è malformata). Concatena i mensili quando la finestra copre più mesi. Righe malformate (JSON non valido, chiavi obbligatorie mancanti, enum sconosciuti in `esito` o `fonte`): scartale, contale, e riporta il conteggio nell'output ("N righe malformate ignorate") — non fermarti e non correggerle inventando valori. Il JSONL è robusto proprio qui: una riga rotta non compromette le altre. Se le righe malformate superano ~20% del totale, segnala che il log è probabilmente corrotto o che la routine ha deviato dal contratto: in quel caso le metriche non sono affidabili e la cosa va sistemata alla fonte.3233## Metriche (definizioni esatte)3435Calcola sulle run disponibili (o su una finestra se l'utente la chiede, es. "ultimo mese" = il/i file `YYYY-MM.jsonl` corrispondenti, o filtrando `run_id`). Ogni metrica è **raggruppabile per `intento_id`** (D2): puoi darle sia per singola `ricerca_id` sia aggregate per intento, secondo cosa chiede l'utente.36371. **Numerosità per ricerca** — per ogni `ricerca_id`: righe totali, media per run, trend (prime run vs ultime). Aggregabile per `intento_id`. Una ricerca che porta ~0 annunci per molte run è morta o mal configurata.382. **Overlap tra ricerche** — per ogni coppia di `ricerca_id`: quanti `annuncio_id` condividono nella stessa run (contando anche le righe `scartato_dedup`, che esistono apposta). Deriva per ogni ricerca la **resa unica**: quota di annunci portati SOLO da quella ricerca. Resa unica bassa + alto overlap con un'altra = candidata alla rimozione. **Overlap TRA intenti** (`intento_id` diversi che portano gli stessi `annuncio_id`): è un caso a sé da segnalare — non è necessariamente un errore (due intenti possono legittimamente sovrapporsi), ma se è alto vale la pena dirlo, perché significa che due intenti stanno cercando quasi la stessa cosa.393. **Tasso fuori scope per ricerca** — quota di righe con `esito` in {`scartato_lingua`, `scartato_livello`, `scartato_idoneita`} sul totale della ricerca (aggregabile per `intento_id`). Alto fuori scope = query troppo larga (es. location che pesca annunci in lingua esclusa) — costa tempo di pipeline anche se il digest resta pulito. ⚠️ `scartato_idoneita` (offerte riservate alle categorie protette) va contato, ma **la sua leva non è sempre la query**: una keyword LinkedIn come `"Java Developer"` non può escludere `Java Developer (Categorie Protette)` a monte — se un `ricerca_id` ne porta molte in modo strutturale, è un fatto della piattaforma da annotare, non necessariamente una query da restringere. **NON includere qui gli esiti `scartato_ruolo`/`scartato_location`**: sono career_page-only e non misurano la qualità di una query (una career page non la puoi restringere, fetcha sempre tutta l'azienda) — vanno nella metrica 3-bis, altrimenti falsano il segnale "query da restringere".403-bis. **Rumore per-azienda del canale `career_page`** (solo se il log contiene righe `fonte: career_page`) — per ogni `azienda_fonte`: quota di `scartato_ruolo` + `scartato_location` sul totale portato da quell'azienda, e conteggio delle righe `incluso_*` sopravvissute. È il segnale di **valore cross-source**: un'azienda che porta 190 righe di cui 189 scartate per ruolo/location e 1 inclusa (già vista anche su Indeed) è candidata a `attiva: false` in `companies.yaml` — non perché la query sia sbagliata (non c'è query), ma perché quella specifica career page rende poco. Distinto dalla metrica 3 proprio perché l'azione è diversa: qui si agisce sull'anagrafica aziende (`job-search-profile`), non sui criteri di ricerca.414. (Di contorno) **quota `non_lavorato_cap`** per `intento_id`, **segmentata per `priorita`** (`alta`/`media`/`bassa`, campo sulle righe `non_lavorato_cap` da 2026-09). Il cap taglia il fondo della classifica di priorità (passo 5-0 di `job-watch`): se il tagliato è tutto `bassa`, il cap sta funzionando come previsto e alzarlo darebbe solo rumore; se compaiono `media`/`alta` in modo ricorrente su un intento, `max_annunci_per_esecuzione` è troppo stretto per quell'intento — raccomanda un aumento mirato (override nell'intento) o criteri più stretti. Riporta anche il numero di run consecutive in cui il cap ha morso.4243### Metriche 5 e 6 — input diverso: `staging/`, non il source-log4445Le due metriche seguenti **non leggono il source-log**: leggono `staging/*/fit.yaml` e `staging/*/staging.yaml`. Non cercare i loro campi nel contratto del log, non ci sono. Sono qui perché rispondono alla stessa famiglia di domande ("il sistema sta rendendo?"), ma guardano a valle del sourcing invece che a monte. Valgono le stesse regole di robustezza: file illeggibile o campo assente → scartalo, contalo, dillo; non ricostruire.46475. **Distribuzione degli score e potere discriminante** (input: `staging/*/fit.yaml`, campo `valutazione`→`score`; in alternativa il campo `score` proiettato in `staging.yaml`, più comodo da leggere in blocco)4849 Distribuzione dei quattro livelli `forte|buono|parziale|debole` sul totale valutato, complessiva **e** per `intento_id`.5051 - **Soglia minima: 20 valutazioni.** Sotto, non calcolare: dillo e fermati su questa metrica (le altre restano valide). Una distribuzione su 8 voci non dice nulla e invita a conclusioni che i dati non reggono.52 - **Segnala esplicitamente le classi vuote o quasi vuote.** Una scala a 4 livelli in cui un livello non viene mai assegnato su un campione ≥ 30 **non sta discriminando**, ed è un fatto da riportare, non da lasciare implicito in una tabella.53 - **Separa le valutazioni a confidenza bassa.** Le voci con `confidenza: bassa` (campo opzionale in `staging.yaml`/`fit.yaml`, default `piena` quando assente — contratto in `job-watch/references/staging-schema.md`) hanno uno score **orientativo**, prodotto senza il corpo della JD: tipicamente alert LinkedIn con solo titolo+azienda+location, e nel source-log portano `esito: incluso_da_verificare`. Riporta la distribuzione **con e senza** quelle voci, e la loro quota sul totale: mischiarle alle altre fa sembrare rappresentativo un campione che non lo è — stesso vizio di `non_lavorato_cap`, causa diversa. Se la quota è alta, è di per sé un risultato da dire (buona parte della coda è valutata a informazione incompleta).54 - **Incrocia SEMPRE con `non_lavorato_cap`** (metrica 4). Se il cap morde, la distribuzione osservata **non è un campione rappresentativo** delle offerte disponibili: le voci tagliate dal cap non sono mai state valutate. Da 2026-09 il cap taglia il **fondo della classifica di priorità** (passo 5-0 di `job-watch`), non più l'ordine di raccolta: le righe `non_lavorato_cap` portano il campo `priorita`. **Leggilo**: se il tagliato è tutto `priorita: bassa`, la distribuzione degli score è comunque ragionevolmente rappresentativa della parte utile del bacino; se compaiono `media`/`alta`, il cap è troppo stretto per quell'intento e va detto. **Dichiara il limite dell'analisi** in entrambi i casi, ma con questa sfumatura invece che come "campione tagliato a caso".5556 **Umiltà causale (obbligatoria, stessa postura della metrica 3-bis).** Se un livello è vuoto, ci sono almeno tre spiegazioni con azioni **diverse e incompatibili**, e questa metrica da sola non le distingue. Presentale come ipotesi alternative, mai come diagnosi:57 1. il sourcing non pesca ruoli di quel livello → si agisce sui criteri (`job-search-profile`);58 2. la rubrica di `role-fit` è tarata in modo che quel livello sia di fatto irraggiungibile → si agisce sulla rubrica;59 3. il cap ha tagliato le candidate migliori prima che venissero valutate → si agisce su `parametri_esecuzione`, o si introduce un pre-ordinamento prima del cap.6061 Non scrivere mai "la tua scala è sbagliata": con un campione tagliato dal cap non è deducibile. Scrivi cosa si osserva, le tre letture possibili, e cosa servirebbe per distinguerle (tipicamente: una run senza cap, o il cap alzato per qualche giro).62636. **Gap di conversione staging → decisione** (input: `staging/*/staging.yaml`)6465 Non è una metrica di sourcing: misura quanto del lavoro prodotto dal batch resta **inutilizzato**. Riporta:66 - quota di voci `pending` sul totale (`pending` / `approved` / `discarded` / `expired`);67 - **età della voce `pending` più vecchia** (dal `run_id` che l'ha creata) e mediana dell'età delle pending;68 - conteggio delle voci con `materials_generated: true` **mai promosse**, cioè materiali pre-generati che nessuno ha mai letto;69 - conteggio delle voci con `materials_flagged: true` (gate di veridicità rosso — vanno guardate prima di essere usate).7071 È spesso l'informazione più azionabile che il sistema possa dare: se le pending crescono in modo monotono e nulla viene promosso o scartato, il collo di bottiglia non è il sourcing ma la **revisione**, e nessuna metrica di tuning delle ricerche lo risolve. Dillo esplicitamente quando i numeri lo mostrano — con la stessa franchezza (e senza colpevolizzare: è un'osservazione sul sistema, non sull'utente).7273Nota per il futuro (non requisito v1): `applications/<id>/application.yaml` porta un `intent_id`; incrociarlo col log abiliterebbe una metrica *candidature-per-intento* (quali intenti non solo portano volume, ma convertono in candidature reali) a costo quasi zero — da tenere presente, non da implementare ora. **Prerequisito reale**: serve che `applications/` contenga almeno ~5 esiti oltre lo stato "candidata"; finché è vuota o quasi, questa metrica non è calcolabile e non va simulata sui dati di staging (che misurano il giudizio del sistema, non l'esito del mercato).7475## Output (in chat)76771. Una tabella riassuntiva per `ricerca_id` (con la colonna `intento_id`, così si legge anche aggregata per intento): volume medio/run, resa unica %, fuori scope %, note.782. Le coppie con overlap rilevante, distinguendo overlap *dentro* lo stesso intento da overlap *tra* intenti diversi.792-bis. Se hai calcolato le metriche 5 e 6: la distribuzione degli score (con il limite dovuto al cap dichiarato accanto, non in nota) e lo stato della coda di staging. Se la metrica 6 mostra una coda che cresce senza essere consumata, mettila **in testa** all'output, non in fondo: è il vincolo che rende poco rilevante tutto il resto.803. **2-4 raccomandazioni qualitative**, nello stile del progetto (pesate, non binarie): non "elimina la ricerca X" ma "X porta il 90% di annunci già portati da Y e quasi nulla di unico: candidata alla rimozione — la decisione è tua". Ogni raccomandazione indica anche DOVE si agisce: criteri → `job-search-profile`, alert sulle piattaforme → `job-alert-config`. **Rendile azionabili**: per le ricerche `linkedin_alert`/`indeed_alert`, risolvi il `ricerca_id` opaco in `searches/alerts-registry.yaml` (se presente) e cita l'**etichetta leggibile** (es. "Integration Engineer — Unione Europea") e, se serve, l'URL/keywords dell'alert reale da disattivare sulla piattaforma — così l'utente sa esattamente quale avviso toccare, non un id criptico.814. Le soglie usate (es. "resa unica < 15% = bassa") sono euristiche dichiarate nel testo, mai tagli automatici.8283## Cosa NON fare8485- Non modificare i file in `searches/` né generare istruzioni alert: solo raccomandare e rimandare a 1.2 / 1.2.1.86- Non ricostruire dati mancanti da fonti alternative (digest email, state.json).87- Non presentare metriche su 1-2 run come conclusive.88- Non inventare chiavi o esiti fuori dal contratto: se il log contiene valori non previsti, è la routine che ha deviato — segnalalo, non adattare silenziosamente il contratto.