@ogygie/generate 0.0.0-stage → 0.19.0

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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -2
  3. package/dist/behavior-binding.d.ts +47 -0
  4. package/dist/behavior-grammar.d.ts +53 -0
  5. package/dist/check-spec.d.ts +53 -0
  6. package/dist/chunk-WVFHAPGC.js +10992 -0
  7. package/dist/cli/check-command.d.ts +3 -0
  8. package/dist/cli/fill-command.d.ts +39 -0
  9. package/dist/cli/generate-command.d.ts +8 -0
  10. package/dist/cli/generate-screen-command.d.ts +6 -0
  11. package/dist/cli/output.d.ts +9 -0
  12. package/dist/cli/preview-command.d.ts +19 -0
  13. package/dist/cli/project.d.ts +97 -0
  14. package/dist/cli/run.d.ts +13 -0
  15. package/dist/cli/run.js +1127 -0
  16. package/dist/cli/schemas-command.d.ts +31 -0
  17. package/dist/cli/verify-command.d.ts +3 -0
  18. package/dist/consumed-feature.d.ts +32 -0
  19. package/dist/emit/db.d.ts +27 -0
  20. package/dist/emit/factories.d.ts +14 -0
  21. package/dist/emit/hole-doc.d.ts +18 -0
  22. package/dist/emit/hole-tests.d.ts +28 -0
  23. package/dist/emit/manifest-entry.d.ts +44 -0
  24. package/dist/emit/mssql/migration.d.ts +2 -0
  25. package/dist/emit/mssql/repository.d.ts +17 -0
  26. package/dist/emit/mssql/sql-types.d.ts +23 -0
  27. package/dist/emit/mssql/statements.d.ts +55 -0
  28. package/dist/emit/mssql/stub.d.ts +17 -0
  29. package/dist/emit/names.d.ts +34 -0
  30. package/dist/emit/plan.d.ts +428 -0
  31. package/dist/emit/repository.d.ts +16 -0
  32. package/dist/emit/routes.d.ts +22 -0
  33. package/dist/emit/runtime.d.ts +47 -0
  34. package/dist/emit/screen/container.d.ts +17 -0
  35. package/dist/emit/screen/fixtures.d.ts +14 -0
  36. package/dist/emit/screen/hole-doc.d.ts +2 -0
  37. package/dist/emit/screen/previews.d.ts +24 -0
  38. package/dist/emit/screen/queries.d.ts +10 -0
  39. package/dist/emit/screen/shared.d.ts +35 -0
  40. package/dist/emit/screen/spec-contracts.d.ts +10 -0
  41. package/dist/emit/screen/tests-static.d.ts +21 -0
  42. package/dist/emit/screen/tests.d.ts +19 -0
  43. package/dist/emit/screen/views.d.ts +10 -0
  44. package/dist/emit/screen/writes.d.ts +9 -0
  45. package/dist/emit/screen-plan.d.ts +187 -0
  46. package/dist/emit/service.d.ts +18 -0
  47. package/dist/emit/spec-contracts.d.ts +27 -0
  48. package/dist/emit/tests.d.ts +3 -0
  49. package/dist/fill/body.d.ts +26 -0
  50. package/dist/fill/fill.d.ts +97 -0
  51. package/dist/fill/model-draw.d.ts +25 -0
  52. package/dist/generate-feature.d.ts +23 -0
  53. package/dist/generate-screen.d.ts +19 -0
  54. package/dist/generation-block.schema.d.ts +1279 -0
  55. package/dist/hole-shape.d.ts +100 -0
  56. package/dist/index.d.ts +45 -0
  57. package/dist/index.js +401 -0
  58. package/dist/manifest-generation.schema.d.ts +239 -0
  59. package/dist/motif-catalog.d.ts +175 -0
  60. package/dist/package-root.d.ts +5 -0
  61. package/dist/prompt-card.d.ts +3 -0
  62. package/dist/schema-checks.d.ts +32 -0
  63. package/dist/schemas/draft.d.ts +111 -0
  64. package/dist/screen-block.schema.d.ts +146 -0
  65. package/dist/screen-checks.d.ts +34 -0
  66. package/dist/screen-grammar.d.ts +36 -0
  67. package/dist/screen-vocabulary.d.ts +47 -0
  68. package/dist/spec-sections.d.ts +51 -0
  69. package/dist/verify-generation.d.ts +93 -0
  70. package/dist/vocabulary.d.ts +39 -0
  71. package/dist/zod-introspection.d.ts +119 -0
  72. package/holes-library/27560e1760bb184c.json +28 -0
  73. package/holes-library/2c26136fc6bff99a.json +30 -0
  74. package/holes-library/43a27770407bb38a.json +27 -0
  75. package/holes-library/46e67880a81012bc.json +27 -0
  76. package/holes-library/48dc5c44dc2cf3b7.json +32 -0
  77. package/holes-library/63d0825a61f6f5d3.json +27 -0
  78. package/holes-library/6784ed6c8e353dac.json +27 -0
  79. package/holes-library/8b4e4c8f04a5fc6a.json +31 -0
  80. package/holes-library/README.md +33 -0
  81. package/holes-library/d3597f50c265ba0c.json +27 -0
  82. package/holes-library/e48695e16221ff1a.json +32 -0
  83. package/package.json +59 -3
