Skip to content

Commit 6abd7ee

Browse files
maxaloneclaude
andcommitted
P48: le manutenzioni ricorrenti diventano una procedura leggibile
I passi di ogni manutenzione entrano nel registro (campo steps dei processi) e vengono pubblicati in manutenzione.json insieme al sito del tenant, perche' il pannello di gestione possa mostrarli a chi deve eseguirli. Chi riceve l'avviso su Telegram trova cosa fare e in che ordine, senza dover conoscere il sistema. - nuovo core/generators/lib/cadences.mjs: sorgente unica delle scadenze, usata sia dall'avviso Telegram sia dal file pubblicato. Prima la logica stava solo in check-cadences.mjs e i due consumatori avrebbero potuto raccontare storie diverse. - il file pubblicato dichiara la data di scadenza, non i giorni di ritardo: quel numero invecchierebbe tra una pubblicazione e l'altra, il conto lo fa chi legge con la data di oggi. - nuovo PRC-dogfooding-anchor: il ciclo di ancoraggio aveva un controllo ma nessuna procedura scritta. La sua scadenza continua a leggersi dai pacchetti in snapshots/anchors/, non da last_run, quindi non puo' divergere dalla realta'. - il messaggio Telegram dice chi deve agire e rimanda al pannello invece di elencare comandi: chi lo riceveva senza essere tecnico non poteva farci niente. - corretto processes_url, rimasto al percorso pre-P44: il link nell'avviso del 17/08 rispondeva 404. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent d200ba9 commit 6abd7ee

14 files changed

Lines changed: 527 additions & 70 deletions

.github/workflows/publish.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ jobs:
3737
- run: npm run validate
3838
- run: npm run score
3939
- run: npm run build-site
40+
# Rigenera manutenzione.json, che il pannello di gestione legge per dire
41+
# a chi lo apre cosa c'è da fare. Sta qui e non solo nel giro settimanale
42+
# delle evidenze perché i passi delle procedure vivono nel registro: una
43+
# correzione a un passo deve arrivare al pannello col push che la fa, non
44+
# il lunedì dopo.
45+
- run: npm run build-cadences
4046
# P45 F5 — genera il sito della RADICE (la pagina del prodotto) e le sue
4147
# copie di compatibilità site/badge.svg e site/score.json. Senza questo
4248
# step la radice pubblicherebbe le sole copie committate: il badge, che

core/generators/build-cadences.mjs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { writeFileSync } from "node:fs";
2+
import { join } from "node:path";
3+
import { loadRegistry } from "./lib/registry.mjs";
4+
import { loadTenant, TENANT_SITE_DIR } from "./lib/tenant.mjs";
5+
import { computeCadences } from "./lib/cadences.mjs";
6+
7+
// Pubblica le manutenzioni ricorrenti in un file che il pannello di gestione
8+
// legge per mostrarle a chi deve eseguirle. I passi vivono nel registro
9+
// (campo steps dei processi) e non nel pannello: così la procedura si corregge
10+
// modificando il registro pubblico, senza un rilascio del motore, e resta una
11+
// cosa sola con ciò che il registro dichiara di fare.
12+
//
13+
// Il file NON contiene "quanti giorni di ritardo": quel numero invecchierebbe
14+
// tra una pubblicazione e l'altra. Contiene la data di scadenza, e chi legge
15+
// calcola il ritardo con la data di oggi — così il file resta vero anche se
16+
// non viene rigenerato per settimane.
17+
function main() {
18+
const cfg = loadTenant();
19+
const items = computeCadences(cfg, loadRegistry()).map(({ overdue_days, ...rest }) => rest);
20+
21+
const out = {
22+
generated_at: new Date().toISOString(),
23+
registry_url: cfg.operations?.processes_url ?? null,
24+
items,
25+
};
26+
27+
const file = join(TENANT_SITE_DIR, "manutenzione.json");
28+
writeFileSync(file, JSON.stringify(out, null, 2) + "\n");
29+
30+
console.log(`Manutenzioni pubblicate in ${file}`);
31+
for (const i of items) {
32+
const passi = i.steps.length ? `${i.steps.length} passi` : "NESSUN PASSO SCRITTO";
33+
console.log(` ${i.id} — prossima scadenza ${i.next_due ?? "(mai eseguito)"}${passi}`);
34+
}
35+
}
36+
37+
main();

