@ogygie/generate 0.20.0 → 0.22.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.
@@ -1,3 +1,4 @@
1
+ import type { CustomViewPlan, ScreenPlan } from "../emit/screen-plan.js";
1
2
  import { type HoleLibraryEntry } from "../hole-shape.js";
2
3
  import { type FillOptions } from "../fill/fill.js";
3
4
  import { type FillModels } from "../fill/model-draw.js";
@@ -12,17 +13,26 @@ export declare function parseFillFlags(argv: readonly string[]): FillCommandFlag
12
13
  /** The variables a subprocess that runs a candidate body may see: the path to its tools, its home, nothing that is a secret. */
13
14
  export declare function scrubbedEnvironment(env: Readonly<Record<string, string | undefined>>): Record<string, string>;
14
15
  export interface HoleTestRunner {
16
+ /** Runs the tests of `testPath` whose name contains `testName` — a hole id, or the title of a view's example test. */
15
17
  (input: {
16
18
  projectRoot: string;
17
19
  testPath: string;
18
- holeId: string;
20
+ testName: string;
19
21
  }): {
20
22
  green: boolean;
21
23
  lines: string[];
22
24
  };
23
25
  }
24
- /** The real runner: the product's Vitest on the hole's own describe, in a process that holds no secret. */
26
+ /** The real runner: the product's Vitest on the named tests, in a process that holds no secret. */
25
27
  export declare const vitestHoleRunner: HoleTestRunner;
