Skip to contents

Questa guida è per chi mantiene il canale: oggi noi, domani chi verrà dopo. Risponde a «da dove arriva questo valore e che cosa si rompe se lo cambio», che è una domanda diversa da quella di chi compila una richiesta — per quella ci sono le work instruction nella cartella condivisa.

Questa guida nomina i ruoli, non i valori. «L’istanza che ospita il registro», «la macchina da cui gira il job», «il segreto del registro in Key Vault». Il repository è pubblico, quindi ogni volta che servirebbe un valore concreto qui si trova il nome del parametro e il rimando al foglio dei parametri di esercizio, che vive nella cartella condivisa. Dei segreti si cita il nome sotto cui stanno, mai il contenuto: il nome è un indirizzo, non una credenziale.

1. Che cos’è il canale

Sette pezzi, ciascuno con un mestiere. Nessuno dei sette sta per «il canale» da solo, e buona parte dei guasti che si vedono in esercizio sono guasti di giunzione fra due di essi.

Il registro delle richieste. Un progetto REDCap su un’istanza dedicata, dove i referenti scrivono chi deve avere che cosa. Il registro dichiara delle richieste; non descrive la realtà. Una coppia che esiste su un’istanza e non compare nel registro non significa niente — in particolare non significa «da revocare». È anche la coda di lavoro: non ce n’è una seconda, e il §4 dice perché.

Il modulo REDCap. Codice PHP installato sulle istanze servite. Espone tre operazioni — leggere lo stato di una coppia, asserire dei diritti, revocarli — dietro un segreto condiviso e una lista di indirizzi ammessi. È l’unico pezzo che tocca \UserRights, e l’unico che calcola le due impronte: quella della superficie di REDCap e quella della propria lista di indirizzi. Vive in inst/redcap-module/ dentro questo repository, e build_redcap_module() ne costruisce l’archivio prendendo il numero di versione da DESCRIPTION: le cartelle numerate sulle istanze sono deployment, non sorgenti.

Il client R. Questo pacchetto. Parla al modulo, risolve le identità, valida le richieste, calcola il confronto fra desiderato e reale, decide che cosa dichiarare al modulo quando chiede di scrivere, compone le notifiche e scrive gli esiti.

Il perimetro. Una macchina virtuale dedicata in Azure, con identità gestita, da cui partono due lavorazioni separate — l’osservatore e il giro del canale. Sul disco della macchina non c’è nessun segreto: il token si chiede al servizio di metadati dell’istanza, che risponde solo a chi gira su quella macchina, e con quel token si legge il Key Vault. L’indirizzo pubblico della macchina è statico per necessità e non per comodità — è l’unica voce ammessa nelle liste di indirizzi delle istanze, e cambiarlo le chiude tutte insieme.

L’anagrafica. Microsoft Entra, letta via Graph con l’identità gestita. Serve a due cose: stabilire chi sia la persona nominata in una richiesta, e crearle un’utenza quando non ne ha. I permessi applicativi sono due e nessun altro — leggere gli utenti e crearne — e la scelta è deliberata: nessuno dei due consente di modificare, disabilitare o cancellare un’utenza che esiste, né di toccarne le credenziali. Chi un giorno aggiungesse un terzo permesso sta allargando quel confine, e questa frase è ciò che glielo dice.

Il servizio di posta. Un servizio esterno a cui il giro consegna i messaggi. Il pacchetto non sa da dove esce la posta: riceve una chiusura a cui chiedere di mandare, e chi la costruisce è la lavorazione, che legge la chiave dal Key Vault e non la lascia uscire dalla funzione che la usa. Il servizio risponde «presa in carico», non «consegnata»: un rimbalzo avviene dopo, in modo asincrono, e non torna dentro il giro.

