MinDatabase

MinDatabase - AI Agent Instructions

Version: 4.13.0 Last Updated: 7 October 2026

Project Scope

MinDatabase is a scholarly, content-only repository of long-form art historical essays.

Canonical Repository Structure

Use this structure as authoritative:

MinDatabase/
├── Content/
│   ├── Artists/
│   │   ├── VII-c/
│   │   ├── VIII-c/
│   │   ├── IX-c/
│   │   ├── X-c/
│   │   ├── XI-c/
│   │   ├── XII-c/
│   │   ├── XIII-c/
│   │   │   └── [Artist-Name].md
│   │   └── XIV-c/
│   ├── Churches/
│   ├── Codex/
│   ├── Papers/
│   ├── Saints/
│   └── prompts/
├── Images/
└── MinDatabase - AI Agent Instructions.md

Naming and Placement Rules

File Naming

Writing Model

Standard Artist Section Flow

Use and adapt as needed by discipline:

  1. Early life and family
  2. Patronage and commissions
  3. Artistic style / technical characteristics
  4. Artistic influences
  5. Travels and career geography
  6. Death and legacy
  7. Works / major works

Front Matter Policy

The repository currently includes YAML front matter in many content types, including Artist files.

Canonical Front Matter

The form followed by the schede that frontmatter_census.py classes as new:

Three content rules apply with the form:

Strategy: schede not yet aligned are converted the first time they are touched for other reasons, in the same commit; scripts/frontmatter_census.py measures the distance.

Footnote Conventions (Mandatory Site-Wide)

All footnotes must use HTML anchor syntax and one ordered list block:

Required Pattern

Inline note<a id="fnref:1" href="#fn:1" class="footnote"><sup>1</sup></a>

<ol class="footnotes">
  <li id="fn:1">
    <p>Footnote text. <a href="#fnref:1" class="footnote__back" aria-label="Back to reference">↩</a></p>
  </li>
</ol>

References to endnotes.html

Endnote Conventions (Page-End Notes)

Use endnotes for supplemental notes that should appear after the main text and after any footnotes, but remain visually distinct from the footnote list.

Required Pattern

Main text with a note<a id="enref:1" href="#en:1" class="endnote"><sup>1</sup></a>

<ol class="footnotes">
  <li id="fn:1">
    <p>Footnote text. <a href="#fnref:1" class="footnote__back" aria-label="Back to reference">↩</a></p>
  </li>
</ol>

<ol class="endnotes">
  <li id="en:1">
    <p>Endnote text. <a href="#enref:1" class="endnote__back" aria-label="Back to endnote reference">↩</a></p>
  </li>
</ol>

Images and Figures

This repository already contains embedded images in content files.

Codex Image Names

Images of the schede in Content/Codex/ are named after what they show, not by a running number. The name is derived from the figcaption, which stays the source of truth: if the caption changes, the file is renamed.

Form: Images/<Scheda-Name>/<slug>-<locator>[-d01|-alt].<ext>, all lowercase, no spaces.

The rule applies to Content/Codex/ only; the other sections keep <subject>-NN.jpg (see Convenzioni). When a scheda is touched for any reason, its images are brought into the form in the same commit; a whole-section round is run with a script in dry-run first.

Codex Zero width
Codex Amiatinus 4
every other codex 3

Cross-Reference Consistency

Editing and Commit Workflow

Tooling Note: add_sections.py

