Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";

const source = await readFile(new URL("./extract-document-markdown/index.ts", import.meta.url), "utf8");

// PRESERVATION_INSTRUCTIONS is built as many small string-literal segments joined with `+`, each
// wrapped for readability. A regex run directly against the .ts source would break every time an
// assertion happens to straddle two segments (the quote/plus/newline between them isn't part of
// the actual instruction text sent to the model). Reconstructing the joined string once here lets
// every assertion below read like the sentence the model actually receives.
function extractPreservationInstructions(tsSource) {
const start = tsSource.indexOf("const PRESERVATION_INSTRUCTIONS =");
const end = tsSource.indexOf("function jsonResponse");
assert.ok(start !== -1 && end !== -1, "PRESERVATION_INSTRUCTIONS block must be present");
const block = tsSource.slice(start, end).replace(/\/\/[^\n]*\n/g, "\n");
const segments = block.match(/"(?:[^"\\]|\\.)*"/g) || [];
return segments
.map((segment) => segment.slice(1, -1).replace(/\\"/g, '"').replace(/\\'/g, "'"))
.join("");
}

const instructions = extractPreservationInstructions(source);

test("requires visual/graphical content to be described, at the same non-optional level as tables", () => {
assert.match(instructions, /est soumis à la même obligation que les tableaux/);
assert.match(instructions, /ce n'est pas une option laissée à ton appréciation/);
});

test("forbids transcribing a heading that announces a figure without describing the figure itself", () => {
assert.match(instructions, /Un intitulé suivi d'une figure/);
assert.match(instructions, /ne le transcris jamais seul, sans décrire ce que montre la figure/);
assert.match(instructions, /si tu vois l'intitulé, tu dois voir la figure/);
});

test("requires describing what distinguishes each case in a comparative series of figures", () => {
assert.match(instructions, /plusieurs figures comparatives se succèdent sous des intitulés voisins/);
assert.match(instructions, /décris ce qui distingue chaque cas/);
});

test("uses a single fixed marker for visual-content descriptions, placed right after the heading", () => {
assert.match(instructions, /forme fixe et reconnaissable `\*\*Schéma visible :\*\*`/);
assert.match(instructions, /Place cette description juste après l'intitulé qu'elle complète/);
assert.match(instructions, /identifiable comme une description que tu as produite et non comme le texte du professeur/);
});

test("excludes purely decorative elements from the visual-content description requirement", () => {
assert.match(instructions, /éléments purement décoratifs \(logo, filet de séparation, photo d'illustration, élément de charte graphique\)/);
assert.match(instructions, /qu'il est inutile de décrire/);
});

test("still forbids inventing beyond what a figure shows, deferring to the no-fabrication rule", () => {
assert.match(instructions, /Décris uniquement ce que la figure montre, sans l'interpréter ni compléter ce qu'elle ne montre pas/);
assert.match(instructions, /l'interdiction d'inventer prime toujours/);
});

test("does not regress the existing table, presenter-notes, page-numbering or no-fabrication instructions", () => {
assert.match(instructions, /Ne résume pas et ne reformule pas : préserve fidèlement le contenu/);
assert.match(instructions, /Il est interdit d'omettre un tableau ou une donnée/);
assert.match(instructions, /Avant de transcrire les notes de présentateur d'une page, vérifie qu'elles/);
assert.match(instructions, /Ne mélange jamais les notes d'une page avec le contenu de la page suivante/);
// The printed-slide-number rule lives outside PRESERVATION_INSTRUCTIONS (in pageOutputRule), so
// it's checked against the full .ts source rather than the reconstructed instructions string.
assert.match(source, /Numéro imprimé : N/);
assert.match(source, /N'insère aucun en-tête de position dans le champ `markdown`/);
});
24 changes: 23 additions & 1 deletion supabase/functions/extract-document-markdown/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,29 @@ const PRESERVATION_INSTRUCTIONS =
"mise en forme visuelle d'origine (couleurs, surlignage), qui n'est jamais " +
"une raison suffisante pour omettre des données. Avant de terminer la " +
"transcription d'une page, vérifie qu'aucun tableau, grille ou liste de " +
"valeurs visible sur la diapositive n'a été oublié.";
"valeurs visible sur la diapositive n'a été oublié. " +
// Constat fait sur une transcription réelle de cours de mathématiques : trois séries de
// graphiques comparatifs (fonctions continues/discontinues/discrètes) ont disparu sans laisser
// aucun texte citable, alors que les tableaux ci-dessus étaient déjà protégés. Ce paragraphe
// porte l'exigence sur le contenu graphique au même niveau, dans toutes les disciplines.
"Le contenu porté par une figure — courbe, graphe, diagramme, carte, frise, montage ou schéma " +
"annoté, dans n'importe quelle discipline — est soumis à la même obligation que les tableaux : " +
"ce n'est pas une option laissée à ton appréciation, c'est une exigence de transcription au même " +
"titre. Un intitulé suivi d'une figure (une étiquette terminée par deux-points, ou un titre suivi " +
"d'une zone graphique) est une promesse de contenu : ne le transcris jamais seul, sans décrire ce " +
"que montre la figure qui lui est associée — si tu vois l'intitulé, tu dois voir la figure. Quand " +
"plusieurs figures comparatives se succèdent sous des intitulés voisins pour illustrer des cas " +
"distincts, décris ce qui distingue chaque cas : c'est précisément là que se trouve " +
"l'enseignement. Décris uniquement ce que la figure montre, sans l'interpréter ni compléter ce " +
"qu'elle ne montre pas — l'interdiction d'inventer prime toujours : une courbe qui présente un " +
"saut se décrit comme telle, sans en déduire une propriété mathématique absente du support. " +
"Place cette description juste après l'intitulé qu'elle complète, sous la forme fixe et " +
"reconnaissable `**Schéma visible :**` suivie d'une description courte, jamais sous une autre " +
"formulation, afin qu'elle reste toujours identifiable comme une description que tu as produite " +
"et non comme le texte du professeur. Cette exigence ne porte que sur les figures qui apportent " +
"une information pédagogique : elle ne couvre pas les éléments purement décoratifs (logo, filet " +
"de séparation, photo d'illustration, élément de charte graphique), qu'il est inutile de " +
"décrire.";

function jsonResponse(payload: unknown, status = 200): Response {
return new Response(JSON.stringify(payload), { status, headers: jsonHeaders });
Expand Down
Loading