core/generators/check-cadences.mjs

Lines changed: 22 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,6 @@
1-
import { readFileSync, readdirSync, existsSync } from "node:fs";
2-
import { join } from "node:path";
3-
import { loadRegistry, byFolder } from "./lib/registry.mjs";
4-
import { loadTenant, TENANT_SNAPSHOTS_DIR } from "./lib/tenant.mjs";
5-
6-
const SNAPSHOTS_DIR = TENANT_SNAPSHOTS_DIR;
7-
const ANCHORS_DIR = join(SNAPSHOTS_DIR, "anchors");
8-
9-
// Data di nascita del GTF: baseline per i processi mai eseguiti finora
10-
// (nessun last_run dichiarato) e per l'attesa del primo ancoraggio dogfooding.
11-
const GTF_BIRTH = "2026-07-09";
12-
13-
// Grazia oltre il mese per il ciclo dogfooding (cadenza mensile, GTF-ARCH §6.4).
14-
const DOGFOODING_GRACE_DAYS = 35;
15-
16-
function daysSince(dateStr) {
17-
return Math.floor((Date.now() - new Date(dateStr).getTime()) / 86400000);
18-
}
19-
20-
// Ultimo bundle dogfooding committato (fonte di verità unica: nessun campo
21-
// da aggiornare a mano, a differenza dei PRC qui sotto — vedi CTL-dogfooding-anchor).
22-
function latestAnchor() {
23-
if (!existsSync(ANCHORS_DIR)) return null;
24-
const bundles = readdirSync(ANCHORS_DIR)
25-
.filter((f) => /^\d{4}-\d{2}-bundle\.json$/.test(f))
26-
.sort();
27-
if (bundles.length === 0) return null;
28-
try {
29-
const data = JSON.parse(readFileSync(join(ANCHORS_DIR, bundles[bundles.length - 1]), "utf8"));
30-
return data.generated_at ?? null;
31-
} catch {
32-
return null;
33-
}
34-
}
1+
import { loadRegistry } from "./lib/registry.mjs";
2+
import { loadTenant } from "./lib/tenant.mjs";
3+
import { computeCadences, overdueOnly } from "./lib/cadences.mjs";
354

365
// B3 (voce del piano 2026, img-auth-hub): un allarme che raggiunge solo il
376
// gestore non è un allarme se il gestore è la persona indisponibile. Secondo
@@ -63,6 +32,8 @@ async function sendTelegram(text) {
6332
}
6433