@@ -0,0 +1,1127 @@
1
+ import {
2
+ DEFAULT_FILL_MODELS,
3
+ EMPTY_GUARD_REGISTRY,
4
+ FeatureNameSchema,
5
+ GENERATOR_VERSION,
6
+ MANIFEST_FILE,
7
+ PREVIEWS_FILE_SUFFIX,
8
+ acceptExamplesOf,
9
+ applyGeneratedContracts,
10
+ applyManifestEntry,
11
+ applySpecContracts,
12
+ boundBehaviorsOf,
13
+ boundScreenBehaviorsOf,
14
+ buildFeaturePlan,
15
+ buildScreenPlan,
16
+ checkSpec,
17
+ contractViolationCodeOf,
18
+ createMessagesClient,
19
+ emitScreenSpecContracts,
20
+ entryStatusForSpec,
21
+ featureTargetOf,
22
+ formatProblem,
23
+ generateFeature,
24
+ generateScreen,
25
+ importProductModule,
26
+ intentForManifest,
27
+ intentOfSpec,
28
+ loadConsumedFeatures,
29
+ loadGenerateProject,
30
+ loadHolesLibrary,
31
+ migrationFileNameFor,
32
+ modelsOfModule,
33
+ ogygieError,
34
+ pascalCaseOf,
35
+ runFillCommand,
36
+ schemaModelsLoader,
37
+ screenBodyDefault,
38
+ screenBodyPath,
39
+ screenTargetOf,
40
+ scrubbedEnvironment,
41
+ serializeManifest,
42
+ tableNameOf,
43
+ targetKindOf,
44
+ throwOgygieError,
45
+ verifyGeneration
46
+ } from "../chunk-WVFHAPGC.js";
47
+
48
+ // src/cli/check-command.ts
49
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
50
+ import { join } from "node:path";
51
+ var SPEC_FILE_NAME = /^(?!_)[^/]+\.spec\.md$/u;
52
+ async function checkOne(project, specPath, out) {
53
+ const markdown = readFileSync(join(project.root, specPath), "utf8");
54
+ const guards = project.generation?.guards ?? EMPTY_GUARD_REGISTRY;
55
+ const first = checkSpec({ markdown, specPath, guards, generation: project.generation });
56
+ if (first.kind === "screen" && first.screen !== null) {
57
+ const consumed = await loadConsumedFeatures(project, first.screen);
58
+ return checkSpec({ markdown, specPath, guards, generation: project.generation, consumed });
59
+ }
60
+ const owner = project.owners.find((candidate) => candidate.specPath === specPath);
61
+ if (first.block === null || owner === void 0) return first;
62
+ const modulePath = `${owner.folder}/${first.block.contracts.module.replace(/^\.\//u, "")}`;
63
+ const schemas = await schemaModelsLoader(project.root)(modulePath);
64
+ if (schemas === null) {
65
+ out.error(`ogygie generate check \u2014 ${modulePath} n'existe pas : le bloc est v\xE9rifi\xE9 sans son module (D19 : schemas.ts est le premier geste humain).`);
66
+ return first;
67
+ }
68
+ return checkSpec({ markdown, specPath, guards, generation: project.generation, schemas });
69
+ }
70
+ function printReport(specPath, report, out) {
71
+ for (const problem of report.problems) out.error(formatProblem(problem));
72
+ const { derivable, hole, unbound } = report.tally;
73
+ const verdict = report.ok ? "OK" : `${report.problems.length} probl\xE8me(s)`;
74
+ out.log(
75
+ report.generated ? ` ${specPath} \u2014 ${verdict} ; ${derivable} ligne(s) li\xE9e(s), ${hole} port\xE9e(s) par un trou, ${unbound} hors vocabulaire` : ` ${specPath} \u2014 sans section # G\xE9n\xE9ration, rien \xE0 v\xE9rifier`
76
+ );
77
+ }
78
+ async function runCheckCommand(project, featureId, out) {
79
+ const specPaths = [];
80
+ if (featureId !== void 0) {
81
+ const target = targetKindOf(project, featureId) === "screen" ? screenTargetOf(project, featureId) : featureTargetOf(project, featureId);
82
+ if (!target.ok) {
83
+ out.error(formatProblem(target.problem));
84
+ return 1;
85
+ }
86
+ specPaths.push(target.target.specPath);
87
+ } else {
88
+ const folder = project.specsFolder.replace(/\/+$/u, "");
89
+ const absolute = join(project.root, folder);
90
+ const names = existsSync(absolute) ? readdirSync(absolute).filter((name) => SPEC_FILE_NAME.test(name)).sort() : [];
91
+ specPaths.push(...names.map((name) => `${folder}/${name}`));
92
+ }
93
+ let problems = 0;
94
+ let generated = 0;
95
+ for (const specPath of specPaths) {
96
+ const report = await checkOne(project, specPath, out);
97
+ printReport(specPath, report, out);
98
+ problems += report.problems.length;
99
+ if (report.generated) generated += 1;
100
+ }
101
+ if (problems > 0) {
102
+ out.error(`ogygie generate check \u2014 ${problems} probl\xE8me(s) sur ${specPaths.length} spec(s).`);
103
+ return 1;
104
+ }
105
+ out.log(`ogygie generate check \u2014 OK, ${specPaths.length} spec(s) lue(s) dont ${generated} avec une section # G\xE9n\xE9ration.`);
106
+ return 0;
107
+ }
108
+
109
+ // src/cli/generate-command.ts
110
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
111
+ import { dirname as dirname2, join as join3 } from "node:path";
112
+ import { performance as performance2 } from "node:perf_hooks";
113
+
114
+ // ../ogygie-core/src/spec.schema.ts
115
+ import { z } from "zod";
116
+ var DeferredBehaviorSchema = z.string().regex(
117
+ /^.+ blockedBy:[a-z][a-z0-9]*(-[a-z0-9]+)*$/,
118
+ 'Expected "<behavior> blockedBy:<feature-name>" (feature name in kebab-case)'
119
+ );
120
+ var SpecFrontmatterSchema = z.object({
121
+ id: FeatureNameSchema.describe("Identical to the feature name in the manifest"),
122
+ status: z.enum(["draft", "validated", "implemented"]),
123
+ depends_on: z.array(FeatureNameSchema),
124
+ priority: z.enum(["P0", "P1", "P2"]),
125
+ refs: z.array(z.string()).default([]).describe("References to the domain model, e.g. 'modele-gally \xA74.2'"),
126
+ deferred: z.array(DeferredBehaviorSchema).default([]).describe("Behaviors deferred to a later feature, blocked by name")
127
+ });
128
+ function splitInlineArrayElements(inner) {
129
+ const elements = [];
130
+ let current = "";
131
+ let openQuote = null;
132
+ for (const character of inner) {
133
+ if (openQuote === null && (character === '"' || character === "'")) {
134
+ openQuote = character;
135
+ current += character;
136
+ } else if (character === openQuote) {
137
+ openQuote = null;
138
+ current += character;
139
+ } else if (openQuote === null && character === ",") {
140
+ elements.push(current);
141
+ current = "";
142
+ } else {
143
+ current += character;
144
+ }
145
+ }
146
+ elements.push(current);
147
+ return { elements, unbalancedQuote: openQuote !== null };
148
+ }
149
+ function unquote(raw, location, problems) {
150
+ const trimmed = raw.trim();
151
+ const first = trimmed[0];
152
+ if (first === '"' || first === "'") {
153
+ if (trimmed.length < 2 || !trimmed.endsWith(first)) {
154
+ problems.push(`${location}: unbalanced quotes in ${JSON.stringify(trimmed)}`);
155
+ return null;
156
+ }
157
+ return trimmed.slice(1, -1);
158
+ }
159
+ return trimmed;
160
+ }
161
+ function parseSpecFrontmatter(text) {
162
+ const value = {};
163
+ const problems = [];
164
+ const lines = text.split("\n");
165
+ for (let index = 0; index < lines.length; index += 1) {
166
+ const trimmed = (lines[index] ?? "").trim();
167
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
168
+ const colonIndex = trimmed.indexOf(":");
169
+ if (colonIndex === -1 || trimmed.startsWith('"') || trimmed.startsWith("'")) {
170
+ problems.push(
171
+ `unparseable line ${JSON.stringify(trimmed)}: expected \`key: value\` \u2014 a spilled array element outside any array? (scalars and arrays of scalars only)`
172
+ );
173
+ continue;
174
+ }
175
+ const key = trimmed.slice(0, colonIndex).trim();
176
+ let rawValue = trimmed.slice(colonIndex + 1).trim();
177
+ if (rawValue.startsWith("[")) {
178
+ let closed = rawValue.endsWith("]");
179
+ while (!closed && index + 1 < lines.length) {
180
+ index += 1;
181
+ rawValue = `${rawValue} ${(lines[index] ?? "").trim()}`;
182
+ closed = rawValue.endsWith("]");
183
+ }
184
+ if (!closed) {
185
+ problems.push(
186
+ `key "${key}": array opened with "[" and never closed with "]" before the end of the frontmatter \u2014 a truncated array would be silently read as empty, hence refused`
187
+ );
188
+ continue;
189
+ }
190
+ const { elements, unbalancedQuote } = splitInlineArrayElements(
191
+ rawValue.slice(1, -1)
192
+ );
193
+ if (unbalancedQuote) {
194
+ problems.push(`key "${key}": unbalanced quotes inside the array`);
195
+ continue;
196
+ }
197
+ const parsedElements = [];
198
+ let broken = false;
199
+ for (const element of elements) {
200
+ if (element.trim() === "") continue;
201
+ const unquoted = unquote(element, `key "${key}"`, problems);
202
+ if (unquoted === null) broken = true;
203
+ else parsedElements.push(unquoted);
204
+ }
205
+ if (!broken) value[key] = parsedElements;
206
+ } else {
207
+ const unquoted = unquote(rawValue, `key "${key}"`, problems);
208
+ if (unquoted !== null) value[key] = unquoted;
209
+ }
210
+ }
211
+ return { value, problems };
212
+ }
213
+ var FRONTMATTER_BLOCK = /^---\n([\s\S]*?)\n---/;
214
+ function parseSpecDocument(content) {
215
+ const match = FRONTMATTER_BLOCK.exec(content);
216
+ if (match === null || match[1] === void 0) {
217
+ return {
218
+ frontmatter: null,
219
+ problems: ["file does not start with a --- ... --- frontmatter block"],
220
+ body: content
221
+ };
222
+ }
223
+ const body = content.slice(match[0].length);
224
+ const parsed = parseSpecFrontmatter(match[1]);
225
+ if (parsed.problems.length > 0) {
226
+ return { frontmatter: null, problems: parsed.problems, body };
227
+ }
228
+ const checked = SpecFrontmatterSchema.safeParse(parsed.value);
229
+ if (!checked.success) {
230
+ return {
231
+ frontmatter: null,
232
+ problems: checked.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`),
233
+ body
234
+ };
235
+ }
236
+ return { frontmatter: checked.data, problems: [], body };
237
+ }
238
+ var DESIGN_SECTION_TITLE = "Design";
239
+ var COMPOSITION_LINE = /^\s*(?:[-*]\s*)?[Cc]omposition\s*:\s*`?([a-z][a-z0-9]*(?:-[a-z0-9]+)*)`?(?![A-Za-z0-9])/mu;
240
+ function compositionOfSpec(content) {
241
+ const heading = new RegExp(`^# ${DESIGN_SECTION_TITLE}\\s*$`, "mu").exec(content);
242
+ if (heading === null) return null;
243
+ const rest = content.slice(heading.index + heading[0].length);
244
+ const next = /^# /mu.exec(rest);
245
+ const section = next === null ? rest : rest.slice(0, next.index);
246
+ return COMPOSITION_LINE.exec(section)?.[1] ?? null;
247
+ }
248
+
249
+ // src/cli/generate-screen-command.ts
250
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
251
+ import { dirname, join as join2 } from "node:path";
252
+ import { performance } from "node:perf_hooks";
253
+ function consumedInputsOf(loaded) {
254
+ const inputs = [];
255
+ for (const feature of loaded.values()) {
256
+ if (feature.schemas === null) continue;
257
+ inputs.push({ featureId: feature.featureId, block: feature.block, schemas: feature.schemas, routes: feature.routes });
258
+ }
259
+ return inputs;
260
+ }
261
+ async function runGenerateScreenCommand(project, featureId, out) {
262
+ const lookup = screenTargetOf(project, featureId);
263
+ if (!lookup.ok) {
264
+ out.error(formatProblem(lookup.problem));
265
+ return 1;
266
+ }
267
+ const { target } = lookup;
268
+ const markdown = readFileSync2(join2(project.root, target.specPath), "utf8");
269
+ const first = checkSpec({ markdown, specPath: target.specPath, generation: target.generation });
270
+ if (first.kind !== "screen" || first.screen === null) {
271
+ for (const problem of first.problems) out.error(formatProblem(problem));
272
+ out.error(`ogygie generate \u2014 ${target.specPath} n'a pas de bloc # G\xE9n\xE9ration d'\xE9cran lisible (consumes, views, holes).`);
273
+ return 1;
274
+ }
275
+ const loaded = await loadConsumedFeatures(project, first.screen);
276
+ const report = checkSpec({ markdown, specPath: target.specPath, generation: target.generation, consumed: loaded });
277
+ if (!report.ok || report.screen === null) {
278
+ for (const problem of report.problems) out.error(formatProblem(problem));
279
+ out.error(`ogygie generate \u2014 ${report.problems.length} probl\xE8me(s), rien n'est \xE9crit.`);
280
+ return 1;
281
+ }
282
+ const started = performance.now();
283
+ const plan = buildScreenPlan({
284
+ featureId,
285
+ folder: target.folder,
286
+ appPath: target.appPath,
287
+ specPath: target.specPath,
288
+ block: report.screen,
289
+ consumed: consumedInputsOf(loaded),
290
+ front: target.front,
291
+ guards: target.generation.guards,
292
+ contractViolationCode: contractViolationCodeOf(target.generation.target),
293
+ generatorVersion: GENERATOR_VERSION,
294
+ behaviors: boundScreenBehaviorsOf(report.behaviors)
295
+ });
296
+ const existingCustomViews = /* @__PURE__ */ new Map();
297
+ for (const view of plan.views) {
298
+ if (view.kind !== "custom") continue;
299
+ const path = join2(project.root, plan.folder, "views", `${view.componentName}.tsx`);
300
+ existingCustomViews.set(view.id, existsSync2(path) ? readFileSync2(path, "utf8") : null);
301
+ }
302
+ const files = generateScreen(plan, { existingCustomViews });
303
+ const specContracts = applyGeneratedContracts(markdown, emitScreenSpecContracts(plan), GENERATOR_VERSION);
304
+ const elapsedMs = Math.round(performance.now() - started);
305
+ let written = 0;
306
+ let emitted = 0;
307
+ for (const file of files) {
308
+ const absolute = join2(project.root, file.path);
309
+ const exists = existsSync2(absolute);
310
+ emitted += file.content.split("\n").length - 1;
311
+ if (exists && readFileSync2(absolute, "utf8") === file.content) {
312
+ out.log(` identique ${file.path}`);
313
+ continue;
314
+ }
315
+ mkdirSync(dirname(absolute), { recursive: true });
316
+ writeFileSync(absolute, file.content, "utf8");
317
+ written += 1;
318
+ out.log(` ${exists ? "r\xE9\xE9crit " : "\xE9crit "} ${file.path}`);
319
+ }
320
+ const bodyPath = screenBodyPath(plan);
321
+ const bodyAbsolute = join2(project.root, bodyPath);
322
+ if (existsSync2(bodyAbsolute)) {
323
+ out.log(` conserv\xE9 ${bodyPath} (corps \xE9crit, jamais compar\xE9 \u2014 D40)`);
324
+ } else {
325
+ mkdirSync(dirname(bodyAbsolute), { recursive: true });
326
+ writeFileSync(bodyAbsolute, screenBodyDefault(plan), "utf8");
327
+ written += 1;
328
+ out.log(` \xE9crit ${bodyPath} (corps par d\xE9faut, une fois : le v\xF4tre d\xE9sormais \u2014 D40)`);
329
+ }
330
+ if (specContracts.outcome === "section-missing") {
331
+ out.error(`ogygie generate \u2014 ${target.specPath} n'a pas de section # Contrats de donn\xE9es, le bloc g\xE9n\xE9r\xE9 n'a nulle part o\xF9 aller (section obligatoire, verify-specs).`);
332
+ return 1;
333
+ }
334
+ if (specContracts.outcome === "unchanged") {
335
+ out.log(` identique ${target.specPath} \u203A bloc @generated de # Contrats de donn\xE9es`);
336
+ } else {
337
+ writeFileSync(join2(project.root, target.specPath), specContracts.markdown, "utf8");
338
+ out.log(` ${specContracts.outcome === "inserted" ? "ins\xE9r\xE9 " : "r\xE9\xE9crit "} ${target.specPath} \u203A bloc @generated de # Contrats de donn\xE9es`);
339
+ }
340
+ out.log(
341
+ `ogygie generate \u2014 ${featureId} (\xE9cran) : ${files.length} fichier(s) \xE9mis en ${elapsedMs} ms (${emitted} lignes \xE9mises, bloc de la spec compris), ${written} \xE9crit(s), ${report.tally.derivable} ligne(s) li\xE9e(s), ${report.tally.hole} port\xE9e(s) par un trou ; ${plan.views.filter((view) => view.kind === "custom").length} vue(s) custom sur ${plan.views.length}.`
342
+ );
343
+ return 0;
344
+ }
345
+
346
+ // src/cli/generate-command.ts
347
+ function generatedFeatureIdsOf(project) {
348
+ const kinds = [];
349
+ for (const feature of project.manifest.features) {
350
+ if (feature.spec === null) continue;
351
+ const kind = targetKindOf(project, feature.name);
352
+ if (kind === null) continue;
353
+ const specPath = join3(project.root, feature.spec);
354
+ if (!existsSync3(specPath)) continue;
355
+ const first = checkSpec({ markdown: readFileSync3(specPath, "utf8"), specPath: feature.spec, generation: project.generation });
356
+ if (kind === "feature" ? first.block !== null : first.screen !== null) kinds.push({ id: feature.name, kind });
357
+ }
358
+ return [...kinds.filter((entry) => entry.kind === "feature"), ...kinds.filter((entry) => entry.kind === "screen")].map((entry) => entry.id);
359
+ }
360
+ async function runGenerateAllCommand(project, out) {
361
+ const ids = generatedFeatureIdsOf(project);
362
+ if (ids.length === 0) {
363
+ out.log("ogygie generate --all \u2014 aucune spec de la carte ne porte de bloc # G\xE9n\xE9ration, rien \xE0 r\xE9g\xE9n\xE9rer.");
364
+ return 0;
365
+ }
366
+ const refused = [];
367
+ let current = project;
368
+ for (const [index, id] of ids.entries()) {
369
+ if (index > 0) {
370
+ const reloaded = loadGenerateProject(project.root);
371
+ if (!reloaded.ok) {
372
+ for (const problem of reloaded.problems) out.error(formatProblem(problem));
373
+ out.error(`ogygie generate --all \u2014 le projet ne se relit plus apr\xE8s ${ids[index - 1]}, arr\xEAt.`);
374
+ return 1;
375
+ }
376
+ current = reloaded.project;
377
+ }
378
+ const code = await runGenerateFeatureCommand(current, id, out);
379
+ if (code !== 0) refused.push(id);
380
+ }
381
+ out.log(`ogygie generate --all \u2014 ${ids.length} spec(s) g\xE9n\xE9r\xE9e(s) de la carte : ${ids.length - refused.length} r\xE9g\xE9n\xE9r\xE9e(s)${refused.length === 0 ? "" : `, ${refused.length} refus\xE9e(s) (${refused.join(", ")})`}.`);
382
+ return refused.length === 0 ? 0 : 1;
383
+ }
384
+ async function runGenerateFeatureCommand(project, featureId, out) {
385
+ if (featureId === void 0 || featureId === "") {
386
+ out.error("usage: ogygie generate generate <feature-id> | --all");
387
+ return 1;
388
+ }
389
+ if (targetKindOf(project, featureId) === "screen") return runGenerateScreenCommand(project, featureId, out);
390
+ const lookup = featureTargetOf(project, featureId);
391
+ if (!lookup.ok) {
392
+ out.error(formatProblem(lookup.problem));
393
+ return 1;
394
+ }
395
+ const { target } = lookup;
396
+ const markdown = readFileSync3(join3(project.root, target.specPath), "utf8");
397
+ const firstPass = checkSpec({ markdown, specPath: target.specPath, generation: target.generation });
398
+ if (firstPass.block === null) {
399
+ for (const problem of firstPass.problems) out.error(formatProblem(problem));
400
+ out.error(`ogygie generate \u2014 ${target.specPath} n'a pas de bloc # G\xE9n\xE9ration lisible.`);
401
+ return 1;
402
+ }
403
+ const modulePath = `${target.folder}/${firstPass.block.contracts.module.replace(/^\.\//u, "")}`;
404
+ const schemas = await schemaModelsLoader(project.root)(modulePath);
405
+ if (schemas === null) {
406
+ out.error(`ogygie generate \u2014 ${modulePath} n'existe pas (D19 : schemas.ts est le premier geste humain).`);
407
+ return 1;
408
+ }
409
+ const report = checkSpec({ markdown, specPath: target.specPath, generation: target.generation, schemas });
410
+ if (!report.ok || report.block === null) {
411
+ for (const problem of report.problems) out.error(formatProblem(problem));
412
+ out.error(`ogygie generate \u2014 ${report.problems.length} probl\xE8me(s), rien n'est \xE9crit.`);
413
+ return 1;
414
+ }
415
+ const started = performance2.now();
416
+ const migrationsDir = join3(project.root, target.appPath, target.generation.target.migrations);
417
+ const existingMigrations = existsSync3(migrationsDir) ? readdirSync2(migrationsDir) : [];
418
+ const plan = buildFeaturePlan({
419
+ featureId,
420
+ folder: target.folder,
421
+ appPath: target.appPath,
422
+ specPath: target.specPath,
423
+ block: report.block,
424
+ schemas,
425
+ generation: target.generation,
426
+ ...target.tenantColumn === void 0 ? {} : { tenantColumn: target.tenantColumn },
427
+ migrationFileName: migrationFileNameFor(existingMigrations, report.block.contracts.table === void 0 ? featureId : tableNameOf(report.block.contracts.table)),
428
+ generatorVersion: GENERATOR_VERSION,
429
+ behaviors: boundBehaviorsOf(report.behaviors)
430
+ });
431
+ const logicPath = join3(project.root, target.folder, "logic.ts");
432
+ const existingLogic = existsSync3(logicPath) ? readFileSync3(logicPath, "utf8") : null;
433
+ const migrationPath = join3(project.root, plan.migrationPath);
434
+ const existingMigration = existsSync3(migrationPath) ? readFileSync3(migrationPath, "utf8") : null;
435
+ const files = generateFeature(plan, { existingLogic, existingMigration, library: loadHolesLibrary() });
436
+ const manifestEntry = applyManifestEntry(project.rawManifest, plan, intentOfSpec(markdown), entryStatusForSpec(parseSpecDocument(markdown).frontmatter?.status));
437
+ const specContracts = applySpecContracts(markdown, plan);
438
+ const elapsedMs = Math.round(performance2.now() - started);
439
+ let written = 0;
440
+ let emitted = 0;
441
+ for (const file of files) {
442
+ const absolute = join3(project.root, file.path);
443
+ const exists = existsSync3(absolute);
444
+ emitted += file.content.split("\n").length - 1;
445
+ if (exists && readFileSync3(absolute, "utf8") === file.content) {
446
+ out.log(` identique ${file.path}`);
447
+ continue;
448
+ }
449
+ mkdirSync2(dirname2(absolute), { recursive: true });
450
+ writeFileSync2(absolute, file.content, "utf8");
451
+ written += 1;
452
+ out.log(` ${exists ? "r\xE9\xE9crit " : "\xE9crit "} ${file.path}`);
453
+ }
454
+ if (manifestEntry.outcome === "unchanged") {
455
+ out.log(` identique ${MANIFEST_FILE} \u203A features[${featureId}].routes, dbTables`);
456
+ } else {
457
+ writeFileSync2(join3(project.root, MANIFEST_FILE), serializeManifest(manifestEntry.manifest), "utf8");
458
+ out.log(
459
+ ` ${manifestEntry.outcome === "created" ? "ajout\xE9e " : "r\xE9\xE9crite "} ${MANIFEST_FILE} \u203A features[${featureId}] (${plan.operations.length} route(s), ${1 + plan.slots.length + plan.references.filter((reference) => reference.table !== null).length} table(s)${manifestEntry.outcome === "created" ? `, status ${entryStatusForSpec(parseSpecDocument(markdown).frontmatter?.status)} : \xE0 vous de dire delivered` : ""})`
460
+ );
461
+ }
462
+ if (specContracts.outcome === "section-missing") {
463
+ out.error(`ogygie generate \u2014 ${target.specPath} n'a pas de section # Contrats de donn\xE9es, le bloc g\xE9n\xE9r\xE9 n'a nulle part o\xF9 aller (section obligatoire, verify-specs).`);
464
+ return 1;
465
+ }
466
+ if (specContracts.outcome === "unchanged") {
467
+ out.log(` identique ${target.specPath} \u203A bloc @generated de # Contrats de donn\xE9es`);
468
+ } else {
469
+ const addedLines = specContracts.markdown.split("\n").length - markdown.split("\n").length;
470
+ writeFileSync2(join3(project.root, target.specPath), specContracts.markdown, "utf8");
471
+ out.log(
472
+ ` ${specContracts.outcome === "inserted" ? "ins\xE9r\xE9 " : "r\xE9\xE9crit "} ${target.specPath} \u203A bloc @generated de # Contrats de donn\xE9es${specContracts.outcome === "inserted" ? ` (+${addedLines} lignes)` : ""}`
473
+ );
474
+ }
475
+ out.log(
476
+ `ogygie generate \u2014 ${featureId} : ${files.length} fichier(s) \xE9mis en ${elapsedMs} ms (${emitted} lignes \xE9mises, entr\xE9e du manifeste et bloc de la spec compris dans le temps), ${written} \xE9crit(s), ${report.tally.derivable} ligne(s) li\xE9e(s), ${report.tally.hole} port\xE9e(s) par un trou.`
477
+ );
478
+ return 0;
479
+ }
480
+
481
+ // src/cli/preview-command.ts
482
+ import { spawnSync } from "node:child_process";
483
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "node:fs";
484
+ import { join as join4 } from "node:path";
485
+ var PREVIEWS_ROOT = "previews";
486
+ function previewsDirOf(featureId, composition) {
487
+ return composition === null ? `${PREVIEWS_ROOT}/${featureId}` : `${PREVIEWS_ROOT}/${featureId}/${composition}`;
488
+ }
489
+ var vitestPreviewRunner = ({ projectRoot, testPath, outDir }) => {
490
+ const result = spawnSync("pnpm", ["exec", "vitest", "run", testPath], {
491
+ cwd: projectRoot,
492
+ env: { ...process.env, OGYGIE_PREVIEW_OUT: outDir, CI: "1" },
493
+ encoding: "utf8"
494
+ });
495
+ const lines = `${result.stdout ?? ""}
496
+ ${result.stderr ?? ""}`.split("\n").filter((line) => line.trim() !== "");
497
+ return { code: result.status ?? 1, lines };
498
+ };
499
+ async function runPreviewCommand(project, featureId, out, runner = vitestPreviewRunner) {
500
+ if (featureId === void 0) {
501
+ out.error("usage: ogygie generate preview <screen-id>");
502
+ return 1;
503
+ }
504
+ const lookup = screenTargetOf(project, featureId);
505
+ if (!lookup.ok) {
506
+ out.log(`ogygie generate preview \u2014 ${featureId} n'est pas un \xE9cran de la carte : rien \xE0 rendre (${lookup.problem.cause}).`);
507
+ return 0;
508
+ }
509
+ const { target } = lookup;
510
+ if (target.front.tests !== "static-render") {
511
+ out.log(`ogygie generate preview \u2014 la cible front est ${target.front.tests} : les aper\xE7us demandent static-render (D43), rien \xE0 rendre.`);
512
+ return 0;
513
+ }
514
+ const testPath = `${target.folder}/${pascalCaseOf(featureId)}Screen${PREVIEWS_FILE_SUFFIX}`;
515
+ if (!existsSync4(join4(project.root, testPath))) {
516
+ out.error(`ogygie generate preview \u2014 ${testPath} n'existe pas : lancez \`ogygie generate generate ${featureId}\` d'abord, le test des aper\xE7us est \xE9mis avec l'\xE9cran.`);
517
+ return 1;
518
+ }
519
+ const specAbsolute = join4(project.root, target.specPath);
520
+ const composition = existsSync4(specAbsolute) ? compositionOfSpec(readFileSync4(specAbsolute, "utf8")) : null;
521
+ const relativeDir = previewsDirOf(featureId, composition);
522
+ const outDir = join4(project.root, relativeDir);
523
+ const run = runner({ projectRoot: project.root, testPath, outDir });
524
+ for (const line of run.lines) out.log(` ${line}`);
525
+ if (run.code !== 0) {
526
+ out.error(`ogygie generate preview \u2014 le rendu des aper\xE7us de ${featureId} a \xE9chou\xE9 (code ${run.code}) : rien n'est publi\xE9 tant qu'un \xE9tat ne rend pas.`);
527
+ return run.code;
528
+ }
529
+ const pages = existsSync4(outDir) ? readdirSync3(outDir).filter((name) => name.endsWith(".html")).sort() : [];
530
+ out.log(`ogygie generate preview \u2014 ${featureId} : ${pages.length} page(s) dans ${relativeDir}/ (${pages.map((name) => name.replace(/\.html$/u, "")).join(", ")})${composition === null ? "" : ` \u2014 composition ${composition}`}.`);
531
+ return pages.length === 0 ? 1 : 0;
532
+ }
533
+
534
+ // src/cli/schemas-command.ts
535
+ import { spawnSync as spawnSync2 } from "node:child_process";
536
+ import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
537
+ import { dirname as dirname3, join as join5 } from "node:path";
538
+ import { pathToFileURL } from "node:url";
539
+
540
+ // src/schemas/draft.ts
541
+ var SCHEMAS_NEEDS_OPERATOR = "OGYGIE_SCHEMAS_NEEDS_OPERATOR";
542
+ var SCHEMAS_ATTEMPTS = 2;
543
+ var SCHEMAS_SYSTEM_PROMPT = [
544
+ "You write the FIRST `schemas.ts` of a feature generated by the Ogygie framework: the Zod v4 data contracts its `# G\xE9n\xE9ration` block cites. You receive the spec \u2014 its block, its behaviours, its prose \u2014 and nothing else. The schemas are the source of truth the generator introspects: columns, factories and output mappings derive from them.",
545
+ "",
546
+ "Answer with the COMPLETE TypeScript module only, in one fenced block or bare: no prose before or after.",
547
+ "",
548
+ "Rules of the module:",
549
+ '- `import { z } from "zod";` is the only import (a sibling feature\'s contracts may be imported by a relative path ending in `schemas.js`); no runtime value but schemas; never `process`, `fetch`, `require`, a clock;',
550
+ "- every schema the block cites (`input`, `output`, `resource`, `rowSchema`, slot and reference `schema`, `orderBy`) is `export const <Name>Schema = z.\u2026` with its `export type <Name> = z.infer<typeof <Name>Schema>`; names end in `Schema`, identifiers and comments are English;",
551
+ "- the resource schema is one `z.object` whose fields are the table's columns, camelCase: `id: z.uuid()`, dates `z.iso.date()` or `z.iso.datetime()`, money and measures `z.number()`, closed lists `z.enum([...])`; a motif `signedWrite: { by, at }` means the resource carries those two fields; an `appendOnlySupersedes` resource carries `supersedes` and `supersededBy` (nullable uuids);",
552
+ "- a read's output wraps a collection: `{ <field>: z.array(<Element>Schema) }`, the element COVERING the resource (`ResourceSchema.extend({...})` for slot arrays, reference counts, view or project fields the block declares); a create's input carries the resource fields the client sends (never `id`, never the signature, never a derived field); an update's input is the partial body and REFUSES an empty body (`.partial().refine((body) => Object.values(body).some((value) => value !== undefined), { message: \"at least one field must be sent\" })`); a setOrClear's input is `{ <value field> }` nullable; a delete's input is `z.object({})`; an import's input is `z.object({ csv: z.string() })`, its row schema the create's input;",
553
+ '- a field the structure alone cannot populate \u2014 a regex, a `.refine`, a `.trim()`, a `.transform` \u2014 carries its example: `.meta({ examples: ["\u2026"] })`, and the example passes the schema;',
554
+ '- the examples of the `holes` block parse against the fields they read: when an accept or reject example gives `valuationDate: "2026-02-28"`, the input\'s `valuationDate` accepts that string;',
555
+ // Assembled from fragments: the repo's lint reads source text and would refuse the spelled-out words.
556
+ `- no \`any\`, no \`@ts-${"ignore"}\`, no \`as unknown ${"as"}\`, no default export; two-space indentation; a short JSDoc on each exported schema.`
557
+ ].join("\n");
558
+ async function draftSchemas(deps, attempts = SCHEMAS_ATTEMPTS) {
559
+ const usage = [];
560
+ let previous;
561
+ let model = null;
562
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
563
+ const candidate = await deps.draw(previous);
564
+ usage.push({ ...candidate.usage, model: candidate.model });
565
+ model = candidate.model;
566
+ const bans = forbiddenInSchemas(candidate.source);
567
+ const problems = bans.length > 0 ? bans : (await deps.judge(candidate.source, attempt)).problems;
568
+ deps.log(JSON.stringify({ action: "schemas", attempt, model: candidate.model, outcome: problems.length === 0 ? "green" : bans.length > 0 ? "banned" : "red", problems: problems.length }));
569
+ if (problems.length === 0) return { outcome: "written", attempts: attempt, problems: [], model, usage };
570
+ await deps.discard();
571
+ previous = { source: candidate.source, problems };
572
+ }
573
+ return { outcome: "needs-operator", attempts, problems: previous?.problems ?? [], model, usage };
574
+ }
575
+ function extractModuleSource(answer) {
576
+ const fenced = /```(?:ts|tsx|typescript)?\s*\n([\s\S]*?)\n```/u.exec(answer);
577
+ return `${(fenced?.[1] ?? answer).replace(/\r\n/gu, "\n").trim()}
578
+ `;
579
+ }
580
+ var banned = (...fragments) => new RegExp(fragments.join(""), "u");
581
+ var IMPORT = /^\s*import\s+(?:type\s+)?[^;]*?\sfrom\s+["']([^"']+)["']/gmu;
582
+ var ALLOWED_IMPORT = /^(?:zod|zod\/v4|\.{1,2}\/[^"']*schemas(?:\.js|\.ts)?)$/u;
583
+ function forbiddenInSchemas(source) {
584
+ const code = source.replace(/\/\*[\s\S]*?\*\//gu, "").replace(/(^|[^:])\/\/.*$/gmu, "$1");
585
+ const rules = [];
586
+ for (const match of code.matchAll(IMPORT)) {
587
+ const specifier = match[1] ?? "";
588
+ if (!ALLOWED_IMPORT.test(specifier)) rules.push(`import de ${JSON.stringify(specifier)} : un schemas.ts n'importe que zod et les contrats d'une feature voisine (puret\xE9 des contrats)`);
589
+ }
590
+ const checks = [
591
+ [banned("(:\\s*a", "ny\\b)|(<a", "ny[,>])|(\\bas a", "ny\\b)|(\\ba", "ny\\[\\])"), "type any interdit"],
592
+ [banned("@ts-", "ignore"), "ts-ignore interdit"],
593
+ [banned("\\bas unknown ", "as\\b"), ["as unknown ", "as interdit"].join("")],
594
+ [/\brequire\s*\(/u, "require interdit"],
595
+ [/\bprocess\.(env|exit|argv)\b/u, "process hors de port\xE9e d'un contrat"],
596
+ [/\bfetch\s*\(/u, "aucun r\xE9seau dans un contrat"],
597
+ [/\beval\s*\(|\bnew Function\s*\(/u, "aucune \xE9valuation dynamique"],
598
+ [/\bnew Date\s*\(\s*\)|\bDate\.now\s*\(/u, "aucune horloge dans un contrat"],
599
+ [/^\s*export\s+default\b/mu, "pas d'export par d\xE9faut : chaque contrat est un export nomm\xE9"]
600
+ ];
601
+ for (const [pattern, rule] of checks) if (pattern.test(code)) rules.push(rule);
602
+ if (!/^\s*export\s+const\s+[A-Z][A-Za-z0-9]*Schema\s*=/mu.test(code)) rules.push("aucun `export const <Nom>Schema` : le module n'exporte aucun contrat");
603
+ return rules;
604
+ }
605
+ function citedSchemaNamesOf(block) {
606
+ const cited = /* @__PURE__ */ new Map();
607
+ const cite = (name, role) => {
608
+ if (name !== void 0 && !cited.has(name)) cited.set(name, role);
609
+ };
610
+ cite(block.contracts.resource, "contracts.resource \u2014 la ressource, une ligne de la table");
611
+ for (const [slot, declaration] of Object.entries(block.contracts.slots ?? {})) cite(declaration.schema, `contracts.slots.${slot}.schema \u2014 une case de la ressource`);
612
+ for (const [reference, declaration] of Object.entries(block.contracts.references ?? {})) cite(declaration.schema, `contracts.references.${reference}.schema \u2014 une ligne qui pointe la ressource`);
613
+ for (const operation of block.operations) {
614
+ cite(operation.input, `operations.${operation.id}.input (${operation.kind})`);
615
+ cite(operation.output, `operations.${operation.id}.output (${operation.kind})`);
616
+ if (operation.kind === "read" && typeof operation.orderBy === "string") cite(operation.orderBy, `operations.${operation.id}.orderBy`);
617
+ }
618
+ cite(block.motifs.allOrNothingImport?.rowSchema, "motifs.allOrNothingImport.rowSchema \u2014 une ligne du fichier import\xE9");
619
+ return [...cited.entries()].map(([name, role]) => ({ name, role }));
620
+ }
621
+ function fieldParserOf(moduleExports, schemaName, field) {
622
+ const schema = moduleExports[schemaName];
623
+ if (typeof schema !== "object" || schema === null || !("shape" in schema)) return null;
624
+ const shape = schema.shape;
625
+ if (typeof shape !== "object" || shape === null) return null;
626
+ const candidate = shape[field];
627
+ return typeof candidate === "object" && candidate !== null && "safeParse" in candidate ? candidate : null;
628
+ }
629
+ function holeExampleProblems(block, moduleExports) {
630
+ const problems = [];
631
+ const schemasReadBy = (holeId) => {
632
+ const names = /* @__PURE__ */ new Set();
633
+ for (const operation of block.operations) {
634
+ if (operation.kind === "command" || !(operation.holes ?? []).includes(holeId)) continue;
635
+ const rowSchema = operation.kind === "import" ? block.motifs.allOrNothingImport?.rowSchema : void 0;
636
+ names.add(rowSchema ?? operation.input);
637
+ }
638
+ return [...names];
639
+ };
640
+ const parseExample = (holeId, label, schemaName, example, fields) => {
641
+ for (const field of fields) {
642
+ if (!(field in example)) continue;
643
+ const parser = fieldParserOf(moduleExports, schemaName, field);
644
+ if (parser === null) continue;
645
+ const parsed = parser.safeParse(example[field]);
646
+ if (!parsed.success) {
647
+ const why = (parsed.error?.issues ?? []).map((issue) => issue.message).join(" ; ");
648
+ problems.push(`holes.${holeId}.examples.${label}.${field}: ${schemaName}.${field} refuse ${JSON.stringify(example[field])} (${why})`);
649
+ }
650
+ }
651
+ };
652
+ for (const hole of block.holes) {
653
+ if (hole.kind !== "validate" && hole.kind !== "derive") continue;
654
+ for (const schemaName of schemasReadBy(hole.id)) {
655
+ if (hole.kind === "validate") {
656
+ acceptExamplesOf(hole).forEach((example, index) => parseExample(hole.id, `accept${index === 0 ? "" : `.${index}`}`, schemaName, example, hole.reads));
657
+ parseExample(hole.id, "reject", schemaName, hole.examples.reject, hole.reads);
658
+ } else {
659
+ hole.examples.accept.forEach((example, index) => parseExample(hole.id, `accept.${index}.input`, schemaName, example.input, hole.reads));
660
+ if (hole.examples.reject !== void 0) parseExample(hole.id, "reject.input", schemaName, hole.examples.reject.input, hole.reads);
661
+ }
662
+ }
663
+ }
664
+ return problems;
665
+ }
666
+ function writeInputProblems(block, moduleExports) {
667
+ const problems = [];
668
+ for (const operation of block.operations) {
669
+ if (operation.kind !== "update") continue;
670
+ const schema = moduleExports[operation.input];
671
+ if (typeof schema !== "object" || schema === null || !("safeParse" in schema)) continue;
672
+ if (schema.safeParse({}).success) {
673
+ problems.push(`operations.${operation.id}.input: ${operation.input} accepte un corps vide {} \u2014 une mise \xE0 jour partielle refuse le corps sans aucun champ (.refine)`);
674
+ }
675
+ }
676
+ return problems;
677
+ }
678
+ function fieldsOf(moduleExports, schemaName) {
679
+ if (schemaName === void 0) return null;
680
+ const schema = moduleExports[schemaName];
681
+ return schema !== void 0 && typeof schema === "object" && schema !== null && typeof schema.shape === "object" && schema.shape !== null ? Object.keys(schema.shape) : null;
682
+ }
683
+ function resourceCoverageProblems(block, moduleExports) {
684
+ const resourceFields = fieldsOf(moduleExports, block.contracts.resource);
685
+ if (resourceFields === null) return [];
686
+ const problems = [];
687
+ const signed = block.motifs.signedWrite;
688
+ const derived = new Set(block.holes.flatMap((hole) => hole.kind === "derive" ? [hole.writes] : []));
689
+ for (const operation of block.operations) {
690
+ if (operation.kind !== "create") continue;
691
+ const inputFields = fieldsOf(moduleExports, operation.input);
692
+ if (inputFields === null) continue;
693
+ const covered = /* @__PURE__ */ new Set(["id", ...inputFields, ...derived, ...signed === void 0 ? [] : [signed.by, signed.at]]);
694
+ for (const field of resourceFields) {
695
+ if (covered.has(field)) continue;
696
+ problems.push(`${block.contracts.resource ?? "resource"}.${field}: ni envoy\xE9 par ${operation.input}, ni d\xE9riv\xE9 par un trou, ni sign\xE9 \u2014 l'insertion g\xE9n\xE9r\xE9e ne saurait pas le remplir (retirer le champ, l'ajouter \xE0 l'entr\xE9e, ou le d\xE9river)`);
697
+ }
698
+ }
699
+ return problems;
700
+ }
701
+ function missingExportsOf(block, exportedNames) {
702
+ const exported = new Set(exportedNames);
703
+ return citedSchemaNamesOf(block).filter(({ name }) => !exported.has(name)).map(({ name, role }) => `${name} n'est pas export\xE9 par le module (${role})`);
704
+ }
705
+ function buildSchemasPrompt(input) {
706
+ const cited = citedSchemaNamesOf(input.block).map(({ name, role }) => `- \`${name}\` \u2014 ${role}`);
707
+ const sections = [
708
+ `# Feature \`${input.featureId}\` \u2014 write \`${input.modulePath}\``,
709
+ "",
710
+ `## The spec (\`${input.specPath}\`)`,
711
+ "",
712
+ input.specMarkdown.trim(),
713
+ "",
714
+ "## Schemas the block cites \u2014 each one an export of the module",
715
+ "",
716
+ ...cited
717
+ ];
718
+ if (input.previous !== void 0) {
719
+ sections.push(
720
+ "",
721
+ "## Your previous module was refused \u2014 correct it, keep what was right",
722
+ "",
723
+ "```ts",
724
+ input.previous.source.trim(),
725
+ "```",
726
+ "",
727
+ "Problems:",
728
+ ...input.previous.problems.map((problem) => `- ${problem}`)
729
+ );
730
+ }
731
+ return `${sections.join("\n")}
732
+ `;
733
+ }
734
+
735
+ // src/cli/schemas-command.ts
736
+ function parseSchemasFlags(argv) {
737
+ const index = argv.indexOf("--model");
738
+ const value = argv[index + 1];
739
+ return { model: index !== -1 && value !== void 0 ? value : DEFAULT_FILL_MODELS.spec };
740
+ }
741
+ var SCHEMAS_GATE_SCRIPTS = ["typecheck", "ogygie:verify-contracts"];
742
+ var pnpmGateRunner = ({ projectRoot, script }) => {
743
+ const result = spawnSync2("pnpm", ["run", script], { cwd: projectRoot, env: scrubbedEnvironment(process.env), encoding: "utf8" });
744
+ const lines = `${result.stdout ?? ""}
745
+ ${result.stderr ?? ""}`.split("\n").filter((line) => line.trim() !== "");
746
+ return { green: result.status === 0, lines };
747
+ };
748
+ function featureFolderFor(project, featureId) {
749
+ const parents = project.manifest.features.filter((feature) => feature.app === void 0 || feature.app === project.apiApp?.name).map((feature) => feature.folder.split("/").slice(0, -1).join("/")).filter((parent) => parent !== "");
750
+ const counts = /* @__PURE__ */ new Map();
751
+ for (const parent of parents) counts.set(parent, (counts.get(parent) ?? 0) + 1);
752
+ const common = [...counts.entries()].sort((left, right) => right[1] - left[1])[0]?.[0] ?? "src/features";
753
+ return `${common}/${featureId}`;
754
+ }
755
+ function availableGateScripts(projectRoot) {
756
+ const path = join5(projectRoot, "package.json");
757
+ if (!existsSync5(path)) return [];
758
+ const scripts = JSON.parse(readFileSync5(path, "utf8")).scripts ?? {};
759
+ return SCHEMAS_GATE_SCRIPTS.filter((script) => script in scripts);
760
+ }
761
+ async function runSchemasCommand(project, featureId, argv, out, deps = {}) {
762
+ if (featureId === void 0 || featureId === "") {
763
+ out.error("usage: ogygie generate schemas <feature-id> [--model <model>]");
764
+ return 1;
765
+ }
766
+ const onMap = project.manifest.features.find((feature) => feature.name === featureId);
767
+ const specPath = onMap?.spec ?? `${project.specsFolder.replace(/\/+$/u, "")}/${featureId}.spec.md`;
768
+ const specAbsolute = join5(project.root, specPath);
769
+ if (!existsSync5(specAbsolute)) {
770
+ out.error(formatProblem(ogygieError({ code: "OGYGIE_GENERATE_PROJECT_INVALID", message: `No spec carries the feature "${featureId}".`, file: specPath, cause: `${specPath} does not exist`, suggestedFix: "Write the spec first: a schemas module derives from a spec, never the reverse." })));
771
+ return 1;
772
+ }
773
+ if (project.apiApp === void 0 || project.generation === null) {
774
+ out.error(formatProblem(ogygieError({ code: "OGYGIE_GENERATE_PROJECT_INVALID", message: `The feature "${featureId}" cannot be generated from this map.`, file: MANIFEST_FILE, cause: project.apiApp === void 0 ? "apps[] has no entry with type=api" : "the manifest declares no generation block", suggestedFix: "Declare the api app and the `generation` block in the manifest." })));
775
+ return 1;
776
+ }
777
+ const markdown = readFileSync5(specAbsolute, "utf8");
778
+ const first = checkSpec({ markdown, specPath, generation: project.generation });
779
+ if (first.kind !== "feature" || first.block === null) {
780
+ out.log(`ogygie generate schemas \u2014 ${specPath} ${first.kind === "screen" ? "est un \xE9cran : ses contrats sont ceux des features qu'il consomme" : "n'a pas de bloc # G\xE9n\xE9ration de feature"}, rien \xE0 \xE9crire.`);
781
+ return 0;
782
+ }
783
+ const block = first.block;
784
+ const folder = onMap === void 0 ? `${project.apiApp.path}/${featureFolderFor(project, featureId)}` : `${project.apiApp.path}/${onMap.folder}`;
785
+ const modulePath = `${folder}/${block.contracts.module.replace(/^\.\//u, "")}`;
786
+ const moduleAbsolute = join5(project.root, modulePath);
787
+ if (existsSync5(moduleAbsolute)) {
788
+ out.log(`ogygie generate schemas \u2014 ${modulePath} existe : il appartient au repo, jamais r\xE9g\xE9n\xE9r\xE9 (D52).`);
789
+ return 0;
790
+ }
791
+ const modelApiKey = deps.modelApiKey ?? process.env["OGYGIE_MODEL_API_KEY"];
792
+ if (deps.client === void 0 && modelApiKey === void 0) {
793
+ out.error(formatProblem(ogygieError({ code: SCHEMAS_NEEDS_OPERATOR, message: `The first schemas module of "${featureId}" cannot be drawn: no model key.`, file: modulePath, cause: "OGYGIE_MODEL_API_KEY is absent", suggestedFix: "Give the job the client's workspace key (repo secret), or write the module by hand: it is the operator's gesture then." })));
794
+ return 2;
795
+ }
796
+ const flags = parseSchemasFlags(argv);
797
+ const client = deps.client ?? createMessagesClient({ apiKey: modelApiKey ?? "", fetchImpl: deps.fetchImpl ?? fetch, credentialHint: "OGYGIE_MODEL_API_KEY" });
798
+ const gateRunner = deps.gateRunner ?? pnpmGateRunner;
799
+ const gateScripts = availableGateScripts(project.root);
800
+ for (const script of SCHEMAS_GATE_SCRIPTS) if (!gateScripts.includes(script)) out.log(`ogygie generate schemas \u2014 le projet n'a pas de script \`${script}\` : ce gate ne juge pas le module ici, la CI le fera.`);
801
+ const draftDeps = {
802
+ async draw(previous) {
803
+ const answer = await client.complete({
804
+ model: flags.model,
805
+ system: SCHEMAS_SYSTEM_PROMPT,
806
+ messages: [{ role: "user", content: buildSchemasPrompt({ featureId, specPath, modulePath, specMarkdown: markdown, block, previous }) }],
807
+ maxTokens: 16e3,
808
+ effort: "high"
809
+ });
810
+ return { source: extractModuleSource(answer.text), model: answer.model, usage: answer.usage };
811
+ },
812
+ async judge(source, attempt) {
813
+ mkdirSync3(dirname3(moduleAbsolute), { recursive: true });
814
+ writeFileSync3(moduleAbsolute, source, "utf8");
815
+ let moduleExports;
816
+ try {
817
+ moduleExports = await importProductModule(`${pathToFileURL(moduleAbsolute).href}?attempt=${attempt}`);
818
+ } catch (thrown) {
819
+ return { problems: [`le module ne se charge pas : ${thrown instanceof Error ? thrown.message : String(thrown)}`] };
820
+ }
821
+ const missing = missingExportsOf(block, Object.keys(moduleExports));
822
+ if (missing.length > 0) return { problems: missing };
823
+ const report = checkSpec({ markdown, specPath, generation: project.generation, schemas: modelsOfModule(moduleExports) });
824
+ const problems = report.problems.map((problem) => `${problem.code} ${String(problem.details?.["path"] ?? "")}: ${problem.cause}`);
825
+ problems.push(...holeExampleProblems(block, moduleExports), ...writeInputProblems(block, moduleExports), ...resourceCoverageProblems(block, moduleExports));
826
+ if (problems.length > 0) return { problems };
827
+ for (const script of gateScripts) {
828
+ const run = gateRunner({ projectRoot: project.root, script });
829
+ if (!run.green) return { problems: [`\`pnpm run ${script}\` est rouge :`, ...run.lines.slice(-20)] };
830
+ }
831
+ return { problems: [] };
832
+ },
833
+ async discard() {
834
+ rmSync(moduleAbsolute, { force: true });
835
+ },
836
+ log: (line) => out.log(` ${line}`)
837
+ };
838
+ const result = await draftSchemas(draftDeps);
839
+ const summaryPath = deps.summaryPath ?? process.env["GITHUB_STEP_SUMMARY"];
840
+ if (summaryPath !== void 0 && result.usage.length > 0) {
841
+ const rows = result.usage.map((usage, index) => `| ${index + 1} | ${result.outcome} | ${usage.model} | ${usage.inputTokens} | ${usage.outputTokens} |`);
842
+ appendFileSync(summaryPath, `
843
+ ### ogygie generate schemas \u2014 ${featureId}
844
+
845
+ | tentative | issue | mod\xE8le | entr\xE9e | sortie |
846
+ |---|---|---|---|---|
847
+ ${rows.join("\n")}
848
+ `, "utf8");
849
+ }
850
+ const totalInput = result.usage.reduce((sum, usage) => sum + usage.inputTokens, 0);
851
+ const totalOutput = result.usage.reduce((sum, usage) => sum + usage.outputTokens, 0);
852
+ if (result.outcome === "needs-operator") {
853
+ out.error(
854
+ formatProblem(
855
+ ogygieError({
856
+ code: SCHEMAS_NEEDS_OPERATOR,
857
+ message: `The first schemas module of "${featureId}" still fails the gates after ${result.attempts} attempts; nothing was written.`,
858
+ file: modulePath,
859
+ cause: result.problems.join(" ; "),
860
+ suggestedFix: "Write the module by hand from the spec (the operator's gesture, D19), or split the block: the listed problems say what the model could not reconcile.",
861
+ details: { problems: result.problems, attempts: result.attempts, model: result.model }
862
+ })
863
+ )
864
+ );
865
+ out.error(`ogygie generate schemas \u2014 ${featureId} : l'op\xE9rateur reprend la main (needs-operator) ; ${result.usage.length} appel(s) de mod\xE8le (${totalInput} jetons d'entr\xE9e, ${totalOutput} de sortie).`);
866
+ return 2;
867
+ }
868
+ out.log(` \xE9crit ${modulePath}`);
869
+ if (onMap === void 0) {
870
+ const raw = structuredClone(project.rawManifest);
871
+ const features = Array.isArray(raw["features"]) ? raw["features"] : [];
872
+ raw["features"] = features;
873
+ const status = entryStatusForSpec(parseSpecDocument(markdown).frontmatter?.status);
874
+ features.push({ name: featureId, intent: intentForManifest(intentOfSpec(markdown), specPath), folder: featureFolderFor(project, featureId), spec: specPath, routes: [], dbTables: [], dependsOn: [], status });
875
+ writeFileSync3(join5(project.root, MANIFEST_FILE), serializeManifest(raw), "utf8");
876
+ out.log(` ajout\xE9e ${MANIFEST_FILE} \u203A features[${featureId}] (routes et tables viendront de \`generate\`, status ${status}${status === "skeleton" ? " tant que la spec est draft" : ""})`);
877
+ }
878
+ out.log(`ogygie generate schemas \u2014 ${featureId} : module \xE9crit en ${result.attempts} tentative(s) par ${result.model ?? "?"}, ${result.usage.length} appel(s) de mod\xE8le (${totalInput} jetons d'entr\xE9e, ${totalOutput} de sortie) ; commit\xE9 une fois, jamais r\xE9g\xE9n\xE9r\xE9 (D52).`);
879
+ return 0;
880
+ }
881
+
882
+ // src/cli/verify-command.ts
883
+ import { performance as performance3 } from "node:perf_hooks";
884
+
885
+ // ../ogygie-core/src/repo-adapter.ts
886
+ import { readFile as readFileFromDisk, readdir, realpath, stat } from "node:fs/promises";
887
+ import { isAbsolute, join as join6, resolve, sep } from "node:path";
888
+ import { z as z2 } from "zod";
889
+ var RepoFileEntrySchema = z2.object({
890
+ /** Path relative to the LISTED directory, "/" separated, never absolute. */
891
+ path: z2.string(),
892
+ sizeBytes: z2.number().int().nonnegative()
893
+ });
894
+ var NEVER_LISTED = /* @__PURE__ */ new Set(["node_modules"]);
895
+ function isSkippedDirectory(name) {
896
+ return name.startsWith(".") || NEVER_LISTED.has(name);
897
+ }
898
+ function refusePathOutsideRepo(relativePath, cause) {
899
+ return throwOgygieError(
900
+ {
901
+ code: "PATH_OUTSIDE_REPO",
902
+ message: "The requested path resolves outside the repo root.",
903
+ file: relativePath,
904
+ cause,
905
+ suggestedFix: "Address a path INSIDE the repo, written relative to its root and without `..`."
906
+ },
907
+ 400
908
+ );
909
+ }
910
+ function resolveInsideRoot(rootDir, relativePath) {
911
+ if (isAbsolute(relativePath)) {
912
+ refusePathOutsideRepo(relativePath, `"${relativePath}" is an absolute path`);
913
+ }
914
+ const root = resolve(rootDir);
915
+ const absolute = resolve(root, relativePath);
916
+ if (absolute !== root && !absolute.startsWith(root + sep)) {
917
+ refusePathOutsideRepo(relativePath, `"${relativePath}" resolves to "${absolute}", outside "${root}"`);
918
+ }
919
+ return absolute;
920
+ }
921
+ async function assertRealPathInsideRoot(rootDir, absolute, relativePath) {
922
+ let realRoot;
923
+ let realTarget;
924
+ try {
925
+ realRoot = await realpath(rootDir);
926
+ realTarget = await realpath(absolute);
927
+ } catch {
928
+ return;
929
+ }
930
+ if (realTarget !== realRoot && !realTarget.startsWith(realRoot + sep)) {
931
+ refusePathOutsideRepo(
932
+ relativePath,
933
+ `"${relativePath}" is a symbolic link leading to "${realTarget}", outside "${realRoot}"`
934
+ );
935
+ }
936
+ }
937
+ async function listFilesRecursively(absoluteDir, prefix) {
938
+ let entries;
939
+ try {
940
+ entries = await readdir(absoluteDir, { withFileTypes: true });
941
+ } catch {
942
+ return [];
943
+ }
944
+ const files = [];
945
+ for (const entry of entries) {
946
+ const relativePath = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
947
+ if (entry.isDirectory()) {
948
+ if (isSkippedDirectory(entry.name)) continue;
949
+ files.push(...await listFilesRecursively(join6(absoluteDir, entry.name), relativePath));
950
+ } else if (entry.isFile()) {
951
+ const stats = await stat(join6(absoluteDir, entry.name));
952
+ files.push({ path: relativePath, sizeBytes: stats.size });
953
+ }
954
+ }
955
+ return files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
956
+ }
957
+ async function resolveGitDirs(rootDir) {
958
+ const dotGit = join6(rootDir, ".git");
959
+ let gitDir;
960
+ try {
961
+ const stats = await stat(dotGit);
962
+ if (stats.isDirectory()) {
963
+ gitDir = dotGit;
964
+ } else {
965
+ const pointer = (await readFileFromDisk(dotGit, "utf8")).trim();
966
+ const target = pointer.startsWith("gitdir:") ? pointer.slice("gitdir:".length).trim() : "";
967
+ if (target === "") return null;
968
+ gitDir = isAbsolute(target) ? target : resolve(rootDir, target);
969
+ }
970
+ } catch {
971
+ return null;
972
+ }
973
+ try {
974
+ const commonPointer = (await readFileFromDisk(join6(gitDir, "commondir"), "utf8")).trim();
975
+ return { gitDir, commonDir: isAbsolute(commonPointer) ? commonPointer : resolve(gitDir, commonPointer) };
976
+ } catch {
977
+ return { gitDir, commonDir: gitDir };
978
+ }
979
+ }
980
+ async function readRefSha(dirs, refName) {
981
+ for (const base of [dirs.gitDir, dirs.commonDir]) {
982
+ try {
983
+ const sha = (await readFileFromDisk(join6(base, refName), "utf8")).trim();
984
+ if (sha !== "") return sha;
985
+ } catch {
986
+ }
987
+ }
988
+ try {
989
+ const packed = await readFileFromDisk(join6(dirs.commonDir, "packed-refs"), "utf8");
990
+ for (const line of packed.split("\n")) {
991
+ if (line.startsWith("#") || line.startsWith("^")) continue;
992
+ const [sha, name] = line.trim().split(" ");
993
+ if (name === refName && sha !== void 0 && sha !== "") return sha;
994
+ }
995
+ } catch {
996
+ return null;
997
+ }
998
+ return null;
999
+ }
1000
+ function createFilesystemRepoAdapter(rootDir) {
1001
+ return {
1002
+ async readFile(relativePath) {
1003
+ const absolute = resolveInsideRoot(rootDir, relativePath);
1004
+ await assertRealPathInsideRoot(rootDir, absolute, relativePath);
1005
+ try {
1006
+ return await readFileFromDisk(absolute, "utf8");
1007
+ } catch (thrown) {
1008
+ return throwOgygieError({
1009
+ code: "REPO_FILE_NOT_FOUND",
1010
+ message: "The requested file cannot be read from the repo.",
1011
+ file: relativePath,
1012
+ cause: `reading "${absolute}" failed: ${thrown instanceof Error ? thrown.message : String(thrown)}`,
1013
+ suggestedFix: "Check the path against what the map declares \u2014 the manifest is the only chart of a repo."
1014
+ });
1015
+ }
1016
+ },
1017
+ async listDirectory(relativePath) {
1018
+ const absolute = resolveInsideRoot(rootDir, relativePath);
1019
+ await assertRealPathInsideRoot(rootDir, absolute, relativePath);
1020
+ return listFilesRecursively(absolute, "");
1021
+ },
1022
+ async exists(relativePath) {
1023
+ const absolute = resolveInsideRoot(rootDir, relativePath);
1024
+ await assertRealPathInsideRoot(rootDir, absolute, relativePath);
1025
+ try {
1026
+ await stat(absolute);
1027
+ return true;
1028
+ } catch {
1029
+ return false;
1030
+ }
1031
+ },
1032
+ async currentRef() {
1033
+ const dirs = await resolveGitDirs(rootDir);
1034
+ if (dirs === null) return null;
1035
+ let head;
1036
+ try {
1037
+ head = (await readFileFromDisk(join6(dirs.gitDir, "HEAD"), "utf8")).trim();
1038
+ } catch {
1039
+ return null;
1040
+ }
1041
+ if (!head.startsWith("ref:")) return head === "" ? null : head;
1042
+ return readRefSha(dirs, head.slice("ref:".length).trim());
1043
+ }
1044
+ };
1045
+ }
1046
+
1047
+ // src/cli/verify-command.ts
1048
+ async function runVerifyCommand(project, out) {
1049
+ const started = performance3.now();
1050
+ const report = await verifyGeneration({
1051
+ repo: createFilesystemRepoAdapter(project.root),
1052
+ specsFolder: project.specsFolder,
1053
+ scanDirs: project.scanDirs,
1054
+ generation: project.generation,
1055
+ features: project.owners,
1056
+ loadSchemaModels: schemaModelsLoader(project.root),
1057
+ ...project.apiApp === void 0 ? {} : { apiAppPath: project.apiApp.path },
1058
+ ...project.manifest.tenancy === void 0 ? {} : { tenantColumn: project.manifest.tenancy.column },
1059
+ library: loadHolesLibrary(),
1060
+ loadConsumed: (block) => loadConsumedFeatures(project, block)
1061
+ });
1062
+ const elapsedMs = Math.round(performance3.now() - started);
1063
+ if (!report.ok) {
1064
+ for (const problem of report.problems) out.error(formatProblem(problem));
1065
+ out.error(`
1066
+ ogygie generate verify \u2014 ${report.problems.length} probl\xE8me(s).`);
1067
+ return 1;
1068
+ }
1069
+ const generatedSpecs = report.specs.filter((spec) => spec.generated);
1070
+ const derivable = generatedSpecs.reduce((sum, spec) => sum + spec.tally.derivable, 0);
1071
+ const holes = generatedSpecs.reduce((sum, spec) => sum + spec.tally.hole, 0);
1072
+ out.log(
1073
+ `ogygie generate verify \u2014 OK en ${elapsedMs} ms, ${report.specs.length} spec(s) v\xE9rifi\xE9es dont ${generatedSpecs.length} g\xE9n\xE9r\xE9e(s) (${derivable} ligne(s) li\xE9e(s), ${holes} port\xE9e(s) par un trou), ${report.generatedFiles.length} fichier(s) g\xE9n\xE9r\xE9(s) dans ${report.perimeter.scanDirs.join(", ")}.`
1074
+ );
1075
+ out.log(` hors p\xE9rim\xE8tre : ${report.perimeter.outOfScope.join(" ; ")}.`);
1076
+ return 0;
1077
+ }
1078
+
1079
+ // src/cli/run.ts
1080
+ var GENERATE_COMMANDS = ["check", "generate", "verify", "preview", "fill", "schemas"];
1081
+ var GENERATE_USAGE = "OGYGIE_GENERATE_USAGE";
1082
+ function isCommandName(word) {
1083
+ return word !== void 0 && GENERATE_COMMANDS.includes(word);
1084
+ }
1085
+ async function runGenerateCommand(input) {
1086
+ const out = { log: input.log, error: input.error };
1087
+ const [command, featureId] = input.argv.filter((argument) => !argument.startsWith("--"));
1088
+ if (!isCommandName(command)) {
1089
+ out.error(
1090
+ formatProblem(
1091
+ ogygieError({
1092
+ code: GENERATE_USAGE,
1093
+ message: `Unknown generate command ${JSON.stringify(command ?? "")}.`,
1094
+ file: null,
1095
+ cause: `expected one of: ${GENERATE_COMMANDS.join(", ")}`,
1096
+ suggestedFix: "Run `ogygie generate check [feature-id]`, `ogygie generate generate <feature-id|--all>`, `ogygie generate verify`, `ogygie generate preview <screen-id>`, `ogygie generate fill <feature-id>` or `ogygie generate schemas <feature-id>`."
1097
+ })
1098
+ )
1099
+ );
1100
+ return 1;
1101
+ }
1102
+ const loaded = loadGenerateProject(input.projectRoot);
1103
+ if (!loaded.ok) {
1104
+ for (const problem of loaded.problems) out.error(formatProblem(problem));
1105
+ out.error(`ogygie generate ${command} \u2014 ${loaded.problems.length} probl\xE8me(s) dans le projet, rien n'est fait.`);
1106
+ return 1;
1107
+ }
1108
+ switch (command) {
1109
+ case "check":
1110
+ return runCheckCommand(loaded.project, featureId, out);
1111
+ case "generate":
1112
+ return input.argv.includes("--all") ? runGenerateAllCommand(loaded.project, out) : runGenerateFeatureCommand(loaded.project, featureId, out);
1113
+ case "verify":
1114
+ return runVerifyCommand(loaded.project, out);
1115
+ case "preview":
1116
+ return runPreviewCommand(loaded.project, featureId, out);
1117
+ case "fill":
1118
+ return runFillCommand(loaded.project, featureId, input.argv, out);
1119
+ case "schemas":
1120
+ return runSchemasCommand(loaded.project, featureId, input.argv, out);
1121
+ }
1122
+ }
1123
+ export {
1124
+ GENERATE_COMMANDS,
1125
+ GENERATE_USAGE,
1126
+ runGenerateCommand
1127
+ };