28
+ /** The title of a custom view's example test, as `<Ecran>.gen.test.tsx` emits it — up to the fragments, which Vitest matches as a prefix. */
29
+ export declare function viewExampleTestName(view: CustomViewPlan): string;
30
+ /** The title of the nominal page in `<Ecran>.previews.gen.test.tsx` — the static render a view candidate must survive (D53). */
31
+ export declare const NOMINAL_PREVIEW_TEST_NAME = "rend l'\u00E9tat nominal";
32
+ /** The exit code of `fill` when the body of a custom view stays unfilled: the operator's, and the `speculate` job labels the pull request. */
33
+ export declare const FILL_NEEDS_OPERATOR_EXIT = 2;
34
+ /** The product's pattern catalogue, when it keeps one where the starter does — the matter a view candidate composes from (D53). */
35
+ export declare const PATTERN_CATALOGUE_PATH = "design/patterns.md";
26
36
  export interface FillCommandDeps {
27
37
  runner?: HoleTestRunner;
28
38
  /** The model key — absent, the command fills from the library alone and says so. */
@@ -36,4 +46,6 @@ export interface FillCommandDeps {
36
46
  }
37
47
  /** An entry recorded in the library: a new file, or the provenance of an existing one extended. */
38
48
  export declare function recordLibraryEntry(libraryDir: string, entry: HoleLibraryEntry): "created" | "extended" | "unchanged";
49
+ /** The context a view candidate is drawn on: the view's hole doc, then the product's pattern catalogue when the repo keeps one (D53). */
50
+ export declare function viewContextOf(projectRoot: string, plan: ScreenPlan, view: CustomViewPlan): string;
39
51
  export declare function runFillCommand(project: GenerateProject, featureId: string | undefined, argv: readonly string[], out: CommandOutput, deps?: FillCommandDeps): Promise<number>;
@@ -1,6 +1,15 @@
1
- import { type ConsumedFeatureInput } from "../emit/screen-plan.js";
1
+ import { type CheckSpecReport } from "../check-spec.js";
2
+ import { type ConsumedFeatureInput, type ScreenPlan } from "../emit/screen-plan.js";
2
3
  import type { CommandOutput } from "./output.js";
3
- import { type GenerateProject, type LoadedConsumedFeature } from "./project.js";
4
+ import { type GenerateProject, type LoadedConsumedFeature, type ScreenTarget } from "./project.js";
4
5
  /** The consumed features a plan can be built on: every one with its schemas — with or without a block (D41 precision) — `check` refused the rest. */
5
6
  export declare function consumedInputsOf(loaded: ReadonlyMap<string, LoadedConsumedFeature>): ConsumedFeatureInput[];
7
+ export interface ScreenPlanLookup {
8
+ plan: ScreenPlan;
9
+ target: ScreenTarget;
10
+ markdown: string;
11
+ report: CheckSpecReport;
12
+ }
13
+ /** The plan of a screen as `generate` builds it — the spec checked with its consumed features — or null, the problems printed; `fill` builds on the same plan (D53). */
14
+ export declare function screenPlanOf(project: GenerateProject, featureId: string, out: CommandOutput, command?: string): Promise<ScreenPlanLookup | null>;
6
15
  export declare function runGenerateScreenCommand(project: GenerateProject, featureId: string, out: CommandOutput): Promise<number>;
package/dist/cli/run.js CHANGED
@@ -6,22 +6,16 @@ import {
6
6
  MANIFEST_FILE,
7
7
  PREVIEWS_FILE_SUFFIX,
8
8
  acceptExamplesOf,
9
- applyGeneratedContracts,
10
9
  applyManifestEntry,
11
10
  applySpecContracts,
12
11
  boundBehaviorsOf,
13
- boundScreenBehaviorsOf,
14
12
  buildFeaturePlan,
15
- buildScreenPlan,
16
13
  checkSpec,
17
- contractViolationCodeOf,
18
14
  createMessagesClient,
19
- emitScreenSpecContracts,
20
15
  entryStatusForSpec,
21
16
  featureTargetOf,
22
17
  formatProblem,
23
18
  generateFeature,
24
- generateScreen,
25
19
  importProductModule,
26
20
  intentForManifest,
27
21
  intentOfSpec,
@@ -33,9 +27,8 @@ import {
33
27
  ogygieError,
34
28
  pascalCaseOf,
35
29
  runFillCommand,
30
+ runGenerateScreenCommand,
36
31
  schemaModelsLoader,
37
- screenBodyDefault,
38
- screenBodyPath,
39
32
  screenTargetOf,
40
33
  scrubbedEnvironment,
41
34
  serializeManifest,
@@ -43,7 +36,7 @@ import {
43
36
  targetKindOf,
44
37
  throwOgygieError,
45
38
  verifyGeneration
46
- } from "../chunk-WVFHAPGC.js";
39
+ } from "../chunk-ASXSTGY5.js";
47
40
 
48
41
  // src/cli/check-command.ts
49
42
  import { existsSync, readFileSync, readdirSync } from "node:fs";
@@ -107,9 +100,9 @@ async function runCheckCommand(project, featureId, out) {
107
100
  }
108
101
 
109
102
  // 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";
103
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, readdirSync as readdirSync2, writeFileSync } from "node:fs";
104
+ import { dirname, join as join2 } from "node:path";
105
+ import { performance } from "node:perf_hooks";
113
106
 
114
107
  // ../ogygie-core/src/spec.schema.ts
115
108
  import { z } from "zod";
@@ -246,103 +239,6 @@ function compositionOfSpec(content) {
246
239
  return COMPOSITION_LINE.exec(section)?.[1] ?? null;
247
240
  }
248
241
 
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
242
  // src/cli/generate-command.ts
347
243
  function generatedFeatureIdsOf(project) {
348
244
  const kinds = [];
@@ -350,9 +246,9 @@ function generatedFeatureIdsOf(project) {
350
246
  if (feature.spec === null) continue;
351
247
  const kind = targetKindOf(project, feature.name);
352
248
  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 });
249
+ const specPath = join2(project.root, feature.spec);
250
+ if (!existsSync2(specPath)) continue;
251
+ const first = checkSpec({ markdown: readFileSync2(specPath, "utf8"), specPath: feature.spec, generation: project.generation });
356
252
  if (kind === "feature" ? first.block !== null : first.screen !== null) kinds.push({ id: feature.name, kind });
357
253
  }