Removed — this tool is no longer available in the repository. It was previously documented at Content/Artists/XIII-c/add_sections.py (purpose: insert ## headings into prose lacking explicit section markers via exact string replacements), but the file is no longer present. Do not reference or attempt to run it. Insert missing section headings via direct manual edits instead.

Tooling Note: validate_content_indexes.py

scripts/validate_content_indexes.py runs the twelve content checks listed in its own docstring: matching entries, resolvable hrefs, valid front matter, alphabetical ordering, basename rules, Saints reachability, inter-page links, anchors into the two containers, case-exact image references, slug ordering inside the sections of endnotes.html, display-name ordering inside the sections of scholars.html, and the leaf-based names of the images in Content/Codex/. It runs read-only and is executed both by the validate-content-indexes.yml workflow on every push touching Content/**, assets/data/*.json, or the script itself, and by validate.yml alongside the other three validators.

Content/Saints/ is, by editorial choice, an unindexed section: saint entries are reachable only via links from other entries, not from a listing page. The script excludes Saints/ from the “has a JSON entry” check, but flags any Saints entry that isn’t linked from any other entry, since such an entry would otherwise be unreachable.

Quality Checklist Before Finalizing

  1. Folder and filename match canonical structure (Papers, current century folders).
  2. Dates, names, relationships, and attributions are internally consistent.
  3. Prose remains scholarly and readable, not flattened into outline form.
  4. Footnotes use the required <ol class="footnotes"> format.
  5. Images/figures (if present) are formatted consistently and captioned clearly.
  6. Related entries affected by major factual edits are updated or reviewed.
  7. References to endnotes.html use the numeric form, numbered in document order; if the scheda is listed by scripts/endnote_forms.py, convert it in the same commit.

Automazioni

Il repository si mantiene da solo attraverso sei workflow GitHub Actions. Conoscerne l’ordine evita di inseguire problemi che hanno una causa nota.

La catena di pubblicazione

Un push che tocca Content/**/*.md avvia, in parallelo:

  1. Update Push Notices — genera assets/data/push_notices.json con le notizie delle schede pubblicate, poi invia la newsletter agli iscritti e registra l’invio in newsletter_last_notified.json.
  2. Update Gallery Index — rigenera assets/data/gallery-index.json estraendo le <figure> da tutte le schede.

Entrambi committano e, in coda, invocano esplicitamente Deploy Site.

Che cosa produce notizia

Solo le schede: i markdown sotto Content/Artists/, Content/Churches/, Content/Codex/, Content/Papers/ e Content/Saints/. La sezione compare nel badge in homepage e nell’oggetto della newsletter.

Content/Saints/ e’ entrata in quell’elenco tardi. Il classificatore ne ignorava l’esistenza e le otto schede dei santi cadevano fra i non-contenuti: il difetto non si era mai visto perche’ nessun santo era stato pubblicato da quando esistono le notizie, ma la prima pubblicazione sarebbe uscita etichettata «Other». Non indicizzata non vuol dire non contenuto.

Tutto il resto non produce notizia, e viene saltato con una riga di log skipping non-content file: <path> perche’ il salto resti osservabile: i .md di radice — glossario, sistemi di datazione, README e questo stesso file — e Content/prompts/. Senza il filtro finivano annunciati come schede; il file delle istruzioni e’ comparso davvero fra le notizie candidate durante una prova.

La regola e’ a tappeto e non una lista di eccezioni: una cartella nuova sotto Content/ non produce notizie finche’ non la si dichiara in CONTENT_SECTIONS. E’ il verso giusto in cui sbagliare — una scheda non annunciata si rimedia, un’email no — ma va ricordato quando si aggiunge una sezione.

Cosa va in homepage, cosa va per email

Sono due liste diverse, scritte nello stesso push_notices.json.

notices rotola: le notizie del push si fondono con quelle gia’ pubblicate, e le prime MAX_NOTICES riempiono la card «ultime novita’» in homepage. Deve restare piena anche dopo un push che porta una scheda sola, quindi il riporto e’ voluto.

latest_push porta le sole notizie di questo push, ed e’ quella che spedisce la newsletter. Un’email annuncia quel che e’ appena successo: se uscisse sulla lista fusa ripeterebbe fino a MAX_NOTICES - 1 voci gia’ spedite il giro prima, e l’oggetto di un push di una scheda sola diventerebbe ... and 2 more updates invece di [New] <titolo>. E’ successo: e’ la ragione per cui le liste sono due.

Stanno nello stesso file e non in due perche’ nascono dallo stesso input nello stesso run. Separarle in due artefatti significherebbe tenerli allineati, due controlli git diff --quiet nel workflow, e la possibilita’ che uno dei due commit riesca e l’altro no. La chiave e’ affiancata, cosi’ la homepage continua a leggere notices senza sapere che l’altra esiste.

La firma anti-reinvio si calcola su latest_push, non sulla lista fusa: deve rispondere a «c’e’ qualcosa di nuovo da mandare?», e a quella domanda risponde il push. Un latest_push vuoto — push senza notizie proprie — non spedisce nulla e lascia lo stato intatto; una chiave assente, cioe’ un file scritto da una versione precedente alla separazione, nemmeno.

Quante notizie, e in che ordine

MAX_NOTICES = 3 vale per entrambe le liste: un push che tocca piu’ schede le annuncia tutte e tre, in homepage e in newsletter. Prima ne usciva una sola, e in un push misto la scheda pubblicata poteva restare fuori a favore di un ritocco marginale.

L’ordine e’ a tre livelli: pushed_at decrescente, poi created prima di modified, poi il path in ordine alfabetico. Il secondo livello e’ quello che conta, perche’ i file di uno stesso push condividono il timestamp: una scheda nuova precede sempre una ritoccata. Il timestamp e’ uno solo per push — quello del commit piu’ recente, non l’ora corrente, perche’ la firma anti-reinvio lo include e deve poter essere ricalcolata identica — anche quando il push porta piu’ commit. Prima ogni notizia prendeva la data del proprio commit: in un push di tre commit, created nel piu’ vecchio e modified nel piu’ recente, la scheda nuova finiva dietro il ritocco e l’oggetto della newsletter la annunciava come un aggiornamento. Il terzo chiude l’ordine e lo rende totale, cosi’ i due rami da cui lo script ricava i file — il payload dell’evento e il ripiego su git log — producono lo stesso risultato. Non era vero prima: il payload elenca gli added prima dei modified, git log --name-status elenca in ordine di path, e con una sola notizia la differenza decideva da sola che cosa finiva in newsletter.

L’oggetto della newsletter si costruisce sulle notizie del push — quelle di latest_push — e non e’ piu’ fisso. Una notizia sola porta titolo ed etichetta — `Medieval Visions — [New]

` —; due o piu' portano la prima, che l'ordinamento garantisce essere la piu' rilevante, e il conteggio delle altre: `Medieval Visions — and 2 more updates`. I titoli oltre i 60 caratteri sono troncati con un'ellissi, perche' il troncamento del client di posta cadrebbe dove capita e mangerebbe il conteggio. `NEWSLETTER_SUBJECT`, se valorizzata, continua a forzare un oggetto fisso. La firma anti-reinvio in `newsletter_last_notified.json` guarda solo path, timestamp e tipo di modifica: l'oggetto non la tocca, quindi non puo' provocare un reinvio. `scripts/test_update_push_notices.py` fissa queste regole su tredici gruppi di casi presi dalla storia del repository — 58 asserzioni, comprese quelle sul range del ripiego, sulla firma dell'elenco intero e sulla separazione fra lista rotolante e lista solo-push. Si esegue senza dipendenze: `python3 scripts/test_update_push_notices.py`. `send_newsletter_updates.py` ha un parser minimo: `--help` stampa l'uso, un flag ignoto esce con codice 2 senza inviare (prima ogni argomento era ignorato e `--help` lanciava l'invio vero) e `--dry-run` stampa stato, oggetto e **numero** dei destinatari senza aprire SMTP ne' scrivere lo stato. Nessun percorso dello script stampa indirizzi — i log di CI sono pubblici — e gli errori di smtplib passano da `_redact_addresses`. #### Il tag di soppressione Un commit di manutenzione che tocca le schede senza aggiornarne il contenuto — rinomine di campi, riformattazioni, aggiunte di frontmatter — va marcato con `[skip notices]` **nell'oggetto**, cioe' nella prima riga del messaggio: `update_push_notices.py` esce senza scrivere nulla, e di conseguenza non parte neppure la newsletter. Solo la prima riga. Il corpo del messaggio e' prosa e puo' nominare il tag per esteso — spiegarlo, citarlo, motivare perche' non lo si usa — senza attivarlo. La regola nasce da un caso reale: un commit che nel corpo scriveva «nessun [skip notices]» per dire di non averlo usato ha soppresso notizia e newsletter della scheda che stava pubblicando. I commit con il tag si pushano da soli, o insieme ad altra sola manutenzione — mai nello stesso push di aggiornamenti di contenuto: basta che compaia nell'oggetto di un commit qualsiasi del push e sopprime le notizie dell'intero push, comprese quelle delle schede aggiornate davvero. I quattro validatori — indici e contenuti, frontmatter degli studiosi, raggiungibilita' delle pagine non indicizzate, coerenza fra la nav condivisa e le tre nav inline — girano in parallelo e non scrivono nulla. `validate.yml` li lancia tutti e quattro; `validate-content-indexes.yml` e `validate-scholars-frontmatter.yml` ne rilanciano due, con i propri filtri di path. Al quarto validatore `validate.yml` fa seguire un quinto passo, **Push-notices regression battery**, che non valida contenuto ma la toolchain delle notizie. Sta li' perche' `validate.yml` e' l'unico workflow senza filtri di path: la batteria gira quindi sullo stesso push che porta una modifica a `update_push_notices.py` o a `send_newsletter_updates.py`, che nessun altro workflow intercetta. Per la stessa ragione `update-push-notices.yml` **non** elenca `scripts/**` fra i suoi path, e non deve farlo: un push di sola toolchain non ha schede nel payload, farebbe scattare il ripiego su `git log` e riannuncerebbe contenuto gia' annunciato — con una seconda email agli iscritti. Accanto a quella, e per la stessa ragione, gira un sesto passo: **Check-7 regression battery**, `scripts/test_validate_content_indexes.py`, 47 asserzioni in undici gruppi: 28 fissano il contratto dei link interni sul caso che lo ha allargato, 19 il contratto dei nomi delle immagini dei codici (check 12). I primi casi sono letteralmente i due rimandi morti di Andrea di Bonaiuto: `.md` e `%20` verso un file inesistente devono uscire rossi, i due bersagli veri in forma `.html` verdi, e un `.md` verso un file che **esiste** rosso lo stesso. Gli altri gruppi coprono l'URL-decode, le maiuscole esatte, l'ancora scartata, le tre sedi da cui si leggono i link — il caso `- url:` in linea e' una regressione vera, `Maestro-di-Vico-lAbate.md` scrive cosi' — e il verso opposto, cioe' che la prosa che nomina `Content/**` non venga scambiata per un link. #### Il settimo passo non e' Python `validate.yml` chiude con **Gallery JS test battery**: 69 asserzioni che caricano `gallery.html` in un DOM vero con jsdom — 44 sul lightbox, 25 sull'invito al click che compare all'hover. La ragione e' che il sito ha JS non banale. `gallery.html` costruisce da sola la griglia dall'indice, la raggruppa, la filtra, la pagina e apre il lightbox: un validatore Python puo' leggerne il testo, non eseguirlo. Un repo conviene che testi nel linguaggio di cio' che testa, e per la galleria quel linguaggio e' JavaScript. Sta in `validate.yml` per la stessa ragione della batteria sopra: e' l'unico workflow senza filtri di path, quindi il guardiano vede ogni push che tocca la galleria. Tre passi in coda ai sei Python: `actions/setup-node@v4` con `node-version: '22'` e `cache: 'npm'`, poi `npm ci`, poi `npm test`. Nessun `continue-on-error`: un'asserzione rossa ferma il workflow come la ferma un validatore Python. La versione di Node e' fissata di proposito. Senza `node-version` setup-node prende quella preinstallata sul runner, che cambia senza preavviso; il 22 e' LTS fino ad aprile 2027, mentre il 20 e' fuori manutenzione da aprile 2026. `jsdom` 29 accetta `^20.19 || ^22.13 || >=24`. `npm ci` e non `npm install`: installa esattamente `package-lock.json` e fallisce se i due file divergono, cosi' il guardiano non cambia versione di jsdom sotto i piedi. Il lockfile va committato — e' il prerequisito di `npm ci`. `node_modules/` resta ignorato. Lo script `test` lancia i due file **e poi** somma i codici di uscita, invece di incatenarli con `&&`: con `&&` un rosso nel primo file impedirebbe al secondo di partire, e in CI si vedrebbe meta' del quadro. Cosi' girano sempre entrambi e il fallimento si propaga lo stesso. Costo misurato sul primo run: **+11s** sul workflow, da 24s a 35s. Di questi, 4.6s sono i due passi veri — 1.8s `npm ci` e 2.8s `npm test` — e il resto e' `setup-node`. Node 22 sta gia' nella tool cache del runner (`/opt/hostedtoolcache/node/22.23.2`), quindi non si scarica; la cache npm invece era vuota al primo giro (`npm cache is not found`) ed e' stata salvata in coda, percio' dal secondo push in poi l'install costa meno. **La convenzione zero-dipendenze resta vera dove e' nata.** Riguarda i validatori Python e le due batterie, `test_update_push_notices.py` e `test_validate_content_indexes.py`, che si lanciano con `python3 scripts/...` e nient'altro: vale ancora, e va mantenuta. La batteria JS e' l'eccezione motivata — un DOM vero non si simula a mano — ed e' l'unica cosa nel repo che abbia bisogno di `npm install`. In locale: npm install && npm test ### I vincoli da ricordare **I commit fatti con `GITHUB_TOKEN` non generano eventi.** È la protezione di GitHub contro i cicli infiniti. Conseguenza pratica: quando un workflow committa un indice aggiornato, il deploy *non* riparte da solo, e il sito resta indietro di un giro. Per questo entrambi i workflow che scrivono terminano con una chiamata esplicita: gh workflow run deploy-pages.yml Serve `actions: write` fra i permessi del workflow che la esegue. **I workflow che scrivono condividono un gruppo di concorrenza.** Girando in parallelo si contendevano il ramo, e uno dei due falliva con `cannot lock ref 'refs/heads/main'`. Il gruppo `repo-writes` con `cancel-in-progress: false` li mette in fila. **Il payload dell'evento non elenca i file modificati, e il ripiego guarda il range del push.** Le chiavi `added`/`modified` possono mancare del tutto: su questo repository mancano sempre. Su 132 PushEvent osservati, `commits` e' arrivato vuoto 132 volte, e nei tredici push di contenuto non soppressi dal tag il ramo payload non ha mai prodotto una voce. Il ripiego su git non e' quindi un'emergenza rara: e' il percorso normale, e va letto come tale. Quel ripiego leggeva **gli ultimi trenta commit**, e cosi' rimetteva in lista schede gia' annunciate. Il 16 settembre 2026 il push della Fontana Maggiore — un commit solo — ripesco' il push di Pisa del 7 settembre, dieci commit piu' indietro: tre notizie invece di una, e un oggetto `... and 2 more updates` al posto di `[New] `. Ora il ripiego guarda **i soli commit del push**, cioe' `before..after` dell'evento. I due campi sono di primo livello e non dipendono da `commits`: restano popolati esattamente nel caso in cui il ripiego serve — verificato su tutti e 132 gli eventi. Sullo stesso range, il push della Fontana produce una notizia sola. I casi limite scelgono tutti il verso conservativo, perche' una scheda non annunciata si rimedia con un push e un'email no: - `before` a zeri (primo push di un ramo): si annuncia il solo commit di punta; - `before` fuori dal checkout (push piu' lungo di `fetch-depth`): un solo `git fetch --deepen=50`, e se ancora non basta si ripiega sul commit di punta; - fuori da Actions, dove evento non c'e': vale il solo `HEAD`, cioe' l'analogo locale del push di un commit. Prima l'esecuzione locale leggeva trenta commit e dava un elenco che non corrispondeva a nessun push reale. `fetch-depth: 30` resta, ma per una ragione diversa: non piu' «dare storia al fallback», bensi' garantire che `before` sia nel checkout. **La firma anti-reinvio guarda tutto l'elenco.** `send_newsletter_updates.py` confronta con l'invio precedente una firma costruita su path, `pushed_at` e `change_type` di **tutte** le notizie, nel loro ordine — non della sola testa. Titoli e URL ne restano fuori di proposito: l'oggetto dell'email si costruisce sui titoli, e legarli alla firma farebbe partire un reinvio a ogni ritocco di un titolo. La sfumatura: se la testa e' gia' stata notificata ma le compagne cambiano, la newsletter riparte. E' il verso giusto, e lo e' diventato di piu' da quando il ripiego e' limitato al range: una lista diversa non puo' piu' nascere dal trascinamento di storia vecchia, quindi se l'elenco cambia e' perche' il push ha toccato schede diverse. **Il terzo trascinamento, chiuso.** `update_push_notices.py` continua a fondere le notizie del push con quelle gia' pubblicate — la card in homepage deve restare piena — ma la newsletter non legge piu' quella lista. Legge `latest_push`, che porta le sole notizie del push, e su quella calcola anche la firma anti-reinvio. Le voci del giro precedente restano in homepage e non escono piu' per email. Vedi «Cosa va in homepage, cosa va per email». ### Il deploy `deploy-pages.yml` costruisce con `bundle exec jekyll build`, non con `actions/jekyll-build-pages`: quest'ultima ignora il `Gemfile` e usa le versioni che GitHub decide. Con bundler la produzione usa le stesse versioni fissate in `Gemfile.lock`, cioe' quelle con cui si costruisce in locale. Conseguenza: in locale i comandi Jekyll vanno sempre preceduti da `bundle exec`. `vendor/` e' escluso in `_config.yml`: bundler vi installa le gem, e Jekyll vi troverebbe il template di esempio con data segnaposto, che fa fallire la build. ### Passi manuali rimasti **L'iscrizione alla newsletter.** Formspree raccoglie gli indirizzi nella sua interfaccia; vanno copiati a mano nel Secret `NEWSLETTER_SUBSCRIBERS` e in `newsletter_subscribers.csv` (fuori dal repo, escluso anche dalla build). Il sistema regge fino a una trentina di iscritti: oltre, il limite non e' GitHub ma iCloud Mail, che non e' un servizio di invio in massa. **L'indice della galleria.** Si rigenera in locale con `python3 scripts/build_gallery_index.py` e si committa insieme alla scheda che lo cambia, cosi' indice e contenuto restano allineati nello stesso commit. Il workflow **Update Gallery Index** in CI resta attivo e fa da controprova: trovando l'indice gia' aggiornato esce con «unchanged, nothing to commit». ### I dodici controlli `scripts/validate_content_indexes.py` contiene tutti i controlli sul contenuto; il docstring in cima li elenca. Sei meritano una nota: - **7** verifica i link fra pagine, **8** le ancore ai due contenitori (`endnotes.html`, `scholars.html`), **9** le immagini, **12** i nomi delle immagini dei codici, **10** l'ordine per slug dentro le sezioni-lettera di `endnotes.html`, **11** l'ordine dentro le sezioni di `scholars.html`. - Il check 7 copre **qualunque** link che punti sotto `Content/`, non i soli `.html`. Per mesi guardava solo quelli: la regex pretendeva l'estensione, e ogni altra forma gli passava accanto senza lasciare traccia. La scheda di Andrea di Bonaiuto ha portato online due rimandi morti — `/Content/Churches/Santa%20Maria%20Novella.md` e `/Content/Churches/Camposanto%20Monumentale%20Pisa.md` — rimasti 404 per tutta la vita della scheda senza che nulla li segnalasse; la scansione che ha esteso il check ne ha trovati altri sette della stessa famiglia, su tre schede, nella vecchia forma con gli spazi e senza estensione, residuo della rinomina a trattini. Il contratto ora e' uno solo, e ha tre conseguenze che conviene ricordare quando si scrive un rimando: - l'estensione dev'essere `.html`, **anche quando il `.md` esiste**. Un link `.md` funziona su GitHub, che mostra i sorgenti, e non sul sito, che pubblica pagine; un link senza estensione non e' servito da nessuna parte. Quando il bersaglio e' deducibile il messaggio d'errore lo propone gia' scritto; - il confronto avviene dopo l'URL-decode (`%20` torna uno spazio) e con le maiuscole esatte, con la stessa logica del check 9 e per la stessa ragione: GitHub Pages distingue le maiuscole, il Mac no; - l'ancora si scarta prima del confronto, perche' il bersaglio del check e' il file: delle ancore risponde gia' il check 8. I link si leggono dalle tre sedi in cui il repository li scrive davvero — i campi `url:` del frontmatter, i link markdown e gli `href` inline — e non dal testo grezzo. Serve in entrambi i versi: un `url:` di frontmatter puo' contenere spazi, ed e' esattamente la forma dei sette morti, che una regex delimitata dallo spazio troncherebbe a meta'; e la prosa di questo file nomina percorsi come `Content/**` che non sono link e non vanno verificati. Il grafo di raggiungibilita' del check 6 si costruisce sugli stessi link validi, cosi' la sua nozione di «linkata» non diverge in silenzio da quella del 7. - Il 10 e l'11 sono gemelli con la chiave opposta, e la differenza e' voluta. Le note si ordinano per slug; gli studiosi per titolo visualizzato, perche' li' lo slug e' spesso una forma storica che non segue il titolo: `max-seidel` per «Seidel, Max», `hendrik-willem-van-os` per «van Os, Hendrik Willem». Ordinare per slug segnalerebbe quelle voci come fuori posto; ordinare per titolo le assolve da solo, e nessuna lista di eccezioni e' necessaria. La collazione riproduce l'uso del file, non le regole bibliotecarie: cognome poi nome, diacritici e punteggiatura ignorati, confronto parola per parola — «De Marchi» < «De Rossi» < «Del Migliore» < «Delisle» < «Della Valle» — e una particella di una sola lettera che fa corpo con la parola seguente, cosi' «Ó Floinn» si archivia come OFloinn e sta dopo Offner, come O'Curry e O'Loughlin. - Il check 8 esiste perche' i contenitori sono due: un link puo' nominare un'ancora reale ma cercarla nel file sbagliato. Le note del mondo antico stavano in un terzo contenitore, `ancient-world.html`, fuso in `endnotes.html`: quel path ora e' solo uno stub di redirect che conserva l'hash, e non va piu' usato nei rimandi. - Il check 9 confronta le maiuscole in modo esplicito, senza affidarsi a `exists()`: il filesystem di macOS non distingue maiuscole e minuscole, GitHub Pages si'. Un `Armagh-01.jpg` che punta ad `armagh-01.jpg` funziona sul Mac e produce un'immagine vuota in produzione. - Il check 12 vale solo per `Content/Codex/` e confronta il nome di ogni immagine con la didascalia della sua figura: cartella uguale al nome della scheda, forma `-<foglio|etichetta>`, foglio e zeri uguali a quelli dichiarati. La grammatica sta in `scripts/codex_image_names.py`, che lo script `scripts/rename_codex_images.py` (dry-run per default) usa per ricavare i nomi. Un doppione di foglio non risolto sta in `OPEN_CONFLICTS` finche' la scheda non decide; oggi e' vuoto. I controlli da 1 a 9 coprono `Content/**/*.md` piu' i `.md` nella radice (glossary, dating-systems), esclusa la cartella `drafts/`. Il 10 e l'11 fanno eccezione: non guardano i `.md`, leggono i due contenitori HTML, `endnotes.html` e `scholars.html`. ### Convenzioni Nessuno spazio nei nomi di file e cartelle: gli spazi diventano `%20` negli URL e hanno gia' rotto centinaia di link. Le immagini seguono la forma `-NN.jpg`, tutta minuscola, con lo zero iniziale per mantenere l'ordinamento oltre la decima; per `Content/Codex/` vale la regola di «Codex Image Names», che nomina le immagini per foglio. I link interni sono sempre root-relative (`/Content/...`, `/endnotes.html`): i percorsi relativi dipendono dalla profondita' della cartella e si rompono a ogni riorganizzazione. Le schede in lavorazione stanno in `drafts/`, esclusa dai controlli, dalla build e — da questo giro — anche da git: la cartella e' in `.gitignore`. E' spazio di lavorazione locale e non entra mai nel repository. Prima era solo non tracciata, quindi compariva in `git status` e un `git add -A` l'avrebbe messa in stage in blocco. Le schede si spostano in `Content/` solo alla pubblicazione: da quel momento ogni commit fa partire una notifica agli iscritti, e una email non si ritira. Lo spostamento e' un `git mv` mancato per costruzione — il file sorgente e' ignorato — quindi la scheda entra come nuova, che e' il verso giusto: il suo `change_type` dev'essere `created`.