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

16 KiB

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) :

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
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 check0 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

  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

// 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

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) :

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.