@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.
- package/README.md +454 -403
- package/dist/shared/feedback.d.ts +3 -2
- package/dist/shared/feedback.js +4 -3
- package/dist/src/core/design-model.d.ts +54 -0
- package/dist/src/core/design-model.js +129 -0
- package/dist/src/core/design-store.d.ts +3 -18
- package/dist/src/core/design-store.js +15 -103
- package/dist/src/core/exchange.d.ts +86 -0
- package/dist/src/core/exchange.js +430 -0
- package/dist/src/core/feedback-model.d.ts +64 -0
- package/dist/src/core/feedback-model.js +211 -0
- package/dist/src/core/feedback-store.d.ts +4 -31
- package/dist/src/core/feedback-store.js +14 -195
- package/dist/src/core/ids.js +1 -2
- package/dist/src/gui-worker.js +42 -53
- package/dist/src/index.js +0 -7
- package/gui/dist/assets/index-DTIGOSpK.js +15 -0
- package/gui/dist/assets/{search-DHQx5ZT0.js → search-CH3uadrp.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +3 -2
- package/gui/dist/assets/index-SUmtOh8T.js +0 -15
|
@@ -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"
|
|
129
|
+
export declare function formatFeedbackRound(round: Pick<FeedbackRound, "number" | "title" | "submittedAt" | "remarks">, coordinatesSource?: string): string;
|
package/dist/shared/feedback.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
126
|
+
this.write(design);
|
|
156
127
|
}
|
|
157
128
|
catch {
|
|
158
129
|
this.cache.delete(id);
|
|
159
130
|
}
|
|
160
|
-
return
|
|
131
|
+
return design;
|
|
161
132
|
}
|
|
162
133
|
if (stamp)
|
|
163
|
-
this.cache.set(id, { design
|
|
164
|
-
return
|
|
134
|
+
this.cache.set(id, { design, stamp });
|
|
135
|
+
return design;
|
|
165
136
|
}
|
|
166
137
|
create(input) {
|
|
167
|
-
const
|
|
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
|
|
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
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
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 {};
|