6534
async function main() {
35+
const cfg = loadTenant();
36+
6637
if (process.env.TEST_TELEGRAM === "true") {
6738
await sendTelegram(
6839
"Genesis Trust Framework — messaggio di prova (check-cadences.mjs, avviato manualmente con test_telegram). " +
@@ -72,46 +43,30 @@ async function main() {
7243
return;
7344
}
7445

75-
const cfg = loadTenant();
76-
const records = loadRegistry();
77-
const overdue = [];
78-
79-
const anchorDate = latestAnchor();
80-
const anchorDays = anchorDate ? daysSince(anchorDate) : null;
81-
if (!anchorDate || anchorDays > DOGFOODING_GRACE_DAYS) {
82-
overdue.push({
83-
id: "CTL-dogfooding-anchor",
84-
title: "Ancoraggio dogfooding mensile",
85-
days: anchorDays,
86-
hint: `npm run anchor-monthly, poi attesta il bundle su ${cfg.operations.attestation_site}`,
87-
});
88-
}
89-
90-
const prc = byFolder(records, "processes").filter((p) => p.frequency_days);
91-
for (const p of prc) {
92-
const ref = p.last_run ?? GTF_BIRTH;
93-
const days = daysSince(ref);
94-
if (days > p.frequency_days) {
95-
overdue.push({
96-
id: p.id,
97-
title: p.title,
98-
days,
99-
hint: p.last_run ? `ultima esecuzione dichiarata: ${p.last_run}` : "mai eseguito da quando il GTF esiste (nessun last_run nel registro)",
100-
});
101-
}
102-
}
46+
const overdue = overdueOnly(computeCadences(cfg, loadRegistry()));
10347

10448
if (overdue.length === 0) {
10549
console.log("Nessun processo ricorrente scaduto.");
10650
return;
10751
}
10852

109-
const lines = overdue.map(
110-
(o) => `- <b>${o.title}</b> (${o.id}) — ${o.days !== null ? `${o.days} giorni` : "mai eseguito"}. ${o.hint}`
111-
);
53+
// Il messaggio dice chi deve agire e manda in UN posto solo, dove i passi
54+
// sono scritti per esteso. Prima elencava i comandi da dare: chi li riceveva
55+
// senza essere tecnico non poteva farci niente, e chi li riceveva da tecnico
56+
// doveva comunque ricostruire il resto della procedura a memoria.
57+
const lines = overdue.map((o) => {
58+
const chi = o.who ? ` — se ne occupa: ${o.who}` : "";
59+
const da = o.never_run ? "mai eseguito finora" : `in ritardo di ${o.overdue_days} giorni`;
60+
return `- <b>${o.title}</b> (${da})${chi}`;
61+
});
62+
63+
const panel = cfg.operations?.panel_url;
11264
const text =
113-
`Genesis Trust Framework — processi in scadenza:\n\n${lines.join("\n")}\n\n` +
114-
`Dettagli: ${cfg.operations.processes_url}`;
65+
`Genesis Trust Framework — manutenzioni da fare:\n\n${lines.join("\n")}\n\n` +
66+
(panel
67+
? `Cosa fare, passo per passo: ${panel}\n(${cfg.operations.panel_label ?? "pannello di gestione"})\n\n`
68+
: "") +
69+
`Registro dei processi: ${cfg.operations.processes_url}`;
11570

11671
console.log(text.replace(/<\/?b>/g, ""));
11772
await sendTelegram(text);

core/generators/lib/cadences.mjs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { readFileSync, readdirSync, existsSync } from "node:fs";
2+
import { join } from "node:path";
3+
import { loadRegistry } from "./registry.mjs";
4+
import { TENANT_SNAPSHOTS_DIR } from "./tenant.mjs";
5+
6+
// Sorgente unica delle scadenze ricorrenti. Esiste perché due consumatori
7+
// devono raccontare la stessa identica storia: l'allarme Telegram
8+
// (check-cadences.mjs) e la scheda Manutenzione del pannello, alimentata dal
9+
// file pubblicato da build-cadences.mjs. Quando la logica stava solo nel
10+
// primo, il secondo avrebbe potuto dire "tutto a posto" mentre il bot
11+
// segnalava una scadenza — la classe di divergenza che il framework esiste
12+
// per evitare.
13+
14+
const ANCHORS_DIR = join(TENANT_SNAPSHOTS_DIR, "anchors");
15+
16+
// Grazia oltre il mese per il ciclo di ancoraggio (GTF-ARCH §6.4): un
17+
// pacchetto mensile non cade mai a giorno fisso.
18+
export const ANCHOR_GRACE_DAYS = 35;
19+
20+
const DAY_MS = 86400000;
21+
22+
function isoDay(ms) {
23+
return new Date(ms).toISOString().slice(0, 10);
24+
}
25+
26+
function toMs(dateStr) {
27+
return new Date(dateStr).getTime();
28+
}
29+
30+
// Ultimo pacchetto di ancoraggio committato. È la fonte di verità di quel
31+
// ciclo: nessuna data da aggiornare a mano, quindi nessuna data che possa
32+
// mentire — il pacchetto o c'è o non c'è.
33+
function latestAnchorDate() {
34+
if (!existsSync(ANCHORS_DIR)) return null;
35+
const bundles = readdirSync(ANCHORS_DIR)
36+
.filter((f) => /^\d{4}-\d{2}-bundle\.json$/.test(f))
37+
.sort();
38+
if (bundles.length === 0) return null;
39+
try {
40+
const data = JSON.parse(readFileSync(join(ANCHORS_DIR, bundles[bundles.length - 1]), "utf8"));
41+
return data.generated_at ?? null;
42+
} catch {
43+
return null;
44+
}
45+
}
46+
47+
function registryUrl(cfg, rel) {
48+
const base = cfg.operations?.registry_blob_base;
49+
return base ? `${base}/${rel.split("\\").join("/")}` : null;
50+
}
51+
52+
function makeItem({ id, title, record, rel, cfg, lastDone, dueMs, neverRun }) {
53+
const now = Date.now();
54+
const overdueDays = dueMs !== null && now > dueMs ? Math.floor((now - dueMs) / DAY_MS) : null;
55+
return {
56+
id,
57+
title,
58+
who: record?.who ?? null,
59+
needs_technical: record?.needs_technical ?? null,
60+
duration_minutes: record?.duration_minutes ?? null,
61+
frequency_days: record?.frequency_days ?? null,
62+
last_done: lastDone,
63+
never_run: Boolean(neverRun),
64+
next_due: dueMs !== null ? isoDay(dueMs) : null,
65+
overdue_days: overdueDays,
66+
steps: record?.steps ?? [],
67+
registry_url: rel ? registryUrl(cfg, rel) : null,
68+
};
69+
}
70+
71+
// Elenco completo delle manutenzioni ricorrenti, scadute e non, ordinate per
72+
// urgenza. Chi vuole solo le scadute filtra su overdue_days !== null: la
73+
// scheda del pannello ha bisogno anche delle altre, per dire "nulla da fare"
74+
// invece di non dire niente.
75+
export function computeCadences(cfg, records = loadRegistry()) {
76+
const birth = cfg.operations?.birth_date;
77+
const items = [];
78+
79+
const processes = [...records.values()].filter((r) => r.folder === "processes");
80+
const byId = new Map(processes.map((r) => [r.record.id, r]));
81+
82+
// 1. Il ciclo di ancoraggio: la cadenza si legge dai pacchetti, i passi dal
83+
// processo che lo descrive. Due sorgenti perché sono due cose diverse —
84+
// quando è stato fatto, e come si fa.
85+
const anchorProcessId = cfg.operations?.anchor_process_id;
86+
const anchorEntry = anchorProcessId ? byId.get(anchorProcessId) : null;
87+
if (anchorEntry) {
88+
const anchorDate = latestAnchorDate();
89+
items.push(
90+
makeItem({
91+
id: anchorEntry.record.id,
92+
title: anchorEntry.record.title,
93+
record: anchorEntry.record,
94+
rel: anchorEntry.rel,
95+
cfg,
96+
lastDone: anchorDate ? isoDay(toMs(anchorDate)) : null,
97+
dueMs: anchorDate ? toMs(anchorDate) + ANCHOR_GRACE_DAYS * DAY_MS : 0,
98+
neverRun: !anchorDate,
99+
})
100+
);
101+
}
102+
103+
// 2. I processi a cadenza dichiarata. Senza last_run si parte dalla nascita
104+
// del registro: un processo mai eseguito deve risultare scaduto, non
105+
// perennemente "non ancora dovuto".
106+
for (const entry of processes) {
107+
const p = entry.record;
108+
if (!p.frequency_days) continue;
109+
const ref = p.last_run ?? birth;
110+
items.push(
111+
makeItem({
112+
id: p.id,
113+
title: p.title,
114+
record: p,
115+
rel: entry.rel,
116+
cfg,
117+
lastDone: p.last_run ?? null,
118+
dueMs: ref ? toMs(ref) + p.frequency_days * DAY_MS : 0,
119+
neverRun: !p.last_run,
120+
})
121+
);
122+
}
123+
124+
items.sort((a, b) => {
125+
if (a.next_due === b.next_due) return a.id.localeCompare(b.id);
126+
if (a.next_due === null) return -1;
127+
if (b.next_due === null) return 1;
128+
return a.next_due < b.next_due ? -1 : 1;
129+
});
130+
return items;
131+
}
132+
133+
export function overdueOnly(items) {
134+
return items.filter((i) => i.overdue_days !== null);
135+
}

core/schemas/process.schema.json

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,25 @@
1515
"last_run": { "type": "string", "format": "date", "description": "Data dell'ultima esecuzione, aggiornata a mano dal maintainer dopo averla svolta" },
1616
"last_outcome": { "type": "string", "enum": ["ok", "failed"], "description": "Esito dell'esecuzione registrata in last_run" },
1717
"visibility": { "type": "string", "enum": ["public", "internal", "secret"], "default": "public" },
18-
"why_not_public": { "type": "string" }
18+
"why_not_public": { "type": "string" },
19+
"who": { "type": "string", "description": "Chi esegue il processo, in chiaro e per esteso — a differenza di owner, che è un indirizzo o un ruolo. Mostrato a chi apre la scheda Manutenzione del pannello: serve a capire in un colpo d'occhio se una scadenza è propria o di qualcun altro." },
20+
"needs_technical": { "type": "boolean", "description": "true se il processo richiede terminale, repository o credenziali tecniche. La scheda Manutenzione lo usa per dire a un non tecnico di girare la scadenza al gestore invece di provarci." },
21+
"duration_minutes": { "type": "integer", "minimum": 1, "description": "Quanto dura una singola esecuzione, non il costo mensile (per quello c'è human_cost_minutes_month)." },
22+
"steps": {
23+
"type": "array",
24+
"description": "Passi in lingua piana, nell'ordine in cui vanno eseguiti, scritti per chi non conosce il sistema. Sono la sorgente unica della guida mostrata nella scheda Manutenzione del pannello admin: la scheda non contiene testo scritto a mano (principio PRN-03).",
25+
"items": {
26+
"type": "object",
27+
"properties": {
28+
"do": { "type": "string", "description": "L'azione, all'imperativo: cosa fare, non perché." },
29+
"note": { "type": "string", "description": "Facoltativa: l'unica cosa che serve sapere per non sbagliare il passo." },
30+
"link": { "type": "string", "format": "uri" },
31+
"link_label": { "type": "string" }
32+
},
33+
"required": ["do"],
34+
"additionalProperties": false
35+
}
36+
}
1937
},
2038
"required": ["id", "title", "owner", "trigger", "output", "human_cost_minutes_month"],
2139
"allOf": [

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@
1111
"scan-scorecard": "node core/generators/scan-scorecard.mjs",
1212
"anchor-monthly": "node core/generators/anchor-monthly.mjs",
1313
"check-cadences": "node core/generators/check-cadences.mjs",
14+
"build-cadences": "node core/generators/build-cadences.mjs",
1415
"check-core": "node core/generators/check-core-isolation.mjs",
1516
"score": "node core/generators/score.mjs",
1617
"build-site": "node core/generators/build-site.mjs",
1718
"build-root": "node core/generators/build-root.mjs",
1819
"build-changelog": "node core/generators/build-changelog.mjs",
19-
"build": "npm run validate && npm run check-core && npm run score && npm run build-site && npm run build-root && npm run build-changelog"
20+
"build": "npm run validate && npm run check-core && npm run score && npm run build-site && npm run build-cadences && npm run build-root && npm run build-changelog"
2021
},
2122
"license": "MIT",
2223
"dependencies": {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
id: PRC-dogfooding-anchor
2+
title: Ancoraggio mensile del registro delle evidenze in Bitcoin
3+
owner: it@spaziogenesi.org
4+
who: Il gestore per la parte tecnica; l'attestazione sul sito può farla chiunque abbia accesso al pannello
5+
needs_technical: true
6+
duration_minutes: 20
7+
trigger: "Cadenza mensile (CTL-dogfooding-anchor). La scadenza non si legge da last_run ma dal pacchetto più recente in snapshots/anchors/: è il pacchetto stesso la prova che il mese è stato coperto, quindi non c'è una data da aggiornare a mano e non può divergere dalla realtà."
8+
output: "Un pacchetto <periodo>-bundle.json committato nel registro, attestato sul servizio e ancorato in Bitcoin via OpenTimestamps; impronta e link permanente riportati in EVD-dogfooding-anchor."
9+
produces_evidence: [EVD-dogfooding-anchor]
10+
human_cost_minutes_month: 20
11+
steps:
12+
- do: "Nella cartella del registro, esegui: npm run anchor-monthly"
13+
note: "Crea il pacchetto del mese e stampa a schermo il nome del file e la sua impronta. Tieni da parte quell'impronta: serve al passo 4."
14+
- do: "Apri il servizio di attestazione, scheda Attesta, e scegli il file appena creato in tenants/attestazione/snapshots/anchors/."
15+
link: "https://attestazione.spaziogenesi.org"
16+
link_label: "Vai ad attestare"
17+
- do: "Genera l'attestazione e scarica il certificato PDF."
18+
note: "Scaricare il PDF non è un passaggio facoltativo: è quello che innesca davvero l'ancoraggio in Bitcoin. Fermarsi all'attestazione non conta come fatto."
19+
- do: "Controlla che l'impronta stampata sul certificato sia identica a quella stampata al passo 1."
20+
- do: "Committa il pacchetto nel registro e riporta impronta e link permanente del certificato in EVD-dogfooding-anchor."
21+
note: "Da qui in poi chiunque può ricalcolare l'impronta del file nel repository, confrontarla col certificato e verificare la prova su opentimestamps.org senza chiedere niente a noi. È il motivo per cui questa procedura esiste."
22+
visibility: public

tenants/attestazione/registry/processes/PRC-restore-drill.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,17 @@ human_cost_minutes_month: 10
88
frequency_days: 180
99
last_run: "2026-07-19"
1010
last_outcome: ok
11+
who: Il gestore
12+
needs_technical: true
13+
duration_minutes: 90
14+
steps:
15+
- do: "Scegli tre opere già attestate di periodi diversi: una recente, una dei primi mesi, una con l'ancoraggio Bitcoin ormai maturo."
16+
- do: "Scarica quelle tre dal backup esterno (non dall'archivio di produzione: è il backup che si sta provando)."
17+
- do: "Su ciascuna, verifica tre cose indipendenti: che la firma estratta dal certificato sia valida, che la prova di ancoraggio regga contro un blocco Bitcoin reale, e che il badge pubblico dell'opera risulti verde."
18+
note: "Se una sola delle tre fallisce, la prova è fallita: va scritto così nel verbale, non ammorbidito."
19+
- do: "Cancella i file ripristinati: non devono finire in nessun repository."
20+
- do: "Scrivi il verbale in docs/verbali/ e aggiorna last_run e last_outcome in questo file."
21+
note: "Il verbale va scritto anche quando è andato tutto bene: una prova senza verbale non è dimostrabile a nessuno."
22+
link: "https://github.com/SPAZIO-GENESI/gtf/edit/main/tenants/attestazione/registry/processes/PRC-restore-drill.yaml"
23+
link_label: "Apri il file da aggiornare"
1124
visibility: public

0 commit comments

Comments
 (0)