358
254
  return [...kinds.filter((entry) => entry.kind === "feature"), ...kinds.filter((entry) => entry.kind === "screen")].map((entry) => entry.id);
@@ -393,7 +289,7 @@ async function runGenerateFeatureCommand(project, featureId, out) {
393
289
  return 1;
394
290
  }
395
291
  const { target } = lookup;
396
- const markdown = readFileSync3(join3(project.root, target.specPath), "utf8");
292
+ const markdown = readFileSync2(join2(project.root, target.specPath), "utf8");
397
293
  const firstPass = checkSpec({ markdown, specPath: target.specPath, generation: target.generation });
398
294
  if (firstPass.block === null) {
399
295
  for (const problem of firstPass.problems) out.error(formatProblem(problem));
@@ -412,9 +308,9 @@ async function runGenerateFeatureCommand(project, featureId, out) {
412
308
  out.error(`ogygie generate \u2014 ${report.problems.length} probl\xE8me(s), rien n'est \xE9crit.`);
413
309
  return 1;
414
310
  }
415
- const started = performance2.now();
416
- const migrationsDir = join3(project.root, target.appPath, target.generation.target.migrations);
417
- const existingMigrations = existsSync3(migrationsDir) ? readdirSync2(migrationsDir) : [];
311
+ const started = performance.now();
312
+ const migrationsDir = join2(project.root, target.appPath, target.generation.target.migrations);
313
+ const existingMigrations = existsSync2(migrationsDir) ? readdirSync2(migrationsDir) : [];
418
314
  const plan = buildFeaturePlan({
419
315
  featureId,
420
316
  folder: target.folder,
@@ -428,33 +324,33 @@ async function runGenerateFeatureCommand(project, featureId, out) {
428
324
  generatorVersion: GENERATOR_VERSION,
429
325
  behaviors: boundBehaviorsOf(report.behaviors)
430
326
  });
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;
327
+ const logicPath = join2(project.root, target.folder, "logic.ts");
328
+ const existingLogic = existsSync2(logicPath) ? readFileSync2(logicPath, "utf8") : null;
329
+ const migrationPath = join2(project.root, plan.migrationPath);
330
+ const existingMigration = existsSync2(migrationPath) ? readFileSync2(migrationPath, "utf8") : null;
435
331
  const files = generateFeature(plan, { existingLogic, existingMigration, library: loadHolesLibrary() });
436
332
  const manifestEntry = applyManifestEntry(project.rawManifest, plan, intentOfSpec(markdown), entryStatusForSpec(parseSpecDocument(markdown).frontmatter?.status));
437
333
  const specContracts = applySpecContracts(markdown, plan);
438
- const elapsedMs = Math.round(performance2.now() - started);
334
+ const elapsedMs = Math.round(performance.now() - started);
439
335
  let written = 0;
440
336
  let emitted = 0;
441
337
  for (const file of files) {
442
- const absolute = join3(project.root, file.path);
443
- const exists = existsSync3(absolute);
338
+ const absolute = join2(project.root, file.path);
339
+ const exists = existsSync2(absolute);
444
340
  emitted += file.content.split("\n").length - 1;
445
- if (exists && readFileSync3(absolute, "utf8") === file.content) {
341
+ if (exists && readFileSync2(absolute, "utf8") === file.content) {
446
342
  out.log(` identique ${file.path}`);
447
343
  continue;
448
344
  }
449
- mkdirSync2(dirname2(absolute), { recursive: true });
450
- writeFileSync2(absolute, file.content, "utf8");
345
+ mkdirSync(dirname(absolute), { recursive: true });
346
+ writeFileSync(absolute, file.content, "utf8");
451
347
  written += 1;
452
348
  out.log(` ${exists ? "r\xE9\xE9crit " : "\xE9crit "} ${file.path}`);
453
349
  }
454
350
  if (manifestEntry.outcome === "unchanged") {
455
351
  out.log(` identique ${MANIFEST_FILE} \u203A features[${featureId}].routes, dbTables`);
456
352
  } else {
457
- writeFileSync2(join3(project.root, MANIFEST_FILE), serializeManifest(manifestEntry.manifest), "utf8");
353
+ writeFileSync(join2(project.root, MANIFEST_FILE), serializeManifest(manifestEntry.manifest), "utf8");
458
354
  out.log(
459
355
  ` ${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
356
  );
@@ -467,7 +363,7 @@ async function runGenerateFeatureCommand(project, featureId, out) {
467
363
  out.log(` identique ${target.specPath} \u203A bloc @generated de # Contrats de donn\xE9es`);
468
364
  } else {
469
365
  const addedLines = specContracts.markdown.split("\n").length - markdown.split("\n").length;
470
- writeFileSync2(join3(project.root, target.specPath), specContracts.markdown, "utf8");
366
+ writeFileSync(join2(project.root, target.specPath), specContracts.markdown, "utf8");
471
367
  out.log(
472
368
  ` ${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
369
  );
@@ -480,8 +376,8 @@ async function runGenerateFeatureCommand(project, featureId, out) {
480
376
 
481
377
  // src/cli/preview-command.ts
482
378
  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";
379
+ import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync3 } from "node:fs";
380
+ import { join as join3 } from "node:path";
485
381
  var PREVIEWS_ROOT = "previews";
486
382
  function previewsDirOf(featureId, composition) {
487
383
  return composition === null ? `${PREVIEWS_ROOT}/${featureId}` : `${PREVIEWS_ROOT}/${featureId}/${composition}`;
@@ -512,29 +408,29 @@ async function runPreviewCommand(project, featureId, out, runner = vitestPreview
512
408
  return 0;
513
409
  }
514
410
  const testPath = `${target.folder}/${pascalCaseOf(featureId)}Screen${PREVIEWS_FILE_SUFFIX}`;
515
- if (!existsSync4(join4(project.root, testPath))) {
411
+ if (!existsSync3(join3(project.root, testPath))) {
516
412
  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
413
  return 1;
518
414
  }
519
- const specAbsolute = join4(project.root, target.specPath);
520
- const composition = existsSync4(specAbsolute) ? compositionOfSpec(readFileSync4(specAbsolute, "utf8")) : null;
415
+ const specAbsolute = join3(project.root, target.specPath);
416
+ const composition = existsSync3(specAbsolute) ? compositionOfSpec(readFileSync3(specAbsolute, "utf8")) : null;
521
417
  const relativeDir = previewsDirOf(featureId, composition);
522
- const outDir = join4(project.root, relativeDir);
418
+ const outDir = join3(project.root, relativeDir);
523
419
  const run = runner({ projectRoot: project.root, testPath, outDir });
524
420
  for (const line of run.lines) out.log(` ${line}`);
525
421
  if (run.code !== 0) {
526
422
  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
423
  return run.code;
528
424
  }
529
- const pages = existsSync4(outDir) ? readdirSync3(outDir).filter((name) => name.endsWith(".html")).sort() : [];
425
+ const pages = existsSync3(outDir) ? readdirSync3(outDir).filter((name) => name.endsWith(".html")).sort() : [];
530
426
  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
427
  return pages.length === 0 ? 1 : 0;
532
428
  }
533
429
 
534
430
  // src/cli/schemas-command.ts
535
431
  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";
432
+ import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
433
+ import { dirname as dirname2, join as join4 } from "node:path";
538
434
  import { pathToFileURL } from "node:url";
539
435
 
540
436
  // src/schemas/draft.ts
@@ -753,9 +649,9 @@ function featureFolderFor(project, featureId) {
753
649
  return `${common}/${featureId}`;
754
650
  }
755
651
  function availableGateScripts(projectRoot) {
756
- const path = join5(projectRoot, "package.json");
757
- if (!existsSync5(path)) return [];
758
- const scripts = JSON.parse(readFileSync5(path, "utf8")).scripts ?? {};
652
+ const path = join4(projectRoot, "package.json");
653
+ if (!existsSync4(path)) return [];
654
+ const scripts = JSON.parse(readFileSync4(path, "utf8")).scripts ?? {};
759
655
  return SCHEMAS_GATE_SCRIPTS.filter((script) => script in scripts);
760
656
  }
761
657
  async function runSchemasCommand(project, featureId, argv, out, deps = {}) {
@@ -765,8 +661,8 @@ async function runSchemasCommand(project, featureId, argv, out, deps = {}) {
765
661
  }
766
662
  const onMap = project.manifest.features.find((feature) => feature.name === featureId);
767
663
  const specPath = onMap?.spec ?? `${project.specsFolder.replace(/\/+$/u, "")}/${featureId}.spec.md`;
768
- const specAbsolute = join5(project.root, specPath);
769
- if (!existsSync5(specAbsolute)) {
664
+ const specAbsolute = join4(project.root, specPath);
665
+ if (!existsSync4(specAbsolute)) {
770
666
  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
667
  return 1;
772
668
  }
@@ -774,7 +670,7 @@ async function runSchemasCommand(project, featureId, argv, out, deps = {}) {
774
670
  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
671
  return 1;
776
672
  }
777
- const markdown = readFileSync5(specAbsolute, "utf8");
673
+ const markdown = readFileSync4(specAbsolute, "utf8");
778
674
  const first = checkSpec({ markdown, specPath, generation: project.generation });
779
675
  if (first.kind !== "feature" || first.block === null) {
780
676
  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.`);
@@ -783,8 +679,8 @@ async function runSchemasCommand(project, featureId, argv, out, deps = {}) {
783
679
  const block = first.block;
784
680
  const folder = onMap === void 0 ? `${project.apiApp.path}/${featureFolderFor(project, featureId)}` : `${project.apiApp.path}/${onMap.folder}`;
785
681
  const modulePath = `${folder}/${block.contracts.module.replace(/^\.\//u, "")}`;
786
- const moduleAbsolute = join5(project.root, modulePath);
787
- if (existsSync5(moduleAbsolute)) {
682
+ const moduleAbsolute = join4(project.root, modulePath);
683
+ if (existsSync4(moduleAbsolute)) {
788
684
  out.log(`ogygie generate schemas \u2014 ${modulePath} existe : il appartient au repo, jamais r\xE9g\xE9n\xE9r\xE9 (D52).`);
789
685
  return 0;
790
686
  }
@@ -810,8 +706,8 @@ async function runSchemasCommand(project, featureId, argv, out, deps = {}) {
810
706
  return { source: extractModuleSource(answer.text), model: answer.model, usage: answer.usage };
811
707
  },
812
708
  async judge(source, attempt) {
813
- mkdirSync3(dirname3(moduleAbsolute), { recursive: true });
814
- writeFileSync3(moduleAbsolute, source, "utf8");
709
+ mkdirSync2(dirname2(moduleAbsolute), { recursive: true });
710
+ writeFileSync2(moduleAbsolute, source, "utf8");
815
711
  let moduleExports;
816
712
  try {
817
713
  moduleExports = await importProductModule(`${pathToFileURL(moduleAbsolute).href}?attempt=${attempt}`);
@@ -872,7 +768,7 @@ ${rows.join("\n")}
872
768
  raw["features"] = features;
873
769
  const status = entryStatusForSpec(parseSpecDocument(markdown).frontmatter?.status);
874
770
  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");
771
+ writeFileSync2(join4(project.root, MANIFEST_FILE), serializeManifest(raw), "utf8");
876
772
  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
773
  }
878
774
  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).`);
@@ -880,11 +776,11 @@ ${rows.join("\n")}
880
776
  }
881
777
 
882
778
  // src/cli/verify-command.ts
883
- import { performance as performance3 } from "node:perf_hooks";
779
+ import { performance as performance2 } from "node:perf_hooks";
884
780
 
885
781
  // ../ogygie-core/src/repo-adapter.ts
886
782
  import { readFile as readFileFromDisk, readdir, realpath, stat } from "node:fs/promises";
887
- import { isAbsolute, join as join6, resolve, sep } from "node:path";
783
+ import { isAbsolute, join as join5, resolve, sep } from "node:path";
888
784
  import { z as z2 } from "zod";
889
785
  var RepoFileEntrySchema = z2.object({
890
786
  /** Path relative to the LISTED directory, "/" separated, never absolute. */
@@ -946,16 +842,16 @@ async function listFilesRecursively(absoluteDir, prefix) {
946
842
  const relativePath = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
947
843
  if (entry.isDirectory()) {
948
844
  if (isSkippedDirectory(entry.name)) continue;
949
- files.push(...await listFilesRecursively(join6(absoluteDir, entry.name), relativePath));
845
+ files.push(...await listFilesRecursively(join5(absoluteDir, entry.name), relativePath));
950
846
  } else if (entry.isFile()) {
951
- const stats = await stat(join6(absoluteDir, entry.name));
847
+ const stats = await stat(join5(absoluteDir, entry.name));
952
848
  files.push({ path: relativePath, sizeBytes: stats.size });
953
849
  }
954
850
  }
955
851
  return files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
956
852
  }
957
853
  async function resolveGitDirs(rootDir) {
958
- const dotGit = join6(rootDir, ".git");
854
+ const dotGit = join5(rootDir, ".git");
959
855
  let gitDir;
960
856
  try {
961
857
  const stats = await stat(dotGit);
@@ -971,7 +867,7 @@ async function resolveGitDirs(rootDir) {
971
867
  return null;
972
868
  }
973
869
  try {
974
- const commonPointer = (await readFileFromDisk(join6(gitDir, "commondir"), "utf8")).trim();
870
+ const commonPointer = (await readFileFromDisk(join5(gitDir, "commondir"), "utf8")).trim();
975
871
  return { gitDir, commonDir: isAbsolute(commonPointer) ? commonPointer : resolve(gitDir, commonPointer) };
976
872
  } catch {
977
873
  return { gitDir, commonDir: gitDir };
@@ -980,13 +876,13 @@ async function resolveGitDirs(rootDir) {
980
876
  async function readRefSha(dirs, refName) {
981
877
  for (const base of [dirs.gitDir, dirs.commonDir]) {
982
878
  try {
983
- const sha = (await readFileFromDisk(join6(base, refName), "utf8")).trim();
879
+ const sha = (await readFileFromDisk(join5(base, refName), "utf8")).trim();
984
880
  if (sha !== "") return sha;
985
881
  } catch {
986
882
  }
987
883
  }
988
884
  try {
989
- const packed = await readFileFromDisk(join6(dirs.commonDir, "packed-refs"), "utf8");
885
+ const packed = await readFileFromDisk(join5(dirs.commonDir, "packed-refs"), "utf8");
990
886
  for (const line of packed.split("\n")) {
991
887
  if (line.startsWith("#") || line.startsWith("^")) continue;
992
888
  const [sha, name] = line.trim().split(" ");
@@ -1034,7 +930,7 @@ function createFilesystemRepoAdapter(rootDir) {
1034
930
  if (dirs === null) return null;
1035
931
  let head;
1036
932
  try {
1037
- head = (await readFileFromDisk(join6(dirs.gitDir, "HEAD"), "utf8")).trim();
933
+ head = (await readFileFromDisk(join5(dirs.gitDir, "HEAD"), "utf8")).trim();
1038
934
  } catch {
1039
935
  return null;
1040
936
  }
@@ -1046,7 +942,7 @@ function createFilesystemRepoAdapter(rootDir) {
1046
942
 
1047
943
  // src/cli/verify-command.ts
1048
944
  async function runVerifyCommand(project, out) {
1049
- const started = performance3.now();
945
+ const started = performance2.now();
1050
946
  const report = await verifyGeneration({
1051
947
  repo: createFilesystemRepoAdapter(project.root),
1052
948
  specsFolder: project.specsFolder,
@@ -1059,7 +955,7 @@ async function runVerifyCommand(project, out) {
1059
955
  library: loadHolesLibrary(),
1060
956
  loadConsumed: (block) => loadConsumedFeatures(project, block)
1061
957
  });
1062
- const elapsedMs = Math.round(performance3.now() - started);
958
+ const elapsedMs = Math.round(performance2.now() - started);
1063
959
  if (!report.ok) {
1064
960
  for (const problem of report.problems) out.error(formatProblem(problem));
1065
961
  out.error(`
@@ -7,4 +7,6 @@ export declare function customViewPropsLines(view: CustomViewPlan): string[];
7
7
  export declare function customViewSignature(view: CustomViewPlan): string;
8
8
  /** The imports a custom view's body needs — written between `// @hole-imports` markers, kept verbatim at regeneration: a body composes patterns and hooks the generator cannot foresee (astorg-ai-pdlc, lot 4 M3). */
9
9
  export declare function customImportsLines(view: CustomViewPlan, existing: string | null): string[];
10
+ /** The visible placeholder of a view nobody wrote yet — what `fill` looks for, and puts back when every candidate is red (D53). */
11
+ export declare function customViewPlaceholderLine(holeId: string): string;
10
12
  export declare function emitCustomView(plan: ScreenPlan, view: CustomViewPlan, existing: string | null): string;
@@ -22,5 +22,5 @@ export interface BodyBan {
22
22
  rule: string;
23
23
  }
24
24
  export declare const BODY_BANS: readonly BodyBan[];
25
- /** The rules a body breaks — empty when it may be tested. */
26
- export declare function forbiddenInBody(body: string): string[];
25
+ /** The rules a body breaks — empty when it may be tested; a view hands its own bans (fill/view.ts). */
26
+ export declare function forbiddenInBody(body: string, bans?: readonly BodyBan[]): string[];
@@ -9,6 +9,13 @@
9
9
  * proven in memory and the real effects (vitest in a subprocess, the Messages
10
10
  * client of the core) stay in the command.
11
11
  *
12
+ * The decision is the same for the two kinds of hole the framework has — a
13
+ * logic hole of D23 in `logic.ts`, and the body of a custom view of D53 in
14
+ * `views/<Vue>.tsx` — and what differs between them is a set of RULES handed
15
+ * with the input: what an unfilled body looks like, what a candidate may not
16
+ * carry, what the library offers for this hole, and how a retained body is
17
+ * recorded. `logicHoleRules` is the first set; `fill/view.ts` the second.
18
+ *
12
19
  * The threshold of D24 is counted, not enforced: a feature whose holes reach
13
20
  * escalation more than one in ten is said to have holes too big for a model —
14
21
  * the spec splits them, nobody raises a model.
@@ -53,14 +60,28 @@ export interface FillDeps {
53
60
  repo: string;
54
61
  log(line: string): void;
55
62
  }
63
+ /** What distinguishes one kind of hole from another under the same decision (D24). */
64
+ export interface HoleFillRules {
65
+ /** Whether a body is still the generator's — `todo(...)` for a logic hole, the placeholder for a view. */
66
+ isUnfilled(body: string): boolean;
67
+ /** The rules a candidate breaks before any test — empty when it may be tested. */
68
+ bans(body: string): string[];
69
+ /** What the library offers for this hole, in order — the same signature first, then the same shape — at most five. */
70
+ libraryCandidates(): Candidate[];
71
+ /** The entry a retained body makes, with its provenance. */
72
+ libraryEntryOf(body: string, provenance: HoleProvenance): HoleLibraryEntry;
73
+ }
74
+ /** The rules of a logic hole of D23: `todo` is unfilled, the bans of a pure body, the library by signature then by shape with the template instantiated. */
75
+ export declare function logicHoleRules(hole: HolePlan, plan: FeaturePlan, library: readonly HoleLibraryEntry[]): HoleFillRules;
56
76
  export interface HoleFillInput {
57
- hole: HolePlan;
58
- plan: FeaturePlan;
77
+ holeId: string;
78
+ /** The feature (or screen) the hole belongs to — the provenance names it. */
79
+ featureId: string;
59
80
  /** `holes/<id>.gen.md`, the one context a model gets. */
60
81
  context: string;
61
- /** What logic.ts holds for this hole now. */
82
+ /** What the file holds for this hole now. */
62
83
  currentBody: string;
63
- library: readonly HoleLibraryEntry[];
84
+ rules: HoleFillRules;
64
85
  }
65
86
  export interface HoleFillResult {
66
87
  holeId: string;
@@ -78,7 +99,7 @@ export interface HoleFillResult {
78
99
  /** The runner's last lines when nothing was green — what a human reads first. */
79
100
  lastRun: string[];
80
101
  }
81
- /** Fills ONE hole along D24's order; the body ends written (green) or back to `todo`. */
102
+ /** Fills ONE hole along D24's order; the body ends written (green) or back to the generator's. */
82
103
  export declare function fillHole(input: HoleFillInput, deps: FillDeps, options?: FillOptions): Promise<HoleFillResult>;
83
104
  export interface FeatureFillSummary {
84
105
  featureId: string;
@@ -93,5 +114,5 @@ export interface FeatureFillSummary {
93
114
  model: string;
94
115
  }[];
95
116
  }
96
- /** Fills every `todo` hole of a feature, one after the other (one logic.ts, one test file). */
117
+ /** Fills every unfilled hole of a feature, one after the other (one file written at a time, one test file). */
97
118
  export declare function fillFeature(inputs: readonly HoleFillInput[], deps: FillDeps, options?: FillOptions): Promise<FeatureFillSummary>;
@@ -5,7 +5,9 @@
5
5
  * package's single configuration (`models: { spec, escalate, draw }`), with
6
6
  * D24's defaults, overridable by the command's flags; `draw` may name a local
7
7
  * Ollama model behind an OpenAI-compatible base, which is the operator's
8
- * call, not this module's.
8
+ * call, not this module's. Two kinds of hole, two sets of instructions and
9
+ * two extractions: the body of a logic hole (D23), the imports and body of a
10
+ * custom view (D53, `fill/view.ts`).
9
11
  */
10
12
  import type { ModelClient } from "@ogygie/core/model";
11
13
  import type { Candidate } from "./fill.js";
@@ -18,8 +20,18 @@ export interface FillModels {
18
20
  export declare const DEFAULT_FILL_MODELS: FillModels;
19
21
  /** The stable instructions — cached by the model side; the hole's context is the user turn. */
20
22
  export declare const HOLE_SYSTEM_PROMPT: string;
23
+ /** The instructions for the body of a custom view (D53): the same contract-only diet, a closed catalogue, the tokens. */
24
+ export declare const VIEW_SYSTEM_PROMPT: string;
21
25
  export interface ModelDrawer {
22
26
  draw(context: string, n: number, round: number): Promise<Candidate[]>;
23
27
  escalate(context: string): Promise<Candidate>;
24
28
  }
25
- export declare function createModelDrawer(client: ModelClient, models?: FillModels): ModelDrawer;
29
+ export interface ModelDrawerOptions {
30
+ /** The stable instructions of this kind of hole — a logic body by default. */
31
+ system?: string;
32
+ /** How a candidate is read out of an answer — a logic body by default. */
33
+ extract?: (answer: string) => string;
34
+ }
35
+ /** The drawer of a logic hole's body (D23), or of a custom view's body when given `VIEW_DRAWER_OPTIONS` (D53). */
36
+ export declare const VIEW_DRAWER_OPTIONS: ModelDrawerOptions;
37
+ export declare function createModelDrawer(client: ModelClient, models?: FillModels, options?: ModelDrawerOptions): ModelDrawer;