Commit aa3a9050 authored by Kourser's avatar Kourser
Browse files

« Servie » ne disait pas « servie avec quoi »

Les cinq jeux ajoutés hier n'atteignaient pas les organisations servies
l'avant-veille. Le jalon « livree_at » disait qu'une livraison avait eu lieu,
pas laquelle : une organisation qui avait reçu les dix-sept formes maison
restait à dix-sept, et rien ne le signalait — la bibliothèque s'ouvrait,
simplement plus courte que prévu.

C'est le même défaut que la fois précédente, d'un cran plus haut. « Ligne
absente » ne voulait pas dire « jamais servie » ; « servie » ne veut pas dire
« servie avec quoi ». À chaque fois, un état a été pris pour un autre parce que
les deux coïncidaient au moment où le code a été écrit.

Un numéro de livraison le dit. Chaque jeu déclare celle qui l'a introduit, et
l'on ne pose que ce qui est postérieur au numéro retenu par l'organisation. Ce
qui préserve les deux propriétés à la fois : une organisation existante reçoit
les jeux ajoutés depuis, sans qu'on touche à la base à la main ; et celle qui
avait vidé sa bibliothèque ne voit pas revenir ce qu'elle en avait ôté, ces
formes-là appartenant à une livraison déjà comptée.

La migration porte les lignes existantes à la livraison 1, la seule qui existât
quand le jalon a été posé.

Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
parent fb2e051f
Loading
Loading
Loading
Loading
Loading
+31 −0
Changes for apps/api/src/db/migrations/0045_bibliotheque_livraison.sql: 31 added lines, 0 removed lines.
Original line number Diff line number Diff line
-- ═══════════════════════════════════════════════════════════════════════════
-- Ce qui a été livré, et pas seulement qu'il l'a été (EF-1002a).
--
-- « livree_at » disait « cette organisation a été servie ». Cela suffisait tant
-- qu'il n'y avait qu'une livraison. Dès la seconde — cinq jeux repris du dépôt
-- public — les organisations servies la veille se sont retrouvées bloquées à ce
-- qu'elles avaient : le jalon disait « servie », il ne disait pas « servie avec
-- quoi ».
--
-- Un numéro le dit. Chaque forme livrée déclare la livraison qui l'a
-- introduite, et l'on ne pose que ce qui est postérieur au numéro retenu.
--
-- Ce qui préserve les deux propriétés à la fois :
--
--   Une organisation qui a vidé sa bibliothèque ne voit pas revenir ce qu'elle
--   en a ôté : ces formes-là appartiennent à une livraison déjà comptée.
--
--   Une organisation existante reçoit les jeux ajoutés depuis, sans qu'on
--   touche à la base à la main.
-- ═══════════════════════════════════════════════════════════════════════════

alter table bibliotheque_tableau
  add column if not exists livraison integer not null default 0;

-- Celles qui portent « livree_at » ont reçu la première livraison, et elle
-- seule : c'était la seule qui existait quand la colonne a été posée.
update bibliotheque_tableau set livraison = 1
 where livree_at is not null and livraison = 0;

comment on column bibliotheque_tableau.livraison is
  'EF-1002a : numéro de la dernière livraison de formes posée. 0 = jamais servie.';
+24 −18
Changes for apps/api/src/domaine/bibliotheque.ts: 24 added lines, 18 removed lines.
Original line number Diff line number Diff line
import type { Sql } from "../db/client.js";
import { formesLivrees } from "../donnees/bibliotheques/index.js";
import { LIVRAISON, formesLivrees } from "../donnees/bibliotheques/index.js";

