@imenam/mcp-design 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -123,6 +123,7 @@ export declare const PATH_SEPARATOR = " \u203A ";
123
123
  * La GUI l'affiche en aperçu avant l'enregistrement : c'est la même fonction,
124
124
  * donc le même texte. Chaque remarque porte son identifiant, que l'agent passe
125
125
  * à `mark_feedback_processed`. Les captures sont numérotées dans l'ordre des
126
- * remarques ; `get_feedback` joint les images dans cet ordre.
126
+ * remarques ; `get_feedback` joint les images dans cet ordre. `coordinatesSource`
127
+ * nomme ce qui, chez le lecteur, exprime les coordonnées dans le même repère.
127
128
  */
128
- export declare function formatFeedbackRound(round: Pick<FeedbackRound, "number" | "title" | "submittedAt" | "remarks">): string;
129
+ export declare function formatFeedbackRound(round: Pick<FeedbackRound, "number" | "title" | "submittedAt" | "remarks">, coordinatesSource?: string): string;
@@ -118,9 +118,10 @@ function indent(text, prefix) {
118
118
  * La GUI l'affiche en aperçu avant l'enregistrement : c'est la même fonction,
119
119
  * donc le même texte. Chaque remarque porte son identifiant, que l'agent passe
120
120
  * à `mark_feedback_processed`. Les captures sont numérotées dans l'ordre des
121
- * remarques ; `get_feedback` joint les images dans cet ordre.
121
+ * remarques ; `get_feedback` joint les images dans cet ordre. `coordinatesSource`
122
+ * nomme ce qui, chez le lecteur, exprime les coordonnées dans le même repère.
122
123
  */
123
- export function formatFeedbackRound(round) {
124
+ export function formatFeedbackRound(round, coordinatesSource = "get_design") {
124
125
  const pages = new Map();
125
126
  for (const remark of round.remarks) {
126
127
  const list = pages.get(remark.designId);
@@ -133,7 +134,7 @@ export function formatFeedbackRound(round) {
133
134
  const lines = [
134
135
  `Feedback round ${round.number} "${round.title}" — submitted ${round.submittedAt} — ` +
135
136
  `${round.remarks.length} remark(s) on ${pages.size} page(s), ${progress.done} processed.`,
136
- "Coordinates are design pixels in the artboard's space, as in get_design.",
137
+ `Coordinates are design pixels in the artboard's space, as in ${coordinatesSource}.`,
137
138
  ];
138
139
  let image = 0;
139
140
  for (const [designId, remarks] of pages) {
@@ -0,0 +1,54 @@
1
+ import { type Design, type DesignSummary } from "../../shared/schemas/design.schema.js";
2
+ /**
3
+ * Règles d'un design indépendantes de son stockage : création, copie,
4
+ * mutation validée, synchronisation. Le `DesignStore` du serveur les applique à
5
+ * des fichiers JSON, la version autonome de la GUI à son projet en mémoire.
6
+ */
7
+ export interface CreateDesignInput {
8
+ name: string;
9
+ width?: number;
10
+ height?: number;
11
+ background?: string;
12
+ /** Groupe de pages, chemin "Parent/Enfant". Null ou omis : a la racine. */
13
+ group?: string | null;
14
+ }
15
+ export interface MutateOptions {
16
+ /**
17
+ * Révision sur laquelle l'édition est basée. Quand elle est fournie et que le
18
+ * design a avancé entre-temps, l'écriture est refusée : sans cette garde, un
19
+ * éditeur parti d'un état périmé réécrit le document entier et efface le
20
+ * travail de l'autre.
21
+ */
22
+ baseRevision?: number;
23
+ }
24
+ /** Identifiant lisible dérivé du nom, suffixé seulement en cas de collision. */
25
+ export declare function allocateDesignId(name: string, exists: (id: string) => boolean): string;
26
+ /**
27
+ * Lit un design brut, après mise à niveau de son format. `migrated` indique
28
+ * qu'il diffère de ce qui a été lu et mérite d'être réécrit.
29
+ */
30
+ export declare function parseDesign(raw: unknown, label: string): {
31
+ design: Design;
32
+ migrated: boolean;
33
+ };
34
+ export declare function newDesign(id: string, input: CreateDesignInput, now: string): Design;
35
+ /**
36
+ * Nouveau design reprenant la planche et tous les éléments d'un design
37
+ * existant, identifiants compris. La copie part synchronisée : sa référence
38
+ * de comparaison est son propre contenu.
39
+ */
40
+ export declare function duplicatedDesign(source: Design, id: string, input: {
41
+ name?: string;
42
+ group?: string | null;
43
+ }, now: string): Design;
44
+ /**
45
+ * Applique une mutation à une copie du design et incrémente la révision.
46
+ * `editor` distingue une modification de l'agent d'une modification faite à la
47
+ * main dans la GUI — c'est ce qui permet à get_changes de rester lisible.
48
+ */
49
+ export declare function mutatedDesign(current: Design, editor: "agent" | "user", fn: (design: Design) => void, options?: MutateOptions): Design;
50
+ /** Le design avec son état courant comme nouvelle référence de comparaison. */
51
+ export declare function syncedDesign(current: Design, now: string): Design;
52
+ export declare function summaryOf(design: Design): DesignSummary;
53
+ /** Plus récemment modifiés d'abord, comme la liste des pages de la GUI. */
54
+ export declare function sortSummaries(summaries: DesignSummary[]): DesignSummary[];
@@ -0,0 +1,129 @@
1
+ import { DesignSchema, normalizeDesignGroup, } from "../../shared/schemas/design.schema.js";
2
+ import { DesignError } from "./node-factory.js";
3
+ import { normalizeHierarchy } from "./hierarchy.js";
4
+ import { migrateDesign } from "./migrate.js";
5
+ import { shortId, slugify } from "./ids.js";
6
+ import { countChanges } from "./diff.js";
7
+ function issuesOf(error) {
8
+ return error.issues.map((issue) => `${issue.path.map(String).join(".")} ${issue.message}`).join("; ");
9
+ }
10
+ /** Identifiant lisible dérivé du nom, suffixé seulement en cas de collision. */
11
+ export function allocateDesignId(name, exists) {
12
+ const base = slugify(name);
13
+ if (!exists(base))
14
+ return base;
15
+ for (let i = 2; i < 100; i++) {
16
+ const candidate = `${base}-${i}`;
17
+ if (!exists(candidate))
18
+ return candidate;
19
+ }
20
+ return `${base}-${shortId("x", 4)}`;
21
+ }
22
+ /**
23
+ * Lit un design brut, après mise à niveau de son format. `migrated` indique
24
+ * qu'il diffère de ce qui a été lu et mérite d'être réécrit.
25
+ */
26
+ export function parseDesign(raw, label) {
27
+ const migrated = migrateDesign(raw);
28
+ const parsed = DesignSchema.safeParse(migrated ?? raw);
29
+ if (!parsed.success) {
30
+ throw new DesignError(`Design ${label} does not match the expected schema: ${issuesOf(parsed.error)}`);
31
+ }
32
+ return { design: parsed.data, migrated: migrated !== null };
33
+ }
34
+ export function newDesign(id, input, now) {
35
+ const name = input.name?.trim();
36
+ if (!name)
37
+ throw new DesignError(`"name" is required to create a design.`);
38
+ const width = input.width ?? 1440;
39
+ const height = input.height ?? 900;
40
+ if (!Number.isFinite(width) || width <= 0)
41
+ throw new DesignError(`"width" must be a positive number.`);
42
+ if (!Number.isFinite(height) || height <= 0)
43
+ throw new DesignError(`"height" must be a positive number.`);
44
+ return {
45
+ id,
46
+ name,
47
+ group: normalizeDesignGroup(input.group),
48
+ width,
49
+ height,
50
+ background: input.background ?? "#ffffff",
51
+ nodes: [],
52
+ createdAt: now,
53
+ updatedAt: now,
54
+ revision: 0,
55
+ lastEditor: "agent",
56
+ baseline: { revision: 0, at: now, nodes: [] },
57
+ };
58
+ }
59
+ /**
60
+ * Nouveau design reprenant la planche et tous les éléments d'un design
61
+ * existant, identifiants compris. La copie part synchronisée : sa référence
62
+ * de comparaison est son propre contenu.
63
+ */
64
+ export function duplicatedDesign(source, id, input, now) {
65
+ const name = input.name?.trim() || `${source.name} (copy)`;
66
+ return {
67
+ id,
68
+ name,
69
+ group: input.group === undefined ? source.group : normalizeDesignGroup(input.group),
70
+ width: source.width,
71
+ height: source.height,
72
+ background: source.background,
73
+ nodes: structuredClone(source.nodes),
74
+ createdAt: now,
75
+ updatedAt: now,
76
+ revision: 0,
77
+ lastEditor: "agent",
78
+ baseline: { revision: 0, at: now, nodes: structuredClone(source.nodes) },
79
+ };
80
+ }
81
+ /**
82
+ * Applique une mutation à une copie du design et incrémente la révision.
83
+ * `editor` distingue une modification de l'agent d'une modification faite à la
84
+ * main dans la GUI — c'est ce qui permet à get_changes de rester lisible.
85
+ */
86
+ export function mutatedDesign(current, editor, fn, options = {}) {
87
+ if (options.baseRevision !== undefined && options.baseRevision !== current.revision) {
88
+ throw new DesignError(`Design "${current.id}" has changed since it was loaded: your edit is based on revision ${options.baseRevision}, ` +
89
+ `the stored design is at revision ${current.revision}. Reload it with get_design and reapply your change — ` +
90
+ `writing now would discard the other editor's work.`);
91
+ }
92
+ const draft = structuredClone(current);
93
+ fn(draft);
94
+ // Les invariants de hiérarchie sont vérifiés ici, une seule fois : aucun
95
+ // appelant — outil MCP ou sauvegarde depuis la GUI — ne peut les contourner.
96
+ draft.nodes = normalizeHierarchy(draft.nodes);
97
+ draft.revision = current.revision + 1;
98
+ draft.updatedAt = new Date().toISOString();
99
+ draft.lastEditor = editor;
100
+ const parsed = DesignSchema.safeParse(draft);
101
+ if (!parsed.success)
102
+ throw new DesignError(`Resulting design is invalid: ${issuesOf(parsed.error)}`);
103
+ return parsed.data;
104
+ }
105
+ /** Le design avec son état courant comme nouvelle référence de comparaison. */
106
+ export function syncedDesign(current, now) {
107
+ return {
108
+ ...structuredClone(current),
109
+ baseline: { revision: current.revision, at: now, nodes: structuredClone(current.nodes) },
110
+ };
111
+ }
112
+ export function summaryOf(design) {
113
+ return {
114
+ id: design.id,
115
+ name: design.name,
116
+ group: design.group,
117
+ width: design.width,
118
+ height: design.height,
119
+ nodeCount: design.nodes.length,
120
+ updatedAt: design.updatedAt,
121
+ revision: design.revision,
122
+ lastEditor: design.lastEditor,
123
+ pendingChanges: countChanges(design.baseline.nodes, design.nodes),
124
+ };
125
+ }
126
+ /** Plus récemment modifiés d'abord, comme la liste des pages de la GUI. */
127
+ export function sortSummaries(summaries) {
128
+ return summaries.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
129
+ }
@@ -1,22 +1,7 @@
1
- import { type Design, type DesignSummary } from "../../shared/schemas/design.schema.js";
1
+ import type { Design, DesignSummary } from "../../shared/schemas/design.schema.js";
2
+ import { type CreateDesignInput, type MutateOptions } from "./design-model.js";
2
3
  import { ImageStore } from "./image-store.js";
3
- export interface CreateDesignInput {
4
- name: string;
5
- width?: number;
6
- height?: number;
7
- background?: string;
8
- /** Groupe de pages, chemin "Parent/Enfant". Null ou omis : a la racine. */
9
- group?: string | null;
10
- }
11
- export interface MutateOptions {
12
- /**
13
- * Révision sur laquelle l'édition est basée. Quand elle est fournie et que le
14
- * fichier a avancé entre-temps, l'écriture est refusée : sans cette garde, un
15
- * éditeur parti d'un état périmé réécrit le document entier et efface le
16
- * travail de l'autre.
17
- */
18
- baseRevision?: number;
19
- }
4
+ export type { CreateDesignInput, MutateOptions };
20
5
  /**
21
6
  * Persistance des designs : un fichier JSON par design dans <dataDir>/designs.
22
7
  * Pas de base de données — un design tient dans quelques dizaines de Ko et le
@@ -1,12 +1,8 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import { DesignSchema, normalizeDesignGroup, } from "../../shared/schemas/design.schema.js";
4
3
  import { DesignError } from "./node-factory.js";
5
4
  import { withFileLock } from "./file-lock.js";
6
- import { normalizeHierarchy } from "./hierarchy.js";
7
- import { migrateDesign } from "./migrate.js";
8
- import { shortId, slugify } from "./ids.js";
9
- import { countChanges } from "./diff.js";
5
+ import { allocateDesignId, duplicatedDesign, mutatedDesign, newDesign, parseDesign, sortSummaries, summaryOf, syncedDesign, } from "./design-model.js";
10
6
  import { ImageStore } from "./image-store.js";
11
7
  /**
12
8
  * Persistance des designs : un fichier JSON par design dans <dataDir>/designs.
@@ -75,15 +71,7 @@ export class DesignStore {
75
71
  }
76
72
  /** Identifiant lisible dérivé du nom, suffixé seulement en cas de collision. */
77
73
  allocateId(name) {
78
- const base = slugify(name);
79
- if (!fs.existsSync(this.filePath(base)))
80
- return base;
81
- for (let i = 2; i < 100; i++) {
82
- const candidate = `${base}-${i}`;
83
- if (!fs.existsSync(this.filePath(candidate)))
84
- return candidate;
85
- }
86
- return `${base}-${shortId("x", 4)}`;
74
+ return allocateDesignId(name, (id) => fs.existsSync(this.filePath(id)));
87
75
  }
88
76
  list() {
89
77
  const files = fs.existsSync(this.designsDir)
@@ -99,20 +87,9 @@ export class DesignStore {
99
87
  catch {
100
88
  continue; // Fichier corrompu ou hors schéma : il ne doit pas casser la liste.
101
89
  }
102
- summaries.push({
103
- id: design.id,
104
- name: design.name,
105
- group: design.group,
106
- width: design.width,
107
- height: design.height,
108
- nodeCount: design.nodes.length,
109
- updatedAt: design.updatedAt,
110
- revision: design.revision,
111
- lastEditor: design.lastEditor,
112
- pendingChanges: countChanges(design.baseline.nodes, design.nodes),
113
- });
90
+ summaries.push(summaryOf(design));
114
91
  }
115
- return summaries.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
92
+ return sortSummaries(summaries);
116
93
  }
117
94
  has(id) {
118
95
  return fs.existsSync(this.filePath(id));
@@ -138,13 +115,7 @@ export class DesignStore {
138
115
  }
139
116
  // Mise à niveau du format avant validation : un fichier écrit par une
140
117
  // version antérieure reste lisible, et n'est réécrit qu'en cas de changement.
141
- const migrated = migrateDesign(raw);
142
- const parsed = DesignSchema.safeParse(migrated ?? raw);
143
- if (!parsed.success) {
144
- throw new DesignError(`Design "${id}" does not match the expected schema: ${parsed.error.issues
145
- .map((i) => `${i.path.join(".")} ${i.message}`)
146
- .join("; ")}`);
147
- }
118
+ const { design, migrated } = parseDesign(raw, `"${id}"`);
148
119
  if (migrated) {
149
120
  // Best effort : un répertoire de données en lecture seule ne doit pas
150
121
  // empêcher de lire le design, la migration reste appliquée en mémoire.
@@ -152,42 +123,19 @@ export class DesignStore {
152
123
  // de `withLock`, et le verrou n'est pas réentrant. Le contenu écrit est
153
124
  // celui qu'on vient de lire, la mise à niveau ne perd donc rien.
154
125
  try {
155
- this.write(parsed.data);
126
+ this.write(design);
156
127
  }
157
128
  catch {
158
129
  this.cache.delete(id);
159
130
  }
160
- return parsed.data;
131
+ return design;
161
132
  }
162
133
  if (stamp)
163
- this.cache.set(id, { design: parsed.data, stamp });
164
- return parsed.data;
134
+ this.cache.set(id, { design, stamp });
135
+ return design;
165
136
  }
166
137
  create(input) {
167
- const name = input.name?.trim();
168
- if (!name)
169
- throw new DesignError(`"name" is required to create a design.`);
170
- const width = input.width ?? 1440;
171
- const height = input.height ?? 900;
172
- if (!Number.isFinite(width) || width <= 0)
173
- throw new DesignError(`"width" must be a positive number.`);
174
- if (!Number.isFinite(height) || height <= 0)
175
- throw new DesignError(`"height" must be a positive number.`);
176
- const now = new Date().toISOString();
177
- const design = {
178
- id: this.allocateId(name),
179
- name,
180
- group: normalizeDesignGroup(input.group),
181
- width,
182
- height,
183
- background: input.background ?? "#ffffff",
184
- nodes: [],
185
- createdAt: now,
186
- updatedAt: now,
187
- revision: 0,
188
- lastEditor: "agent",
189
- baseline: { revision: 0, at: now, nodes: [] },
190
- };
138
+ const design = newDesign(this.allocateId(input.name?.trim() ?? ""), input, new Date().toISOString());
191
139
  this.write(design);
192
140
  return design;
193
141
  }
@@ -199,21 +147,7 @@ export class DesignStore {
199
147
  duplicate(sourceId, input = {}) {
200
148
  const source = this.get(sourceId);
201
149
  const name = input.name?.trim() || `${source.name} (copy)`;
202
- const now = new Date().toISOString();
203
- const design = {
204
- id: this.allocateId(name),
205
- name,
206
- group: input.group === undefined ? source.group : normalizeDesignGroup(input.group),
207
- width: source.width,
208
- height: source.height,
209
- background: source.background,
210
- nodes: structuredClone(source.nodes),
211
- createdAt: now,
212
- updatedAt: now,
213
- revision: 0,
214
- lastEditor: "agent",
215
- baseline: { revision: 0, at: now, nodes: structuredClone(source.nodes) },
216
- };
150
+ const design = duplicatedDesign(source, this.allocateId(name), { ...input, name }, new Date().toISOString());
217
151
  this.write(design);
218
152
  return design;
219
153
  }
@@ -228,36 +162,14 @@ export class DesignStore {
228
162
  return this.withLock(id, () => this.mutateLocked(id, editor, fn, options));
229
163
  }
230
164
  mutateLocked(id, editor, fn, options) {
231
- const current = this.get(id);
232
- if (options.baseRevision !== undefined && options.baseRevision !== current.revision) {
233
- throw new DesignError(`Design "${id}" has changed since it was loaded: your edit is based on revision ${options.baseRevision}, ` +
234
- `the stored design is at revision ${current.revision}. Reload it with get_design and reapply your change — ` +
235
- `writing now would discard the other editor's work.`);
236
- }
237
- const draft = structuredClone(current);
238
- fn(draft);
239
- // Les invariants de hiérarchie sont vérifiés ici, une seule fois : aucun
240
- // appelant — outil MCP ou sauvegarde depuis la GUI — ne peut les contourner.
241
- draft.nodes = normalizeHierarchy(draft.nodes);
242
- draft.revision = current.revision + 1;
243
- draft.updatedAt = new Date().toISOString();
244
- draft.lastEditor = editor;
245
- const parsed = DesignSchema.safeParse(draft);
246
- if (!parsed.success) {
247
- throw new DesignError(`Resulting design is invalid: ${parsed.error.issues.map((i) => `${i.path.join(".")} ${i.message}`).join("; ")}`);
248
- }
249
- this.write(parsed.data);
250
- return parsed.data;
165
+ const next = mutatedDesign(this.get(id), editor, fn, options);
166
+ this.write(next);
167
+ return next;
251
168
  }
252
169
  /** Fige l'état courant comme nouvelle référence de comparaison. */
253
170
  markSynced(id) {
254
171
  return this.withLock(id, () => {
255
- const current = this.get(id);
256
- const now = new Date().toISOString();
257
- const next = {
258
- ...structuredClone(current),
259
- baseline: { revision: current.revision, at: now, nodes: structuredClone(current.nodes) },
260
- };
172
+ const next = syncedDesign(this.get(id), new Date().toISOString());
261
173
  this.write(next);
262
174
  return next;
263
175
  });
@@ -0,0 +1,86 @@
1
+ import { type FeedbackRound } from "../../shared/feedback.js";
2
+ import { type Design } from "../../shared/schemas/design.schema.js";
3
+ import type { FeedbackFile } from "../../shared/schemas/feedback.schema.js";
4
+ import { DesignError } from "./node-factory.js";
5
+ /**
6
+ * Échanges de la version autonome avec un assistant conversationnel :
7
+ *
8
+ * - le prompt qui lui décrit le format d'une page ;
9
+ * - la lecture stricte d'une page qu'il renvoie, refusée en bloc à la moindre
10
+ * erreur avec un message qu'on peut lui recoller tel quel ;
11
+ * - le récap d'un retour, qui lui transmet les remarques et l'état courant des
12
+ * pages concernées ;
13
+ * - l'export et la relecture du projet complet.
14
+ */
15
+ export declare const PAGE_FORMAT = "mcp-design-page";
16
+ export declare const PROJECT_FORMAT = "mcp-design-project";
17
+ export declare const EXCHANGE_VERSION = 1;
18
+ /** Pages, remarques et captures (data URI PNG par nom de fichier) d'un projet autonome. */
19
+ export interface ProjectData {
20
+ designs: Design[];
21
+ feedback: FeedbackFile;
22
+ captures: Record<string, string>;
23
+ }
24
+ /** Refus d'un fichier importé. `issues` liste chaque problème, `message` s'adresse à l'assistant. */
25
+ export declare class ExchangeError extends DesignError {
26
+ readonly issues: string[];
27
+ constructor(issues: string[]);
28
+ }
29
+ /** Une page lue et validée, prête à remplacer ou rejoindre celles du projet. */
30
+ export interface PageImport {
31
+ /** La page telle qu'elle sera enregistrée, synchronisée sur son nouveau contenu. */
32
+ design: Design;
33
+ /** Page existante que l'import remplace, ou null pour une nouvelle page. */
34
+ replaces: Design | null;
35
+ /** Remarques déclarées traitées, avec le retour qui les contient. */
36
+ processed: {
37
+ round: number;
38
+ remarkId: string;
39
+ note: string | null;
40
+ }[];
41
+ }
42
+ /**
43
+ * Lit une page renvoyée par l'assistant. Rien n'est appliqué ici : le résultat
44
+ * dit ce que l'import ferait, `applyPageImport` l'exécute.
45
+ */
46
+ export declare function parsePageReply(text: string, project: Pick<ProjectData, "designs" | "feedback">): PageImport;
47
+ /** Enregistre une page lue par `parsePageReply` et note les remarques traitées. */
48
+ export declare function applyPageImport(project: ProjectData, plan: PageImport): void;
49
+ interface PageFile {
50
+ format: string;
51
+ version: number;
52
+ page: {
53
+ id: string;
54
+ name: string;
55
+ group: string | null;
56
+ width: number;
57
+ height: number;
58
+ background: string;
59
+ nodes: object[];
60
+ };
61
+ processed: {
62
+ remark: string;
63
+ note?: string;
64
+ }[];
65
+ }
66
+ /** La page au format d'échange. La taille des groupes et des instances, dérivée, n'y figure pas. */
67
+ export declare function pageFileOf(design: Design): PageFile;
68
+ /** JSON d'une page, un élément par ligne : lisible et bien plus court qu'une indentation complète. */
69
+ export declare function pageJson(file: PageFile): string;
70
+ export declare function projectFileOf(project: ProjectData, exportedAt: string): object;
71
+ /** Relit un export complet ; refusé en bloc à la moindre incohérence. */
72
+ export declare function parseProjectFile(text: string): ProjectData;
73
+ /** Ce que l'utilisateur colle en tête de conversation, suivi de sa demande. */
74
+ export declare function creationPrompt(): string;
75
+ /** Ce qui, pour l'assistant, exprime les coordonnées des remarques. */
76
+ export declare const COORDINATES_SOURCE = "the page JSON";
77
+ /** Fichiers joints au récap : une vue de chaque page, et une image par capture. */
78
+ export interface RecapFiles {
79
+ /** Vue d'ensemble de chaque page concernée, par id de page. */
80
+ pages: Map<string, string>;
81
+ /** Fichier de chaque capture, dans l'ordre où le texte du retour les numérote. */
82
+ captures: string[];
83
+ }
84
+ /** Texte du récap d'un retour, à transmettre à l'assistant avec les fichiers joints. */
85
+ export declare function recapText(round: FeedbackRound, designs: Design[], files: RecapFiles): string;
86
+ export {};