Files
system_update/docs/superpowers/plans/2026-06-05-tache2-sj0-socle.md
T
gilles 0fbca06d3d 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>
2026-06-05 19:50:25 +02:00

409 lines
16 KiB
Markdown

# Tâche 2 — SJ-0 (socle : types + réduction + résolution de profil) — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: subagent-driven-development / executing-plans. Étapes checkbox.
**Goal:** Poser le socle de la tâche 2, **purement additif** : étendre `shared/types.ts` (unions élargies + blocs optionnels, rétro-compatibles), enrichir le réducteur de lignes (préfixes Docker), et ajouter `resolveTemplate(action, osFamily)` avec fallback `base`. Aucun changement de wiring (refresh/execute inchangés).
**Architecture:** Extensions additives. Référence design : `docs/design/tache2/40-contrats-json.md` (types), `60-profils-os-machine.md` (résolution), `99-couverture-gate.md`. Tous les ajouts sont optionnels/élargis ⇒ un `UpdateSnapshot`/`ExecutionResult` du jalon 1 reste strictement valide (vérifié par `tsc`).
**Tech Stack:** TypeScript, vitest.
---
## Invariants
- **Rétro-compat stricte** : ne rien retirer/renommer. Préserver `MachineStatus`, `MachineView`, `ServerCapabilities` (WIP) et tout autre contenu actuel de `shared/types.ts`.
- **Aucun changement de comportement** : on n'altère PAS `refresh.ts`/`execute.ts` en SJ-0 (la bascule du refresh sur les nouveaux templates = SJ-1).
- Réducteur : **garder `reduceAptLines`** (imports existants dans refresh/execute) ; ajouter les préfixes Docker et un alias `reduceLines`. **Ne PAS renommer le fichier** `aptReduce.ts` (éviter de toucher les imports de refresh/execute — churn/concurrence).
- Tree partagé / WIP concurrent : ne toucher QUE `shared/types.ts`, `server/templates/aptReduce.ts` (+test), `server/templates/render.ts` (+ test resolveTemplate), et les fichiers de test. **Ne pas committer.**
## File Structure
```
shared/types.ts # MODIF : unions élargies + interfaces + champs optionnels
shared/types.test.ts # NOUVEAU : verrouille la rétro-compat (compile + runtime léger)
server/templates/aptReduce.ts # MODIF : préfixes Docker + alias reduceLines
server/templates/aptReduce.test.ts # MODIF : +cas Docker
server/templates/render.ts # MODIF : +resolveTemplate
server/templates/resolveTemplate.test.ts # NOUVEAU
```
---
## Task 1 : Étendre `shared/types.ts`
**Files:** Modify `shared/types.ts` ; Create `shared/types.test.ts`.
- [ ] **Step 1 : Relire le fichier réel** (`rtk read shared/types.ts`) pour repérer le contenu à préserver (`MachineStatus`, `MachineView`, `ServerCapabilities`, etc.).
- [ ] **Step 2 : Appliquer les extensions** (élargir les unions existantes, remplacer `AptPackage`/`UpdateSnapshot`/`ExecutionResult` par les versions étendues, AJOUTER les nouvelles interfaces). Ne pas supprimer l'existant. Contenu cible (depuis `docs/design/tache2/40-contrats-json.md`) :
```ts
export type OsFamily = "debian" | "ubuntu" | "proxmox" | "raspbian" | "unknown";
export type MachineKind =
| "physical" | "vm" | "proxmox_host" | "lxc"
| "raspberry_pi" | "workstation" | "unknown";
export type AptProxyMode = "direct" | "runtime" | "persistent";
export type ActionType =
| "apt_full_upgrade" | "reboot"
| "apt_update_analyze" | "apt_upgrade" | "apt_dist_upgrade"
| "apt_autoremove" | "apt_clean" | "reboot_verified"
| "docker_scan" | "docker_inspect_current" | "docker_pull_check"
| "docker_compose_apply" | "docker_prune_images" | "docker_compose_down"
| "machine_probe" | "post_install";
export type SnapshotStatus = "ok" | "updates_available" | "warning" | "error";
// ExecutionStatus, MachineStatus : INCHANGÉS (préserver l'existant)
export interface AptPackage {
name: string;
currentVersion: string | null;
targetVersion: string;
origin: string | null;
arch?: string;
operation?: "upgrade" | "install" | "remove" | "hold";
severityHint?: "normal" | "security";
}
export interface AptSnapshotDetail {
enabled: boolean;
count: number;
rebootRequired: boolean;
packages: AptPackage[];
status?: SnapshotStatus;
upgradeCount?: number;
distUpgradeCount?: number;
installed?: AptPackage[];
removed?: AptPackage[];
held?: string[];
rebootPkgs?: string[];
}
export interface DockerSnapshotService {
serviceName: string;
image: string;
currentImageId?: string | null;
currentDigest?: string | null;
candidateImageId?: string | null;
candidateDigest?: string | null;
currentVersion?: string | null;
candidateVersion?: string | null;
sourceUrl?: string | null;
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;
declaredRoots?: string[];
stacks: DockerSnapshotStack[];
status?: SnapshotStatus;
}
export interface SnapshotError {
source: "apt" | "docker" | "post_install" | "ssh" | "system";
kind: string;
severity: "info" | "warning" | "error";
message: string;
remediation?: string;
importantLines?: string[];
}
export interface UpdateSnapshot {
machineId: string;
hostname: string;
os: { family: OsFamily; version: string };
checkedAt: string;
status: MachineStatus;
apt: AptSnapshotDetail;
schemaVersion?: number;
kind?: "apt_update_analyze" | "docker_scan" | "reboot_check" | "combined";
machineKind?: MachineKind;
docker?: DockerSnapshot;
errors?: SnapshotError[];
rawHints?: { logImportantLines: string[] };
}
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[];
applied: AptChange[];
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>;
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";
action: ActionType;
status: ExecutionStatus;
rebootRequiredAfterRun: boolean;
importantLogLines: string[];
rawLogRef: string;
reportRef: string;
schemaVersion?: number;
apt?: AptExecutionResult;
docker?: DockerExecutionResult;
reboot?: RebootResult;
postInstall?: PostInstallResult;
errors?: SnapshotError[];
}
```
> Préserver `MachineStatus`, `MachineView`, `ServerCapabilities` et tout autre contenu présent. Le bloc `apt` de `UpdateSnapshot` reste **requis** (forme jalon 1) ; `mode` de `ExecutionResult` était le littéral `"manual"` → l'union l'inclut.
- [ ] **Step 3 : Test de rétro-compat `shared/types.test.ts`**
```ts
import { describe, it, expect } from "vitest";
import type { UpdateSnapshot, ExecutionResult } from "./types.js";
describe("rétro-compatibilité des contrats", () => {
it("un snapshot jalon 1 (sans blocs optionnels) reste valide", () => {
const snap: UpdateSnapshot = {
machineId: "m1", hostname: "h", os: { family: "debian", version: "12" },
checkedAt: "2026-06-05T10:00:00Z", status: "ok",
apt: { enabled: true, count: 0, rebootRequired: false, packages: [] },
};
expect(snap.apt.count).toBe(0);
});
it("une exécution jalon 1 (mode manual, sans blocs) reste valide", () => {
const exec: ExecutionResult = {
executionId: "e1", machineId: "m1", startedAt: "a", finishedAt: "b",
mode: "manual", action: "apt_full_upgrade", status: "ok",
rebootRequiredAfterRun: false, importantLogLines: [], rawLogRef: "r", reportRef: "rr",
};
expect(exec.action).toBe("apt_full_upgrade");
});
it("accepte les nouveaux blocs optionnels", () => {
const snap: UpdateSnapshot = {
machineId: "m1", hostname: "h", os: { family: "proxmox", version: "8" },
checkedAt: "t", status: "updates_available",
apt: { enabled: true, count: 1, rebootRequired: false, packages: [], status: "updates_available" },
schemaVersion: 1, kind: "apt_update_analyze", machineKind: "proxmox_host",
docker: { enabled: false, installed: false, count: 0, stacks: [] },
errors: [],
};
expect(snap.docker?.installed).toBe(false);
});
});
```
- [ ] **Step 4 :** Run `rtk pnpm vitest run shared/types.test.ts` → PASS (3). Puis `rtk pnpm check`**0 erreur** (c'est le vrai test de rétro-compat : si un consommateur existant casse à cause d'un retrait/renommage, tsc le révèle). Si `check` signale une erreur dans un fichier consommateur (`refresh.ts`/`execute.ts`/`machines.ts`/WIP) causée par TON changement de types, corrige le type (rends additif) — ne casse pas les consommateurs.
- [ ] **Step 5 : (pas de commit)**
---
## Task 2 : Réducteur enrichi (préfixes Docker)
**Files:** Modify `server/templates/aptReduce.ts`, `server/templates/aptReduce.test.ts`.
- [ ] **Step 1 : Relire `server/templates/aptReduce.ts`** (état réel).
- [ ] **Step 2 : Ajouter un cas Docker au test `aptReduce.test.ts`**
```ts
it("garde aussi les lignes Docker utiles", () => {
const raw = [
"Pulling jellyfin ...",
"Status: Downloaded newer image for jellyfin/jellyfin:latest",
"Recreating jellyfin ...",
"Started jellyfin",
"blabla inutile",
"Total reclaimed space: 1.2GB",
].join("\n");
expect(reduceLines(raw)).toEqual([
"Pulling jellyfin ...",
"Status: Downloaded newer image for jellyfin/jellyfin:latest",
"Recreating jellyfin ...",
"Started jellyfin",
"Total reclaimed space: 1.2GB",
]);
});
```
Ajouter `reduceLines` à l'import existant : `import { reduceAptLines, reduceLines } from "./aptReduce.js";`
- [ ] **Step 3 : Lancer (échec attendu)**`rtk pnpm vitest run server/templates/aptReduce.test.ts` → FAIL (`reduceLines` introuvable / lignes Docker non gardées).
- [ ] **Step 4 : Étendre `server/templates/aptReduce.ts`**
```ts
// server/templates/aptReduce.ts
const PREFIXES = [
// APT / dpkg (jalon 1)
"Inst ", "Conf ", "Remv ", "Err ", "E:", "W:", "dpkg:",
// Docker (SJ-0)
"Pulling", "Digest", "Status", "Downloaded newer image", "Recreating", "Started", "Error",
];
const CONTAINS = [
"reboot-required", "REBOOT_REQUIRED",
"deleted", "Total reclaimed space",
];
/** Garde uniquement les lignes informatives (APT + Docker) d'une sortie brute. */
export function reduceLines(raw: string): string[] {
return raw
.split("\n")
.map((l) => l.trimEnd())
.filter((l) => PREFIXES.some((p) => l.startsWith(p)) || CONTAINS.some((c) => l.includes(c)));
}
/** Alias rétro-compatible (jalon 1) : même comportement, conserve les imports existants. */
export const reduceAptLines = reduceLines;
```
> Garder l'export `reduceAptLines` (utilisé par `refresh.ts`/`execute.ts`). `reduceLines` est le nouveau nom canonique.
- [ ] **Step 5 :** Run `rtk pnpm vitest run server/templates/aptReduce.test.ts` → PASS (cas APT existants + nouveau cas Docker). `rtk pnpm check` → 0 erreur.
- [ ] **Step 6 : (pas de commit)**
---
## Task 3 : `resolveTemplate(action, osFamily)`
**Files:** Modify `server/templates/render.ts` ; Create `server/templates/resolveTemplate.test.ts`.
- [ ] **Step 1 : Relire `server/templates/render.ts`** (état réel : `TEMPLATES_ROOT`, `renderTemplate`, `TemplateVars`).
- [ ] **Step 2 : Test `server/templates/resolveTemplate.test.ts`**
```ts
import { describe, it, expect } from "vitest";
import { resolveTemplate } from "./render.js";
describe("resolveTemplate", () => {
it("retombe sur apt/ quand aucun dossier OS spécifique n'existe (fonction exists fournie)", () => {
const noneExist = () => false;
expect(resolveTemplate("full-upgrade", "proxmox", noneExist)).toBe("apt/full-upgrade.sh.tpl");
expect(resolveTemplate("update-analyze", "debian", noneExist)).toBe("apt/update-analyze.sh.tpl");
});
it("choisit le template OS spécifique quand il existe", () => {
const proxmoxExists = (rel: string) => rel === "proxmox/full-upgrade.sh.tpl";
expect(resolveTemplate("full-upgrade", "proxmox", proxmoxExists)).toBe("proxmox/full-upgrade.sh.tpl");
});
it("unknown retombe toujours sur apt/", () => {
const all = () => true;
expect(resolveTemplate("clean", "unknown", all)).toBe("apt/clean.sh.tpl");
});
});
```
- [ ] **Step 3 : Lancer (échec)**`rtk pnpm vitest run server/templates/resolveTemplate.test.ts` → FAIL.
- [ ] **Step 4 : Ajouter `resolveTemplate` à `server/templates/render.ts`** (sans toucher `renderTemplate`/`TemplateVars` existants ; ajouter l'import `existsSync`) :
```ts
import { existsSync } from "node:fs";
// ... (TEMPLATES_ROOT, renderTemplate existants inchangés) ...
/** Existence par défaut d'un template relatif à templates/. */
function defaultExists(rel: string): boolean {
return existsSync(resolve(TEMPLATES_ROOT, rel));
}
/**
* Résout le chemin de template le plus spécifique pour (action, OS) :
* `<osFamily>/<action>.sh.tpl` s'il existe, sinon fallback base `apt/<action>.sh.tpl`.
* `exists` est injectable pour les tests.
*/
export function resolveTemplate(
action: string,
osFamily: string,
exists: (rel: string) => boolean = defaultExists,
): string {
const specific = `${osFamily}/${action}.sh.tpl`;
if (osFamily !== "unknown" && osFamily !== "apt" && exists(specific)) return specific;
return `apt/${action}.sh.tpl`;
}
```
> Note : `renderTemplate` accepte déjà un `relPath` (ex. `apt/full-upgrade.sh.tpl`), donc `renderTemplate(resolveTemplate(action, osFamily), vars)` fonctionnera en SJ-1 sans modifier `renderTemplate`.
- [ ] **Step 5 :** Run `rtk pnpm vitest run server/templates/resolveTemplate.test.ts` → PASS (3). `rtk pnpm check` → 0 erreur.
- [ ] **Step 6 : (pas de commit)**
---
## Task 4 : Vérification finale SJ-0
- [ ] **Step 1 :** `rtk pnpm check && rtk pnpm test && rtk pnpm build`
Expected: 0 erreur TS ; tous tests verts (49 Phase 2 + 3 types + 1 Docker reduce + 3 resolveTemplate ≈ 56) ; build OK.
- [ ] **Step 2 :** Reporter : types étendus rétro-compatibles (tsc vert = preuve), réducteur Docker, `resolveTemplate` prêt pour SJ-1. **Ne pas committer.**
---
## Self-Review (couverture SJ-0)
- Types étendus (unions + blocs optionnels) → Task 1, rétro-compat verrouillée par `tsc` + test. ✓
- Réducteur + préfixes Docker → Task 2 (`reduceLines` + alias `reduceAptLines` conservé). ✓
- `resolveTemplate(action, osFamily)` + fallback base → Task 3. ✓
- `schemaVersion` → présent dans `UpdateSnapshot`/`ExecutionResult` (optionnel). ✓
- Aucun wiring modifié (refresh/execute intacts) ⇒ non-régression jalon 1. ✓
Décisions assumées : fichier `aptReduce.ts` NON renommé (alias `reduceLines` ajouté) pour éviter de toucher les imports de refresh/execute (churn/concurrence) — le nom canonique `reduceLines` est exporté ; renommage physique reporté à un nettoyage ultérieur. `resolveTemplate` avec `exists` injectable pour testabilité des deux branches.