/**
 * La bibliothèque de formes du tableau blanc (EF-1002a).
@@ -30,12 +30,17 @@ export const PLAFOND_OCTETS = 2 * 1024 * 1024;
 * si elle a été constituée avant, et rien n'est constitué avant si tout est à
 * dessiner. Les formes livrées sont donc posées à la première lecture.
 *
 * Posées une fois, et « livree_at » le dit. L'absence de ligne ne le disait
 * pas : la toile enregistre la bibliothèque au moindre changement, fût-il
 * vide, si bien qu'une organisation ayant déjà ouvert un tableau portait une
 * ligne « [] » et ne recevait jamais rien — le défaut qu'on corrigeait, à
 * l'identique. Ce qu'elle en retire ensuite reste retiré : une bibliothèque
 * volontairement vidée le demeure.
 * Un numéro de livraison, et non un jalon.
 *
 * « Ligne absente » ne disait pas « jamais servie » : la toile enregistre la
 * bibliothèque au moindre changement, fût-il vide. Un jalon « servie » ne
 * disait pas « servie avec quoi » : à la deuxième livraison, les organisations
 * servies la veille sont restées à ce qu'elles avaient. Les deux défauts se
 * présentaient de la même façon — une bibliothèque plus courte que prévu, sans
 * rien pour le signaler.
 *
 * Le numéro dit les deux. On ne pose que ce qui lui est postérieur, ce qui
 * garde la propriété qui compte : ce qu'une organisation a ôté reste ôté.
 *
 * L'écriture se fait à la lecture, ce qui n'est pas élégant. L'alternative
 * serait de semer à la création de l'organisation, et de rattraper les
@@ -44,24 +49,25 @@ export const PLAFOND_OCTETS = 2 * 1024 * 1024;
 * elles changent.
 */
export async function lire(sql: Sql, organisationId: string): Promise<unknown[]> {
  const [b] = await sql<{ items: unknown[]; livree_at: Date | null }[]>`
    select items, livree_at from bibliotheque_tableau
  const [b] = await sql<{ items: unknown[]; livraison: number }[]>`
    select items, livraison from bibliotheque_tableau
     where organisation_id = ${organisationId}`;
  if (b?.livree_at) return b.items;
  const recue = b?.livraison ?? 0;
  if (recue >= LIVRAISON) return b?.items ?? [];

  // Ajoutées à ce qui est là, jamais substituées : une organisation qui a
  // constitué ses propres formes avant cette livraison les garde.
  const deja = new Set((b?.items ?? []).map(
    (i) => (i as { id?: unknown }).id));
  // Seulement ce qui est postérieur à ce qu'elle a reçu, et ajouté à ce qu'elle
  // a : une organisation qui a constitué ses propres formes les garde, et celle
  // qui en a ôté ne les voit pas revenir.
  const deja = new Set((b?.items ?? []).map((i) => (i as { id?: unknown }).id));
  const fusion = [
    ...(b?.items ?? []),
    ...formesLivrees().filter((f) => !deja.has(f["id"])),
    ...formesLivrees(recue).filter((f) => !deja.has(f["id"])),
  ];
  await sql`
    insert into bibliotheque_tableau (organisation_id, items, livree_at)
    values (${organisationId}, ${sql.json(fusion as never)}, now())
    insert into bibliotheque_tableau (organisation_id, items, livree_at, livraison)
    values (${organisationId}, ${sql.json(fusion as never)}, now(), ${LIVRAISON})
    on conflict (organisation_id) do update
      set items = excluded.items, livree_at = now()`;
      set items = excluded.items, livree_at = now(), livraison = ${LIVRAISON}`;
  return fusion;
}

+28 −10
Changes for apps/api/src/donnees/bibliotheques/index.ts: 28 added lines, 10 removed lines.
Original line number Diff line number Diff line
@@ -36,13 +36,28 @@ import topologie from "./topologie-reseau.json" with { type: "json" };
/** Ce qu'un identifiant doit être pour qu'on puisse dédoublonner. */
interface Forme { id?: unknown }