La telemetria e gli allarmi. Ogni esecuzione di entrambe le lavorazioni lascia un record in un workspace di Log Analytics, attraverso un punto di raccolta e una regola di raccolta. Le due lavorazioni non condividono la tabella, quindi non condividono nessuna regola: una regola del canale non può leggere un record dell’osservatore, e viceversa. Le regole attive sono undici — sei dell’osservatore, cinque del canale.

     referenti                              chi mantiene
         |                                        |
         v                                        v
  +--------------+                      +----------------------+
  | registro     |  legge le richieste  | giro del canale      |
  | richieste    |<---------------------| (client R,           |
  | (REDCap)     |  scrive identita'    |  ogni quattro ore)   |
  +--------------+  e i soli esiti      +---+--------------+---+
                                            |              |
                       stato / asserisci /  |              | manda PRIMA
                              revoca        |              | di scrivere
                                            v              v
  +-------------------------+      +--------------+  +--------------+
  | perimetro               |      | modulo       |  | servizio     |
  | - identita' gestita     |----->| REDCap sulle |  | di posta     |
  | - segreti in Key Vault  |      | istanze      |  +--------------+
  | - due lavorazioni:      |      | servite      |
  |   osservatore (giorno)  |      +------+-------+
  |   canale (4 ore)        |             |
  +----+---------------+----+             | due impronte:
       |               |                  | superficie, indirizzi
       |               | chi e' la        |
       |               | persona, e       |
       |               | creala se manca  |
       |               v                  |
       |        +--------------+          |
       |        | anagrafica   |          |
       |        | (Entra/Graph)|          |
       |        +--------------+          |
       | un record per lavorazione        |
       v                                  |
  +-----------------------------+         |
  | telemetria: due tabelle     |<--------+
  | + undici regole d'allarme   |
  +-----------------------------+

La macchina del perimetro ospita due lavorazioni, non una, ed è il punto che il diagramma dice per primo. L’osservatore gira ogni giorno, legge lo stato di ogni istanza che ha il modulo e non confronta niente — non legge il registro e non chiama provisioning_diff(), e una guardia in tests/testthat/test-runner-guardie.R lo sorveglia. Il giro del canale è un job separato, apposta: osservare e agire hanno cadenze diverse e conseguenze diverse, e un guasto nell’uno non deve fermare l’altro. Le ragioni della divisione, e perché solo l’osservatore doveva restare senza confronto, stanno nel §2.

La cadenza di quattro ore è una decisione, non un caso. Dice: chi compila una richiesta sa entro quattro ore se è formalmente corretta e, quando la scrittura è accesa, se è stata evasa. Non sono due tempi — il giro valida e applica nella stessa passata.

L’ordine dentro un giro, che è la parte da avere in testa. Il giro (1) legge il dizionario del registro e si ferma se è derivato da quello impacchettato; (2) legge le richieste; (3) risolve l’identità di ogni riga contro l’anagrafica, e crea l’utenza dove manca; (4) scrive identità e username nel registro, per una porta che non prende altro; (5) chiede a ogni istanza il cancello sull’ambito e confronta desiderato e reale per le sole coppie di cui qualcuno ha chiesto; (6) applica; (7) rilegge; (8) manda la posta; (9) scrive gli esiti. Le due scritture nel registro sono due porte distinte e restano tali: la prima porta i verbali del verdetto, la seconda l’esito.

«Manda prima, scrivi dopo» è un contratto, non un ordine di comodo. Una riga entra nel registro solo se la sua notizia è partita: se l’invio fallisce la riga resta come il registro la porta, e il giro seguente la ritrova cambiata e riprova. La coda è il registro stesso — nessun secondo stato, niente da tenere allineato. Il costo accettato in cambio è che una notifica possa arrivare due volte, mai che si perda.

La posta è spenta di default, e la ragione non è intuitiva: con «manda prima, scrivi dopo» una posta semplicemente spenta bloccherebbe ogni scrittura nel registro. Quindi senza la variabile che la accende il giro fa esattamente quello che faceva prima che la posta esistesse — calcola, scrive, non manda — così installare il pacchetto non fa partire nessun messaggio al timer successivo.

Che cosa garantisce una simulazione, e che cosa no. Un giro simulato non scrive né sulle istanze né in ambito, ma riporta l’esito nel registro: è simulated, con applied_as che dice che cosa succederebbe. Fino alla versione 0.10.0 del modulo quel «che cosa succederebbe» era l’eco della richiesta e non un piano verificato: il modulo risolveva il nome di un ruolo e quello di un gruppo solo dentro il ramo che scrive, quindi una richiesta che nominava un ruolo inesistente usciva creato in simulazione e sarebbe stata rifiutata da una scrittura vera. Dalla versione successiva le due risoluzioni girano su ogni giro — sono letture — e una simulazione rifiuta ciò che rifiuterebbe una scrittura. Resta fuori dalla garanzia l’esistenza dell’utente sull’istanza: nessun punto del modulo verifica che uno username esista lì, e REDCap accetta diritti per un login che non c’è, ed è così che nascono le righe orfane. Su un’istanza che non abbia ancora la versione nuova, un simulated va letto come «nessun ostacolo di forma», non come «funzionerà».

2. I due cancelli, e che cosa fa scattare ciascuno

Il canale non decide di scrivere in un posto solo. Due cancelli indipendenti stanno fra una richiesta e una scrittura, e rispondono a due domande diverse.

Il soffitto di major

Il primo cancello confronta le versioni, ed è l’unico punto del codice che lo fa. La finestra è espressa in major interi, non in versioni complete: due versioni dentro lo stesso major non hanno bisogno di essere ordinate, il che toglie di mezzo la trappola dell’ordinamento lessicografico.

Tre regimi:

Regime Quando Effetto
sotto il pavimento major minore del minimo il canale rifiuta ogni operazione
collaudata dentro la finestra operazione ordinaria
sopra il soffitto major maggiore del massimo la scrittura è forzata a simulazione

Il major è una dichiarazione di politica, non una garanzia di compatibilità. Dice quali versioni scegliamo di sostenere e su quali proviamo, e questo è tutto ciò che dice. La compatibilità la controlla l’altro cancello, e la misura sul parco lo mostra: la superficie da cui il canale dipende cambia dentro un major e resta identica attraverso tre major consecutivi.

La stretta di mano sull’impronta

Il secondo cancello confronta le superfici. Il modulo calcola l’impronta della superficie che il canale usa — le firme di sei metodi e la mappa dei privilegi — e la riporta a ogni risposta. Il client dichiara, quando chiede di scrivere, l’elenco delle impronte contro cui è stato collaudato. Il modulo concede la scrittura solo se la propria impronta è in quell’elenco.

check_fingerprint() declassa soltanto: può portare un’istanza da collaudata a non collaudata, mai il contrario. La direzione unica è la proprietà che rende il meccanismo sicuro da estendere.

Un’impronta ignota non è un guasto: è una versione che aspetta di essere collaudata. Il declassamento è il modo in cui il sistema chiede di essere ricollaudato invece di scrivere alla cieca.

Anche una risposta senza impronta, o con un’impronta di forma sbagliata, declassa. La forma si controlla prima del confronto e mai dopo, perché un elemento JSON che arriva come lista di uno invece che come scalare verrebbe altrimenti confrontato — e potrebbe combaciare — dove sta un valore singolo.

Misurate e certificate non sono la stessa cosa

Il registro delle superfici, un file CSV impacchettato, porta due colonne che si somigliano e non vanno confuse.

  • tested_fingerprints() restituisce le impronte misurate: qualcuno ha letto lo stato di quell’istanza e ha registrato che cosa ha visto.
  • certified_fingerprints() restituisce le sole impronte con una data di conformità: su quella superficie una run di conformità ha scritto per davvero, riletto, e visto che il risultato era quello asserito.

Il chiamante ordinario dichiara le certificate. La run di conformità dichiara le misurate, e deve: la superficie che sta per certificare non ha una data per definizione, e pretenderla ricreerebbe la trappola d’ordine — il cancello forzerebbe la simulazione, la run non potrebbe scrivere, e la data non potrebbe essere guadagnata mai. Una riga aggiunta da una semplice lettura di stato misura una superficie, non certifica che cosa succede a scriverci.

Due punti ciechi, dichiarati

L’impronta misura dichiarazioni, non comportamenti. Un cambiamento nel corpo di uno dei sei metodi, a firma invariata, resta invisibile. È per costruzione, non per difetto: la conferma che il lettore legge non allarga ciò che guarda.

L’impronta ha un ingresso che non è la versione di REDCap. La mappa dei privilegi può guadagnare un campo in base a un’impostazione dell’istanza, fuori dal blocco condizionato dal progetto. Se quell’impostazione venisse accesa, l’impronta cambierebbe senza alcun aggiornamento di REDCap: le scritture si degraderebbero a simulazione e si andrebbe a cercare un cambio di versione che non c’è stato. È un rischio di diagnosi, non di sicurezza, ed è scritto qui perché costa mezza giornata a chi non lo sa.

Il confronto fra desiderato e reale: spento nell’osservatore, acceso nel giro

provisioning_diff() esiste, è collaudato, ed è la parte del canale che decide che cosa succede. L’osservatore non lo chiama, e non deve.

La ragione è di ordine, non di qualità. Il registro dichiara il desiderato, e l’osservatore legge lo stato di un’istanza per intero — nessuno gli chiede coppie specifiche, è la sola passata che esiste finché non c’è un registro da cui ricavarne una stretta (dev/runner-osservatore.R lo dice con queste parole). Un confronto fatto lì, fra il desiderato dichiarato dal registro e quel reale letto per intero, classificherebbe ogni coppia esistente come non più voluta, ogni volta che il registro non nomina ancora quella coppia — e all’inizio della migrazione del parco non ne nomina quasi nessuna. Non è la chiamata che scrive a essere pericolosa: è il confronto che la alimenterebbe. Per questo l’osservatore non lo calcola affatto, invece di calcolarlo e non usarlo — e un controllo in tests/testthat/test-runner-guardie.R sorveglia che quel job resti così.

Il giro del canale lo chiama, e può fin da subito. La differenza non è che il registro sia più popolato oggi di quanto lo sarebbe stato ieri: è che il giro non legge mai lo stato di un’istanza per intero. Chiede al modulo le sole coppie che il registro nomina — quelle desiderate e quelle da revocare, più quelle su cui il cancello sull’ambito deve interrogare — e confronta il reale con il desiderato solo su quelle. Un registro vuoto o appena iniziato non produce righe in più da confrontare: produce meno domande da fare, e zero righe è un risultato quieto, non un falso «tutto revocato». Non c’è quindi un interruttore da aspettare: la sicurezza non viene dal registro essere pieno, viene dal confronto non guardare mai più di quanto qualcuno ha chiesto — una proprietà del codice, non una soglia di avanzamento della migrazione.

Questo non riguarda la scrittura, che resta un interruttore separato e distinto dal confronto: il giro simula di default, e scrive solo quando la scrittura è richiesta esplicitamente (§1). Il confronto sempre acceso dice solo che cosa cambierebbe; se scriverlo per davvero è una decisione ulteriore, presa altrove.

3. Il protocollo di riqualificazione

Un aggiornamento di REDCap che cambia la superficie è procedura ordinaria, non caso eccezionale. Da qui in avanti il parco si aggiorna, e ogni aggiornamento che tocca la superficie chiede questi quattro passi, in quest’ordine.

  1. Leggere l’impronta corrente dall’istanza, con una lettura di stato.
  2. Aggiungere la riga al registro delle superfici, con la data di oggi e la conformità vuota. Le righe vecchie non si toccano: certificano le superfici che le istanze non ancora aggiornate espongono ancora.
  3. Eseguire la conformità, che può scrivere perché dichiara le impronte misurate e non quelle certificate.
  4. La run scrive lei la data. Non si digita a mano: un innesco che dipende da qualcuno che si ricorda di scrivere una data è un promemoria, ed è proprio la cosa che il meccanismo esiste per sostituire.

Fra il passo 1 e il passo 4 il chiamante ordinario continua a leggere, e le sue scritture sono degradate a simulazione. È il comportamento voluto.

dev/riqualifica-superficie.R esegue i quattro passi nell’ordine, e ha un modo --diagnosi che dice solo dove siamo senza scrivere niente. Esiste come script invece che come procedura da rifare a mano perché il momento in cui serve è subito dopo una finestra di manutenzione, che è il momento peggiore per improvvisare quattro passi in ordine.

Il vincolo d’ordine fra i due cancelli. Se l’istanza è sopra il soffitto di major, la scrittura è forzata a simulazione e i cinque casi della conformità escono rossi per una ragione che non è una difformità. Va alzato prima il soffitto nel modulo e ridistribuito il modulo, e solo dopo rieseguita la conformità. Mai il contrario. Per un aggiornamento dentro lo stesso major questo ramo non scatta, ed è il caso ordinario.

Due precondizioni che lo script non può verificare da solo, e che vanno controllate prima:

  • l’account di prova non deve avere diritti nel progetto di conformità. Il passo di revoca non ripristina niente: toglie i diritti, e questo coincide con lo stato iniziale solo se quello stato era vuoto;
  • il progetto di conformità deve già definire i ruoli e il gruppo di accesso che i casi nominano. Il canale asserisce le appartenenze e non le crea, e un ruolo mancante torna come errore di dato su un caso — che somiglia a una difformità e non lo è.

Il progetto di conformità dev’essere un progetto destinato a durare. Il primo non lo era: nato da uno spike, marcato come cancellato quando lo spike è stato smontato, avrebbe continuato a funzionare per tutto il conto alla rovescia di REDCap e poi sarebbe stato ripulito. Un progetto di conformità marcato per la cancellazione fallisce nel modo peggiore: non adesso, ma alla prima run dopo un cambio di versione — cioè la run il cui fallimento verrebbe letto come «la versione nuova rompe il canale».

4. Il registro delle richieste

Lo schema è un file, non del codice

Il dizionario del registro vive come CSV impacchettato, e REDCap lo importa com’è: un artefatto, due consumatori. I nomi dei campi del registro sono anche i nomi dei campi della richiesta, quindi fra i due non c’è nessuna mappa e nessuna chiave può cadere in un valore di riserva senza che qualcuno se ne accorga. È l’intera ragione per cui quei nomi sono stati scelti così.

Chi scrive che cosa

Il referente compila la richiesta. I campi d’esito li scrive la lavorazione, e sul form portano @READONLY: la marca governa il form, non l’API, quindi la lavorazione continua a scriverli mentre chi compila non può. Senza quella marca un referente potrebbe digitare «applicata» nel campo d’esito, e il registro porterebbe un successo che nessuno ha prodotto.

Il giro scrive da tre porte, e restano separate

La prima porta deposita identità e username appena la risoluzione è fatta, e non prende altro. La seconda deposita l’esito, e avviene dopo l’invio della notifica. In mezzo c’è la posta. La terza deposita il sigillo, per ultima.

Il sigillo ha una porta sua e non è una colonna dell’esito, e la ragione è che il corpo dell’esito si scrive a ogni esito: una colonna del sigillo lì dentro porterebbe un valore ogni volta, e il valore onesto quando non si è applicato niente è vuoto. Un data_error su una riga già applicata azzererebbe allora il sigillo, togliendo la protezione proprio alle righe che sono le sole ad averla.

La conseguenza da conoscere quando si tocca il testo dei messaggi: il frame da cui il giro compone la notifica è il registro come è stato letto, e resta volutamente immobile — è il metro contro cui si misura che cosa è cambiato. Quindi ciò che il messaggio deve dire e che quel frame ancora non porta va passato esplicitamente a chi compone. Allargare il frame dell’esito sarebbe la scorciatoia sbagliata: quel frame è anche il corpo dell’import, e allargarlo farebbe passare l’identità per la porta dell’esito.

Una richiesta è una persona su un progetto. Una coppia che compare due volte è un errore su entrambe le righe, non su una: una delle due è l’errore e non c’è modo di sapere quale. Indovinare quale vince è ciò che fa un libro mastro; un registro si rifiuta.

Una riga cambiata dopo essere stata applicata

REDCap non ha una proprietà per riga: i diritti sono per strumento, e la sola cosa che partiziona le righe sono i gruppi di accesso, che qui non ci sono per scelta — nasconderebbero le righe altrui e renderebbero i doppioni incomprensibili. Chiunque abbia il registro può quindi modificare la riga di un altro.

Il giro scrive accanto a ogni riga applicata il sigillo di ciò che ha applicato: dodici caratteri sui campi di intento, e su nessun altro. Un sigillo che al giro dopo non corrisponde significa «modificata dopo l’applicazione»: la riga prende l’esito held, si ferma dove si ferma una revocata — prima che il piano si formi, quindi nessuna istanza viene interrogata — e la notizia va a requested_by.

Tre cose da sapere quando si tocca questo meccanismo:

  • il sigillo copre ciò che è stato chiesto, non ciò che è successo. Se si muovesse quando il giro scrive l’esito, ogni riga applicata sembrerebbe modificata un giro dopo: il meccanismo accuserebbe se stesso, su ogni riga, per sempre;
  • il sigillo si calcola sulla riga che il giro sta per lasciare, non su quella che ha letto. Il giro risolve username e lo riscrive nella stessa passata, e username sta dentro al sigillo perché cambiarlo cambia a chi si dà l’accesso. Sigillare la riga come letta sarebbe lo stesso difetto di sopra, per mano del giro invece che dell’esito;
  • il campo di approvazione sta fuori dal sigillo pur non essendo @READONLY. Porta il sigillo che approva, quindi scriverlo non può spostare ciò che confronta. È il primo campo per cui «scritto dal canale» e «fuori dal sigillo» smettono di essere lo stesso insieme, ed è per questo che sono due liste.

L’approvazione vale a due condizioni: porta il sigillo di quella versione della riga, e viene da qualcun altro. La seconda si verifica leggendo il registro degli eventi di REDCap, che dice chi era autenticato e non che cosa qualcuno ha scritto in un campo — la stessa ragione per cui requested_by non basta da solo il giorno in cui si concede il Data Import Tool. Un registro degli eventi illeggibile lascia la riga ferma: «non ho potuto chiedere chi è stato» non è «l’ha approvata un altro».

La finestra del log si chiede nell’ora dell’istanza, non in UTC: REDCap timbra col proprio orologio, e il giro delle 21:03 UTC compare alle 23:03. Lo scarto non è una costante, perché cambia con l’ora legale — per questo la finestra è larga un giorno e la conversione è una comodità, non un cardine.

Verificare che il progetto sia ancora quello che il canale si aspetta

compare_dictionary() rilegge il dizionario dal progetto vivo e lo confronta con quello impacchettato. I due nascono identici; la domanda è se lo siano ancora dopo un import che ha perso un campo, una modifica a mano sul form, o un tipo allentato per far passare una validazione.

Sette codici, e nessuno è cosmetico:

Codice Che cosa è successo Perché conta
campo assente un campo del dizionario non c’è più la richiesta perde quel campo senza dirlo
campo in più c’è un campo che il dizionario non prevede non rompe niente, ed è la prova diretta che qualcuno ha modificato il form a mano
tipo diverso il tipo del campo è cambiato uno stato di richiesta diventato testo libero trasforma un refuso in una richiesta di concedere ciò che qualcuno ha chiesto di revocare
scelte diverse stesso tipo, valori ammessi diversi il valore che revoca può non esserci più
readonly caduto la marca di sola lettura non c’è più chi compila può scrivere un esito che nessuno ha prodotto
colonna assente manca una colonna del dizionario stesso il dizionario è illeggibile, e senza questo codice risulterebbe conforme invece che illeggibile
scelte non confrontate l’elenco delle istanze non è stato fornito le scelte di quel campo sono la flotta, non lo schema: senza l’elenco se ne può giudicare solo la forma, e un confronto che non ha potuto guardare non deve restituire un verde

Un import riuscito non basta, ed è il punto della sezione. Nessuno di quei difetti solleva da nessun’altra parte: la lettura di una riga tratta una colonna assente come «non impostato» invece che come errore, e il canale ignora i campi che non conosce. Sono entrambi comportamenti corretti, e sono anche la ragione per cui la deriva qui è silenziosa. Il confronto è l’unica cosa che guarda.

Il codice sulla colonna assente merita una riga in più: senza di lui un dizionario a cui manca una colonna verrebbe letto come conforme, perché non ci sarebbe niente da confrontare e «nessuna differenza» è la risposta che si dà quando non si è guardato niente.

Il settimo dice una cosa diversa dagli altri sei: non riporta una deriva, riporta un limite del giudizio. Le scelte di quel campo sono l’elenco delle istanze, cioè un dato che cambia quando cambia la flotta e non quando cambia il contratto, e che per questo non vive nel pacchetto — il pacchetto ne porta un modello con quella cella vuota, e chi confronta fornisce l’elenco. Chi lo fornisce riceve il confronto per intero; chi non lo fornisce riceve un giudizio parziale che si dichiara tale, invece di un verde che non ha guardato.

Il modello, di suo, non è importabile: REDCap rifiuta un campo a scelta singola che non ha scelte. È voluto, ed è ciò che impedisce di importarlo per sbaglio al posto del dizionario vero.

5. Gli allarmi

Undici regole attive, tutte con risoluzione automatica, divise in due famiglie che non si parlano: sei dell’osservatore e cinque del canale. La divisione non è organizzativa ma tecnica — le due lavorazioni scrivono in due tabelle diverse, e una regola non può leggere la tabella dell’altra. Chi cerca il perché di una regola mancante deve guardare prima di tutto quale tabella potrebbe leggerla.

La gravità dice la famiglia del problema, non chi lo ha trovato:

  • gravità 1 — «non possiamo più vedere» o «non sta lavorando»: la lavorazione non ha lasciato nessun record; il record c’è ma non porta le colonne che deve; l’ultimo giro si è fermato prima di agire; un’utenza è nata e la sua credenziale non è arrivata a nessuno;
  • gravità 2 — «abbiamo trovato un problema»: una o più istanze non si sono lasciate leggere; un’istanza non è in stato collaudato; la superficie di un’istanza è cambiata; le liste di indirizzi sono cambiate o divergono; gli esiti sono stati calcolati e non scritti; una notifica non è stata recapitata.

I nomi esatti, le gravità e le finestre stanno nel foglio dei parametri.

Le finestre sono diverse, e devono esserlo

L’osservatore gira una volta al giorno e le sue regole guardano indietro due giorni. Il canale gira ogni quattro ore, e le sue guardano indietro cinque, dentro una finestra di sei: la finestra deve contenere il periodo che la query interroga, o la regola scatta sempre.

Una regola vede solo ciò che la sua lavorazione interroga

Il canale interroga solo le istanze che hanno righe nel registro, quindi il suo contatore delle istanze irraggiungibili conta quelle, non la flotta servita. Con un registro che nomina una sola istanza, la regola tace sulle altre anche se sono spente — ed è corretto così: un’istanza che nessuno ha chiesto non è un guasto. Chi vuole sapere se la flotta è viva guarda l’osservatore, che le legge tutte.

Questa asimmetria è costata un guasto vero, ed è la ragione per cui la quinta regola del canale esiste: cinque giri di fila hanno perso l’unica istanza con righe nel registro senza che nessuna delle quattro regole di allora si accorgesse di niente. Non ne avevano motivo — «non ho raggiunto l’istanza» non è un errore del giro, è il suo esito, e finiva in un campo che nessuna regola guardava.

I banchi spenti non fanno scattare la regola sulle irraggiungibili

I banchi sono istanze di prova, spente di norma e accese al bisogno. Stanno fra le istanze col modulo dell’inventario, perché quando sono accese vanno lette, ma la loro voce porta "banco": true. Contati con le altre, terrebbero rossa ogni giorno la regola dell’osservatore sulle istanze irraggiungibili, e una regola sempre rossa non la guarda più nessuno.

Per questo il record dell’osservatore porta due campi accanto a irraggiungibili, che resta com’era: irraggiungibili_produzione, che non conta i banchi, e irraggiungibili_nomi, che elenca tutte le irraggiungibili, banchi compresi. La regola guarda irraggiungibili_produzione, quindi un banco marcato non la fa scattare. Un campo banco assente, o con un valore che non sia esattamente true, vale produzione: un’istanza esce dall’allarme solo se qualcuno l’ha marcata. Un banco spento resta invece un buco di copertura, e flotta_a_una_major resta falso: potrebbe stare su un’altra major.

La garanzia vale per quella regola sola, e solo finché almeno un’istanza col modulo risponde. Se le istanze col modulo sono tutte banchi e sono tutte spente, l’osservatore non legge niente: letture_riuscite è zero e tutti_collaudati è falso, perché un giro che non ha visto nessun cancello non ne ha visto uno buono. Suonano allora la regola sull’assenza, a gravità 1, e quella sul cancello, a gravità 2, e restano rosse finché i banchi restano spenti. Marcare i banchi non le spegne, e non deve: in quel caso l’osservatore non vede nessuna istanza, che è ciò che la gravità 1 dice. Misurato il 2026-08-20, con i soli banchi agganciati e tutti spenti: il cancello è scattato mezz’ora dopo il giro, l’assenza due ore dopo. Il rosso fisso sparisce quando fra le istanze col modulo c’è almeno un’istanza di produzione, e risponde.

Il rilascio segue l’ordine della sezione sulla regola di raccolta, più sotto: prima le due colonne nella tabella dell’osservatore e nella sua regola di raccolta, perché le colonne che la regola non conosce vengono scartate in silenzio; poi il pacchetto e la copia della lavorazione in /opt, in quest’ordine, perché la lavorazione nuova passa un argomento che il pacchetto vecchio non conosce; per ultime, nella stessa finestra e dopo aver visto arrivare un record che porta irraggiungibili_produzione con un valore, la regola d’allarme e quella sul record incompleto. Aggiornata prima, la regola d’allarme leggerebbe una colonna vuota, e in KQL un confronto su un valore nullo è falso e non un errore: tacerebbe anche con una produzione spenta.

La regola sul record incompleto elenca per nome le colonne che pretende, e irraggiungibili_produzione va aggiunta all’elenco. Finché la regola d’allarme guardava irraggiungibili, un campo che la lavorazione scrive da sempre, non serviva; spostata sulla colonna nuova, guarda un campo che nessuna regola sorveglia. Se la colonna tornasse vuota, per un ritorno indietro di pacchetto e lavorazione o per una regola di raccolta riapplicata dalla definizione di prima, il record resterebbe accettato, irraggiungibili continuerebbe a essere scritto, e un’istanza di produzione spenta non farebbe scattare niente. È il passo che la regola sul record incompleto del canale ha fatto con le colonne della posta, col rilascio della 0.12.0.

La forma della query, e perché è quella

Ogni regola prende l’ultima osservazione e la giudica: top 1 by TimeGenerated desc prima dei filtri, non un filtro su una finestra temporale.

La differenza non è di stile. Una query nella forma «è successo qualcosa di brutto nelle ultime ventiquattr’ore?» resta vera per ventiquattr’ore, perché i record sono immutabili: un guasto riparato in cinque minuti tiene la regola rossa per un giorno intero. La forma giusta è «l’ultima osservazione com’è andata?», che torna verde appena atterra una run buona.

Questa è anche la ragione per cui una regola sola con quattro rami in or è stata ritirata: spezzarla in quattro senza rimediare prima la finestra avrebbe fatto ereditare a tutte e quattro il rosso appiccicoso, moltiplicando le notifiche invece di dividerle.

La regola di raccolta impara le colonne prima che il pacchetto le emetta

Vale ogni volta che il record del giro guadagna un campo, e non dà errore se lo si sbaglia — dà silenzio. L’API d’ingestione scarta senza dirlo ogni colonna che la regola di raccolta non conosce. Rilasciare il pacchetto prima e aggiornare la regola dopo non produce un errore: produce colonne vuote, e quindi allarmi che sorvegliano un campo mai scritto — regole che non possono scattare, installate nella convinzione che stiano guardando qualcosa.

Quindi l’ordine è: prima la regola di raccolta, poi il pacchetto, poi gli allarmi nuovi. E una regola d’allarme che verifica quali colonne il record porta va aggiornata nella stessa finestra del rilascio, o scatterebbe su un record perfettamente sano.

Per il dizionario del progetto il vincolo d’ordine è lo stesso, ma guardare i soli campi non basta a decidere se serva l’atto unico, e la distinzione è costata una finestra di canale fermo il 2026-09-01.

Sui campi l’asimmetria è reale e mite: un campo che il pacchetto si aspetta e il progetto non ha ferma il giro (DIZIONARIO_CAMPO_ASSENTE), mentre uno che il progetto ha e il pacchetto non conosce è tollerato (DIZIONARIO_CAMPO_IN_PIU). Da soli, quei due direbbero che basta importare prima e installare dopo.

Sulle scelte di un campo codificato non c’è nessuna asimmetria: DIZIONARIO_SCELTE_DIVERSE ferma il giro in tutti e due i versi, perché una scelta in più da un lato e una in meno dall’altro sono la stessa differenza vista da due parti. Quindi:

  • una modifica che aggiunge solo campi si può fare in due tempi;
  • una che tocca una parola di un vocabolario — cioè le scelte di outcome, identity, request_status, seal_state — vuole l’atto unico: import e installazione nella stessa finestra, con il giro fermo in mezzo.

La 0.14.0 sembrava del primo tipo perché aggiunge tre campi, ed era del secondo: aggiungeva anche held alle scelte di outcome. Il giro si è fermato su DIZIONARIO_DERIVATO fra i due gesti, come doveva.

E server non è un vocabolario del pacchetto, è la flotta. Le sue scelte si generano da instances, e la lavorazione passa lì l’intera flotta dell’inventario, non le sole istanze col modulo. Un dizionario generato con le tre servite supera il controllo dei campi e ferma il giro su DIZIONARIO_SCELTE_DIVERSE:server — misurato lo stesso giorno, generandolo sbagliato.

Due domande diverse decidono la forma di un rilascio

Vanno fatte separatamente, perché fino al 2026-09-01 avevano sempre risposto insieme:

  • tocca lo schema? Allora serve l’atto unico: import del dizionario e installazione nella stessa finestra, con il giro fermo in mezzo.
  • cambia la firma di ciò che la lavorazione chiama? Allora i gesti sono due, e la lavorazione in /opt va ricopiata insieme al pacchetto.

La 0.13.0 ha risposto no alla prima e sì alla seconda: nessun dizionario da importare, ma round_record() aveva preso un terzo argomento obbligatorio, e un pacchetto nuovo con la lavorazione vecchia muore su argument "posta" is missing — cioè il giro non ritorna, non lascia record, e l’allarme sull’assenza suona a gravità 1.

La regola ritirata è disattivata, non cancellata

La vecchia regola a quattro rami è ancora nella sottoscrizione, disattivata, con la descrizione che spiega perché è stata sostituita. Va lasciata dov’è: cancellarla toglierebbe quella spiegazione. Il ritorno indietro è una modifica che la riabilita.

6. Che cosa fare quando suona

Una voce per regola, con la prima cosa da guardare. Le regole sono nominate per contenuto; i nomi stanno nel foglio dei parametri.

Nessun record (gravità 1). La lavorazione non è partita, o è morta prima di emettere. Guardare lo stato dell’unità di sistema e i suoi log sulla macchina del perimetro. Se l’unità è partita e ha fallito, l’errore è quasi sempre nel recupero del token o del segreto. La regola dell’osservatore però non chiede un record qualsiasi, ne chiede uno con almeno una lettura riuscita: se l’unità è sana e il record c’è con letture_riuscite a zero, nessuna istanza col modulo ha risposto, e quando sono tutte banchi spenti è il caso descritto nel §5.

Record incompleto (gravità 1). Il record c’è ma manca una colonna pretesa. Due cause, e vanno distinte prima di toccare qualcosa: o il modulo ha smesso di riportare quel campo su qualche istanza, oppure la regola di raccolta non conosce ancora una colonna che la lavorazione ha cominciato a emettere. La seconda si riconosce dal fatto che la colonna è nuova, e si ripara aggiornando la regola di raccolta.

Istanze irraggiungibili (gravità 2). Una o più istanze di produzione non hanno risposto: nell’osservatore la regola conta irraggiungibili_produzione, che lascia fuori i banchi (§5), e non il campo storico irraggiungibili. Quali siano lo dice irraggiungibili_nomi, che elenca anche i banchi spenti; quali di quei nomi siano banchi lo dice l’inventario. Tolti i banchi, guardare quante: se sono tutte le istanze di produzione, il sospetto è sul perimetro, tipico l’indirizzo della macchina cambiato, che chiude tutte le liste insieme. Se è una sola, il sospetto è su quell’istanza.

Cancello non collaudato (gravità 2). Un’istanza non è in stato collaudato. Leggere quale dei due cancelli l’ha declassata: se è il soffitto di major, l’istanza è stata aggiornata oltre la finestra del modulo; se è l’impronta, la superficie è cambiata dentro lo stesso major. Nel primo caso si alza il soffitto prima di ricollaudare, nel secondo si va direttamente al protocollo di §3. Con cancelli vuoto non c’è nessun declassamento da leggere: il giro non ha letto nessuna istanza, e suona anche la regola sull’assenza.

Superficie disomogenea (gravità 2). Le istanze osservate non espongono la stessa impronta di superficie. Normale durante un’ondata di aggiornamenti, in cui le istanze passano alla versione nuova una per volta; da spiegare se l’ondata è finita.

Liste di indirizzi divergenti (gravità 2). Le istanze non ammettono lo stesso insieme di indirizzi. Un’impronta unica su tutte significa che le liste coincidono; se ne compaiono due, due istanze ammettono indirizzi diversi, e la differenza va spiegata prima di essere accettata. L’impronta è un digest di una manciata di indirizzi: lo spazio dei candidati è minuscolo, e ricostruire il contenuto per enumerazione costa meno che andare a leggere la configurazione. È un rilevatore di cambiamento, non un segreto.

Prima di inseguire un allarme isolato. L’ingestione ha consistenza eventuale: per qualche decina di secondi due query identiche possono vedere «ultimo record» diversi. Una regola che valuta dentro quella finestra può scegliere un record vecchio e produrre uno scatto che si richiude alla valutazione dopo. Con una valutazione all’ora e una run al giorno è raro e costa poco — ma se non lo si sa, è il classico allarme fantasma che si insegue per settimane.

7. Le trappole misurate

Ogni voce è un fatto già pagato una volta. Sono scritte qui perché ricomprarle costa quanto la prima volta.

L’ingestione scarta in silenzio le colonne che la regola di raccolta non conosce. Chi aggiunge un campo al record e non aggiorna prima la regola non ottiene un errore: ottiene un record accettato con quel campo vuoto. Da qui la regola d’ordine: si aggiorna la regola di raccolta prima di distribuire la lavorazione che emette la colonna nuova, mai dopo.

readr::read_csv() ha trim_ws = TRUE per default. Un confronto cella per cella fra due dizionari diceva «nessuna differenza», ed era lo strumento a dirlo: lo spazio iniziale che REDCap aggiunge alle celle Field Annotation veniva tagliato in lettura. Ricompare rileggendo con trim_ws = FALSE.

Su Windows az è az.cmd. Un URL con una & dentro viene rianalizzato da cmd.exe, che spezza il comando. Si passano i parametri con l’opzione dedicata, senza mettere la & nell’URL. E l’uscita in formato tabulato emette terminatori di riga CRLF: il ritorno a capo va tolto prima di confrontare la stringa con qualcosa.

E non è solo la &: anche la barra verticale. Una query di telemetria è fatta di barre verticali, e passata come parametro a az su Windows viene rianalizzata dalla stessa shell. Il modo in cui si scopre è il peggiore possibile: il comando esce zero con una risposta vuota, quindi non somiglia a un errore ma a un workspace senza dati — che è la conclusione sbagliata più costosa che si possa trarre guardando la telemetria.

L’aggiramento che vale per entrambe è lo stesso, e toglie la causa invece di schivarla: si chiede il token con az e si esegue la chiamata come richiesta REST, con la query nel corpo. Un corpo JSON non passa da nessuna shell, quindi nessun carattere ha bisogno di essere protetto.

Alcune sottoutilità di az chiedono conferma interattiva per installare un’estensione. In una shell non interattiva muoiono con un errore di lettura da terminale, che non ha niente a che vedere con il comando. Si aggira chiamando l’API di gestione direttamente, che non installa niente.

accountEnabled non torna se non lo si seleziona. La lettura di un’utenza dalla riga di comando omette la proprietà se non la si chiede esplicitamente, e in formato tabulato esce come «nessun valore» — che si legge come «disattivato» o come «non so», a seconda di come si è disposti. Va chiesta con la selezione esplicita dei campi.

Bloccare l’utenza nel sistema di identità non ferma il canale, e va tenuta bloccata: il token dell’API non passa dal sistema di identità, quindi l’unica credenziale che apre il registro resta quella in Key Vault. Non sospendere invece l’utente dentro REDCap: è una leva dal nome simile e blocca anche l’API.

Tre servizi Azure con consistenza eventuale. Una rilettura immediata non è una verifica. Vale per l’ingestione della telemetria, per lo stato delle regole d’allarme e per le proprietà di un’utenza appena modificata. Quando la domanda è «è cambiato davvero?», la risposta si prende due volte a distanza.

paste0("X:", character(0)) torna "X:", non character(0). Un vettore vuoto in paste0() non propaga il vuoto: lo riempie. È il modo in cui un elenco di errori vuoto diventa un elenco di un errore senza contenuto, che poi viene contato come «c’è un problema».

Il modulo si distribuisce con git archive, mai copiando dal working tree. Su Windows la copia dal working tree porta con sé i terminatori di riga CRLF, e il PHP arriva sull’istanza diverso da quello in git. E in PowerShell la pipe fra git archive e ssh corrompe l’archivio: la pipe non è binaria. Si scrive il tar su file e lo si trasferisce, in due passi.

Il verde di un controllo mai stato rosso vale poco. Ogni guardia di questo canale è stata provata in entrambe le direzioni, e in un caso pinnando la finestra su record storici di stato noto invece di iniettare record falsi nella tabella — che poi resterebbero a inquinare ogni lettura futura.

8. Dove stanno i valori

Nel foglio dei parametri di esercizio, nella cartella condivisa. Il rimando è sempre per nome del parametro, mai per posizione nel foglio, così che riordinare il foglio non invalidi questa guida.

Il foglio raccoglie otto famiglie:

  • il registro delle richieste: URL, istanza, identificativo del progetto, nome del modulo e numero dei campi;
  • l’utenza di servizio: nome utente, gruppo di appartenenza, stato dell’account, ruolo sul registro;
  • i segreti per nome: il Key Vault, il token del registro, il segreto del modulo per ciascuna istanza;
  • la macchina del perimetro: nome, dimensione, indirizzo pubblico, alias di connessione, unità di sistema e orario;
  • la telemetria: workspace, tabella, punto di raccolta, regola di raccolta, gruppo di azione;
  • le variabili dell’unità di sistema, che è dove entrambe le lavorazioni prendono i nomi delle risorse e gli interruttori — la scrittura, la posta, il dirottamento delle notifiche;
  • gli allarmi: le regole attive per ciascuna delle due lavorazioni, la loro gravità, la finestra e lo stato di collaudo;
  • la flotta osservata: quali istanze hanno il modulo, le due impronte correnti, il numero di coppie e quante hanno una scadenza.

Ogni riga del foglio porta la data in cui il valore è stato verificato, e non quella in cui è stato scritto. Su servizi con consistenza eventuale «quando l’hai guardato» è parte del valore: una riga senza data è una riga di cui nessuno sa se è ancora vera.

Le righe della flotta osservata non si modificano a mano: sono misure, e il loro valore corretto è quello che la lavorazione riporta. Stanno nel foglio perché chi legge un allarme deve poter confrontare ciò che vede con ciò che era normale l’ultima volta che qualcuno ha guardato.

Che cosa questa guida non copre

  • Il provisioning delle identità. Non è ancora automatizzato: la creazione dell’account si fa a mano, con la procedura storica. Il confine è nominato invece di taciuto, perché chi cerca «come nasce l’account» deve trovare una risposta, e «non ancora automatizzato» è una risposta.

  • Il caricamento in batch delle richieste. Passa da IT. Un import per identificatore di record permetterebbe a un referente di sovrascrivere le righe di un altro, e la difesa nativa non è ancora in piedi.

  • Quando passare dalla simulazione alla scrittura vera. Vedi §1: il giro del canale simula finché nessuno gli chiede di scrivere per davvero, e il confronto fra desiderato e reale è acceso da subito — non è più lui l’interruttore, vedi §2. Questa guida non descrive quando si decide di girare quell’interruttore, ed è deliberato: una procedura scritta è una procedura che qualcuno esegue prima che sia il momento.

  • Il cancello sull’ambito delle richieste. Il pacchetto porta scope_errors(), che rifiuta una richiesta il cui autore non detiene il permesso user_rights sul progetto che la richiesta nomina — la regola è che il canale non deve permettere a nessuno ciò che non potrebbe già fare a mano. È in catena: il giro lo interroga per ogni istanza che ha risposto, e una guardia nega qualunque funzione del pacchetto che sappia scrivere su un’istanza senza nominarlo. Il cancello si interroga sulle risposte e mai sulle assenze, e la distinzione ha una grana precisa. Un’istanza irraggiungibile torna con il proprio codice di trasporto — quello che riporta module_state(), oppure TRASPORTO_ISTANZA_SENZA_MODULO o TRASPORTO_SEGRETO_NON_LEGGIBILE se non è nemmeno stata interrogata; una che risponde ma il cui modulo è troppo vecchio per riportare il permesso su qualunque riga torna invece TRASPORTO_AMBITO_NON_LEGGIBILE. In entrambi i casi rimettono in coda le sole righe già risolte in una coppia, mai una dichiarazione di fuori ambito — dire «non sei autorizzato» quando la verità è «non ho potuto chiedere» manda a correggere la persona sbagliata. La stessa regola vale una grana più sotto: una singola riga la cui autorizzazione non si lascia risolvere — un ruolo cancellato sotto di lei, su un’istanza che altrove risponde — torna TRASPORTO_PERMESSO_NON_LEGGIBILE e rientra in coda. È rifiutata comunque, perché l’alternativa sarebbe allargare l’accesso proprio nel momento in cui il canale ha smesso di poterlo vedere; ma il rifiuto è indirizzato a chi può ripararlo. Chi ha compilato quella richiesta l’ha compilata bene e potrebbe benissimo detenere il permesso: ciò che è rotto sta nella tabella dei diritti di REDCap, dove arriva IT e non lui.

    La guardia ha un’eccezione dichiarata: lo strumento di conformità (run_conformance_check()), che non legge il registro e non ha un richiedente su cui interrogare il cancello — la regola che il cancello applica («il richiedente avrebbe potuto concederlo a mano») non si pone per uno strumento che un operatore lancia a mano contro un progetto di conformità dedicato, mai su dati derivati dal registro. Dirlo qui evita di promettere una garanzia più larga di quella che il codice mantiene: una guardia descritta come più ampia di quanto sia viene creduta più di quanto meriti.