docs: roadmap tâches 1.9-8 (briefs, gates de validation, designs tâche 2) + plans d'implémentation
Cartographie complète (liste_taches/coherence_taches), briefs tacheN + gates validation_tacheN, design tâche 2 (docs/design/tache2/), specs/plans jalon 1-2 et tâche 1.9/2 (Phase 1, Phase 2, SJ-0→3). Validations consignées (1.9 ✅, 2-8 🟡). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
# 40 — Contrats JSON canoniques étendus + types TypeScript
|
||||
|
||||
> Axe B + livrable §4.3. Tranche la question §3.5 (extensions de `shared/types.ts`). **Tous les ajouts sont rétro-compatibles** : champs optionnels, unions élargies. Un `UpdateSnapshot`/`ExecutionResult` du jalon 1 reste strictement valide.
|
||||
|
||||
---
|
||||
|
||||
## 1. Principe de rétro-compatibilité
|
||||
|
||||
État actuel (`shared/types.ts`) :
|
||||
|
||||
```ts
|
||||
export type OsFamily = "debian" | "ubuntu" | "unknown";
|
||||
export type AptProxyMode = "direct" | "runtime";
|
||||
export type ActionType = "apt_full_upgrade" | "reboot";
|
||||
// UpdateSnapshot.apt: { enabled, count, rebootRequired, packages: AptPackage[] }
|
||||
// ExecutionResult: { ... action: ActionType, status, rebootRequiredAfterRun, importantLogLines, rawLogRef, reportRef }
|
||||
```
|
||||
|
||||
Règles d'extension :
|
||||
1. **Élargir les unions** (`OsFamily`, `AptProxyMode`, `ActionType`) — additif, aucun retrait.
|
||||
2. **Ajouter des blocs optionnels** (`docker?`, `errors?`, `apt` détaillé optionnel, `reboot?`, `postInstall?`) — un payload sans ces blocs reste valide.
|
||||
3. **Ne jamais retirer ni renommer** un champ existant. Le jalon 1 émet `apt: { enabled, count, rebootRequired, packages }` ; on **ajoute** des champs optionnels à côté.
|
||||
4. Versionner via `schemaVersion?: number` (aligné `snapshots.schema_version` / `executions.schema_version` de `tache1.9.md`). Absence ⇒ version 1.
|
||||
|
||||
---
|
||||
|
||||
## 2. Extensions des unions
|
||||
|
||||
```ts
|
||||
// Élargissement additif. Le jalon 1 ("debian"|"ubuntu"|"unknown") reste valide.
|
||||
export type OsFamily = "debian" | "ubuntu" | "proxmox" | "raspbian" | "unknown";
|
||||
|
||||
export type MachineKind =
|
||||
| "physical" | "vm" | "proxmox_host" | "lxc"
|
||||
| "raspberry_pi" | "workstation" | "unknown";
|
||||
|
||||
// "persistent" ajouté (écriture dans /etc/apt/apt.conf.d/).
|
||||
export type AptProxyMode = "direct" | "runtime" | "persistent";
|
||||
|
||||
export type ActionType =
|
||||
// jalon 1 (conservés tels quels)
|
||||
| "apt_full_upgrade" | "reboot"
|
||||
// APT
|
||||
| "apt_update_analyze" | "apt_upgrade" | "apt_dist_upgrade"
|
||||
| "apt_autoremove" | "apt_clean" | "reboot_verified"
|
||||
// Docker
|
||||
| "docker_scan" | "docker_inspect_current" | "docker_pull_check"
|
||||
| "docker_compose_apply" | "docker_prune_images" | "docker_compose_down"
|
||||
// probe + custom
|
||||
| "machine_probe" | "post_install";
|
||||
|
||||
export type SnapshotStatus = "ok" | "updates_available" | "warning" | "error";
|
||||
export type ExecutionStatus = "ok" | "warning" | "error"; // inchangé
|
||||
```
|
||||
|
||||
> `reboot` (jalon 1) et `reboot_verified` coexistent : `reboot_verified` ajoute la vérification boot_id ; le code jalon 1 continue d'émettre `reboot`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Snapshot canonique étendu (`UpdateSnapshot`)
|
||||
|
||||
```ts
|
||||
export interface AptPackage {
|
||||
name: string;
|
||||
currentVersion: string | null;
|
||||
targetVersion: string;
|
||||
origin: string | null;
|
||||
// Ajouts optionnels (rétro-compatibles) :
|
||||
arch?: string;
|
||||
operation?: "upgrade" | "install" | "remove" | "hold";
|
||||
severityHint?: "normal" | "security";
|
||||
}
|
||||
|
||||
export interface AptSnapshotDetail {
|
||||
enabled: boolean;
|
||||
count: number;
|
||||
rebootRequired: boolean;
|
||||
packages: AptPackage[];
|
||||
// Ajouts optionnels :
|
||||
status?: SnapshotStatus; // ok | updates_available | warning | error
|
||||
upgradeCount?: number; // simulation `upgrade`
|
||||
distUpgradeCount?: number; // simulation `dist-upgrade`
|
||||
installed?: AptPackage[]; // nouveaux paquets (dist-upgrade)
|
||||
removed?: AptPackage[]; // suppressions prévues (=> status warning)
|
||||
held?: string[]; // paquets retenus (=> status warning)
|
||||
rebootPkgs?: string[]; // depuis reboot-required.pkgs
|
||||
}
|
||||
|
||||
export interface DockerSnapshotService {
|
||||
serviceName: string;
|
||||
image: string; // image ref (ex. jellyfin/jellyfin:latest)
|
||||
currentImageId?: string | null;
|
||||
currentDigest?: string | null;
|
||||
candidateImageId?: string | null; // après pull-check
|
||||
candidateDigest?: string | null;
|
||||
currentVersion?: string | null; // label OCI org.opencontainers.image.version
|
||||
candidateVersion?: string | null;
|
||||
sourceUrl?: string | null; // label OCI source
|
||||
status?: "up_to_date" | "updates_available" | "warning" | "error";
|
||||
}
|
||||
|
||||
export interface DockerSnapshotStack {
|
||||
name: string;
|
||||
workingDir: string;
|
||||
composeFiles: string[];
|
||||
projectName?: string | null;
|
||||
status: "candidate" | "enabled" | "ignored" | "error";
|
||||
detectedBy?: "root_scan" | "label" | "manual";
|
||||
services: DockerSnapshotService[];
|
||||
}
|
||||
|
||||
export interface DockerSnapshot {
|
||||
enabled: boolean;
|
||||
installed: boolean;
|
||||
count: number; // services avec update dispo
|
||||
declaredRoots?: string[];
|
||||
stacks: DockerSnapshotStack[];
|
||||
status?: SnapshotStatus;
|
||||
}
|
||||
|
||||
export interface SnapshotError {
|
||||
source: "apt" | "docker" | "post_install" | "ssh" | "system";
|
||||
kind: string; // voir 50-erreurs.md (taxonomie)
|
||||
severity: "info" | "warning" | "error";
|
||||
message: string; // nettoyé, jamais de secret
|
||||
remediation?: string;
|
||||
importantLines?: string[];
|
||||
}
|
||||
|
||||
export interface UpdateSnapshot {
|
||||
machineId: string;
|
||||
hostname: string;
|
||||
os: { family: OsFamily; version: string };
|
||||
checkedAt: string; // ISO 8601
|
||||
status: MachineStatus;
|
||||
apt: AptSnapshotDetail; // bloc jalon 1 conservé, champs additifs optionnels
|
||||
// Ajouts optionnels (rétro-compatibles) :
|
||||
schemaVersion?: number;
|
||||
kind?: "apt_update_analyze" | "docker_scan" | "reboot_check" | "combined";
|
||||
machineKind?: MachineKind;
|
||||
docker?: DockerSnapshot;
|
||||
errors?: SnapshotError[];
|
||||
rawHints?: { logImportantLines: string[] };
|
||||
}
|
||||
```
|
||||
|
||||
> Le bloc `apt` reste **requis** (présent au jalon 1) ; seuls ses champs *additifs* sont optionnels. `docker`, `errors`, `machineKind`, `kind`, `schemaVersion` sont **optionnels** → un snapshot jalon 1 (sans eux) reste valide.
|
||||
|
||||
### Bloc Docker minimal exigé par la validation (couverture §6)
|
||||
|
||||
Le bloc snapshot Docker contient au minimum : stacks déclarés (`declaredRoots`), stacks candidats (`stacks[].status="candidate"`), services (`stacks[].services[]`), image ref actuelle (`image`), image ID actuelle (`currentImageId`), digest actuel si dispo (`currentDigest`), labels de version si dispo (`currentVersion`), image ID/digest candidat après pull (`candidateImageId`/`candidateDigest`), statut `up_to_date|updates_available|warning|error`. ✔
|
||||
|
||||
---
|
||||
|
||||
## 4. Résultat d'exécution étendu (`ExecutionResult`)
|
||||
|
||||
```ts
|
||||
export interface AptChange {
|
||||
name: string;
|
||||
arch?: string;
|
||||
fromVersion: string | null;
|
||||
toVersion: string | null;
|
||||
operation: "upgraded" | "installed" | "removed" | "unchanged";
|
||||
origin?: string | null;
|
||||
}
|
||||
|
||||
export interface AptExecutionResult {
|
||||
planned: AptPackage[]; // ce qui était prévu (simulation pré-action)
|
||||
applied: AptChange[]; // diff dpkg réel before/after
|
||||
installed: AptChange[];
|
||||
removed: AptChange[];
|
||||
held: string[];
|
||||
errors?: SnapshotError[];
|
||||
rebootRequiredAfterRun: boolean;
|
||||
}
|
||||
|
||||
export interface DockerImageChange {
|
||||
stack: string;
|
||||
serviceName?: string;
|
||||
imageRef?: string;
|
||||
fromImageId?: string | null;
|
||||
toImageId?: string | null;
|
||||
fromDigest?: string | null;
|
||||
toDigest?: string | null;
|
||||
operation: "pulled" | "recreated" | "pruned";
|
||||
}
|
||||
|
||||
export interface DockerExecutionResult {
|
||||
pull?: { changes: DockerImageChange[]; errors?: SnapshotError[] };
|
||||
up?: { recreated: string[]; running: string[]; exited: string[]; errors?: SnapshotError[] };
|
||||
prune?: { imagesDeleted: string[]; bytesReclaimed: number; errors?: SnapshotError[] };
|
||||
errors?: SnapshotError[];
|
||||
}
|
||||
|
||||
export interface RebootResult {
|
||||
beforeBootId: string | null;
|
||||
afterBootId: string | null;
|
||||
requestedAt: string;
|
||||
sshWentDownAt: string | null;
|
||||
sshCameBackAt: string | null;
|
||||
waitedSeconds: number;
|
||||
status: "ok" | "reboot_command_failed" | "ssh_never_went_down"
|
||||
| "machine_did_not_return" | "boot_id_unchanged" | "timeout";
|
||||
lastRebootDurationSeconds?: number;
|
||||
nextRecommendedWaitSeconds?: number;
|
||||
errors?: SnapshotError[];
|
||||
}
|
||||
|
||||
export interface PostInstallResult {
|
||||
profilesRun: string[];
|
||||
variablesUsed: Record<string, string | number | boolean>; // NON sensible uniquement
|
||||
filesModified: string[];
|
||||
packagesInstalled: string[];
|
||||
servicesEnabled: string[];
|
||||
rebootsRequested: boolean;
|
||||
networkChange?: {
|
||||
oldEndpoint: string | null;
|
||||
newEndpoint: string | null;
|
||||
reconnectHost: string | null;
|
||||
};
|
||||
errors?: SnapshotError[];
|
||||
}
|
||||
|
||||
export interface ExecutionResult {
|
||||
executionId: string;
|
||||
machineId: string;
|
||||
startedAt: string;
|
||||
finishedAt: string;
|
||||
mode: "manual" | "scheduled" | "hermes_requested"; // élargi (jalon 1 = "manual")
|
||||
action: ActionType;
|
||||
status: ExecutionStatus;
|
||||
rebootRequiredAfterRun: boolean;
|
||||
importantLogLines: string[];
|
||||
rawLogRef: string;
|
||||
reportRef: string;
|
||||
// Ajouts optionnels (rétro-compatibles) :
|
||||
schemaVersion?: number;
|
||||
apt?: AptExecutionResult;
|
||||
docker?: DockerExecutionResult;
|
||||
reboot?: RebootResult;
|
||||
postInstall?: PostInstallResult;
|
||||
errors?: SnapshotError[];
|
||||
}
|
||||
```
|
||||
|
||||
> `mode` était `"manual"` (littéral) au jalon 1. L'élargir en union `"manual" | "scheduled" | "hermes_requested"` reste compatible (le jalon 1 émet toujours `"manual"`). Tous les nouveaux blocs (`apt`, `docker`, `reboot`, `postInstall`, `errors`) sont optionnels → une exécution jalon 1 reste valide.
|
||||
|
||||
---
|
||||
|
||||
## 5. Extension de `TemplateVars` (rendu Mustache)
|
||||
|
||||
```ts
|
||||
export interface TemplateVars {
|
||||
aptProxy?: string | null; // existant
|
||||
// Ajouts (tous optionnels) :
|
||||
osProfile?: OsFamily;
|
||||
machineKind?: MachineKind;
|
||||
confValues?: boolean;
|
||||
inactivityTimeout?: number;
|
||||
// Docker :
|
||||
composeRoots?: string; // liste rendue shell-safe par le backend
|
||||
composeScanDepth?: number;
|
||||
stackDir?: string;
|
||||
aggressive?: boolean; // prune agressif
|
||||
// Custom :
|
||||
operatorUser?: string;
|
||||
packages?: string; // liste shell-safe
|
||||
newHostname?: string;
|
||||
interfaceName?: string;
|
||||
staticAddress?: string;
|
||||
reconnectHost?: string;
|
||||
dockerUser?: string;
|
||||
composeRoot?: string;
|
||||
rebootAfterInstall?: boolean;
|
||||
// ... champs de profil custom (typés au cas par cas en tâche 4)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Déduplication (empreinte fonctionnelle)
|
||||
|
||||
- **APT** : `dedupKey = os_family + "|" + package + "|" + from + "|" + to + "|" + origin`. Permet à Hermes de mutualiser une même mise à jour vue sur plusieurs machines (une seule recherche web, un seul résumé). Stocké dans `apt_planned_packages.dedup_key` / `apt_applied_packages.dedup_key`.
|
||||
- **Docker** : `dedupKey = image + "|" + fromDigest + "|" + toDigest` ; fallback `image + "|" + fromImageId + "|" + toImageId` quand le digest manque.
|
||||
|
||||
Le calcul de `dedupKey` se fait **côté backend TS** (déterministe), pas dans le shell.
|
||||
|
||||
---
|
||||
|
||||
## 7. Réduction déterministe avant Hermes/MCP
|
||||
|
||||
Le réducteur actuel (`server/templates/aptReduce.ts`) garde les lignes : `Inst `, `Conf `, `Remv `, `Err `, `E:`, `W:`, `dpkg:`, `reboot-required`/`REBOOT_REQUIRED`. **Extension proposée** (renommage suggéré `reduceLines.ts`, additif, sans casser `reduceAptLines`) ajoutant les préfixes Docker : `Pulling`, `Digest`, `Status`, `Downloaded newer image`, `Recreating`, `Started`, `Error`, `deleted`, `Total reclaimed space`.
|
||||
|
||||
Ce que Hermes reçoit : **JSON canonique réduit** (`important_json`) + lignes importantes (`importantLogLines`). **Jamais** le log brut complet (archivé dans `raw_artifacts`/`rawLogPath`), jamais de secret.
|
||||
|
||||
---
|
||||
|
||||
## 8. Mapping vers `tache1.9.md` (tables dérivées)
|
||||
|
||||
| Bloc JSON | Table dérivée | Colonnes clés |
|
||||
|---|---|---|
|
||||
| `apt.packages` / `installed` / `removed` / `held` (simulation) | `apt_planned_packages` | `mode`, `operation`, `current_version`, `target_version`, `origin`, `dedup_key` |
|
||||
| `apt.applied` (diff dpkg) | `apt_applied_packages` | `from_version`, `to_version`, `operation`, `dedup_key` |
|
||||
| `errors[]` source apt | `apt_errors` | `kind`, `severity`, `message`, `important_lines_json`, `remediation` |
|
||||
| `docker.stacks[]` | `docker_compose_stacks` + `docker_stack_services` | `status`, `detected_by`, `current_image_id`, `candidate_digest`, … |
|
||||
| `docker.pull/up/prune changes` | `docker_image_events` | `from_image_id`, `to_image_id`, `operation`, `bytes_reclaimed` |
|
||||
| lignes importantes/notices | `important_messages` | `source`, `category`, `package_name`, `message` |
|
||||
| payload complet snapshot | `snapshots.payload_json` + `important_json` | `kind`, `schema_version`, `status` |
|
||||
| payload complet exécution | `executions.result_json` + `important_json` | `error_kind`, `error_message`, `exit_code` |
|
||||
|
||||
Le JSON complet reste la vérité canonique (archivé) ; les tables dérivées servent recherche/filtres/dédup/badges (conforme à la règle structurante `tache1.9.md §2`).
|
||||
Reference in New Issue
Block a user