const REPRISES: [string, unknown[]][] = [
  ["topologie-reseau", topologie as unknown[]],
  ["logigramme", logigramme as unknown[]],
  ["azure", azure as unknown[]],
  ["kubernetes", kubernetes as unknown[]],
  ["aws-serverless", awsServerless as unknown[]],
  ["logos-techniques", logosTechniques as unknown[]],
/**
 * La livraison courante.
 *
 * À incrémenter en ajoutant un jeu, jamais autrement. Le numéro retenu par une
 * organisation dit ce qu'elle a déjà reçu ; ne descend que ce qui lui est
 * postérieur.
 *
 * Ce détour a une raison précise, apprise en le manquant. Un simple jalon
 * « servie » suffisait tant qu'il n'y avait qu'une livraison : à la seconde,
 * les organisations servies la veille sont restées à ce qu'elles avaient, sans
 * que rien ne le signale — la bibliothèque s'ouvrait, simplement plus courte.
 */
export const LIVRAISON = 2;

/** Chaque jeu dit la livraison qui l'a introduit. */
const REPRISES: [string, number, unknown[]][] = [
  ["topologie-reseau", 2, topologie as unknown[]],
  ["logigramme", 2, logigramme as unknown[]],
  ["azure", 2, azure as unknown[]],
  ["kubernetes", 2, kubernetes as unknown[]],
  ["aws-serverless", 2, awsServerless as unknown[]],
  ["logos-techniques", 2, logosTechniques as unknown[]],
];

/**
@@ -56,9 +71,12 @@ const REPRISES: [string, unknown[]][] = [
 * tiré le même, et une collision ferait disparaître une forme sans rien dire —
 * la bibliothèque s'ouvrirait simplement plus courte que prévu.
 */
export function formesLivrees(): Record<string, unknown>[] {
  const tout: Record<string, unknown>[] = [...bibliothequeLivree()];
  for (const [jeu, formes] of REPRISES) {
export function formesLivrees(depuis = 0): Record<string, unknown>[] {
  // Les nôtres sont de la première livraison : une organisation qui les a
  // ôtées ne doit pas les voir revenir parce qu'on a ajouté autre chose.
  const tout: Record<string, unknown>[] = depuis < 1 ? [...bibliothequeLivree()] : [];
  for (const [jeu, introduite, formes] of REPRISES) {
    if (introduite <= depuis) continue;
    for (const f of formes as Forme[]) {
      tout.push({ ...(f as Record<string, unknown>), id: `${jeu}:${String(f.id ?? "")}` });
    }
+36 −0
Changes for apps/api/src/verification/tableaux.ts: 36 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -205,6 +205,42 @@ export async function verifierTableaux(
    { status: "published", id: "site", elements: [{ type: "ellipse", id: "b" }] },
  ];
  await bibliotheque.poserDepuisCrise(sql, crise.criseId, formes, compteId);
  /**
   * Une organisation servie par une livraison antérieure reçoit la suite.
   *
   * C'est le défaut suivant, et il s'est présenté comme le précédent : un jalon
   * « servie » ne dit pas « servie avec quoi ». Les organisations servies la
   * veille sont restées aux dix-sept formes de la première livraison quand cinq
   * jeux ont été ajoutés, sans que rien ne le signale.
   */
  const [orgAncienne] = await sql<{ id: string }[]>`
    insert into organisation (nom) values (${"Servie hier"}) returning id`;
  await sql`
    insert into bibliotheque_tableau (organisation_id, items, livree_at, livraison)
    values (${orgAncienne!.id}, ${sql.json(bibliothequeLivree() as never)}, now(), 1)`;
  const rattrapee = await bibliotheque.lire(sql, orgAncienne!.id);
  r.verifie("EF-1002a", "une organisation restée à la livraison 1 reçoit les jeux suivants",
    rattrapee.length === livree.length, `${rattrapee.length} formes`);

  // Et la propriété qui compte tient toujours : ce qu'elle a ôté reste ôté.
  const [orgVidee] = await sql<{ id: string }[]>`
    insert into organisation (nom) values (${"Vidée après la livraison 1"}) returning id`;
  await sql`
    insert into bibliotheque_tableau (organisation_id, items, livree_at, livraison)
    values (${orgVidee!.id}, '[]'::jsonb, now(), 1)`;
  const apresVidage = await bibliotheque.lire(sql, orgVidee!.id) as { id: string }[];
  r.verifie("EF-1002a", "mais celle qui avait vidé la sienne ne voit pas revenir la livraison 1",
    !apresVidage.some((f) => f.id === "kastell-serveur"),
    apresVidage.some((f) => f.id === "kastell-serveur") ? "les nôtres sont revenues" : "restées ôtées");
  r.verifie("EF-1002a", "et reçoit tout de même celles qu'elle n'a jamais eues",
    apresVidage.length === livree.length - bibliothequeLivree().length,
    `${apresVidage.length} formes`);

  // Rien ne redescend deux fois.
  const stable = await bibliotheque.lire(sql, orgAncienne!.id);
  r.verifie("EF-1002a", "une seconde lecture ne repose rien",
    stable.length === rattrapee.length);

  r.verifie("EF-1002a", "ce qu'on y ajoute survit au rechargement",
    (await bibliotheque.lirePourCrise(sql, crise.criseId)).length === 2);