@ogygie/generate 0.20.0 → 0.21.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.
- package/dist/{chunk-WVFHAPGC.js → chunk-ASXSTGY5.js} +471 -112
- package/dist/cli/fill-command.d.ts +14 -2
- package/dist/cli/generate-screen-command.d.ts +11 -2
- package/dist/cli/run.js +55 -159
- package/dist/emit/screen/views.d.ts +2 -0
- package/dist/fill/body.d.ts +2 -2
- package/dist/fill/fill.d.ts +27 -6
- package/dist/fill/model-draw.d.ts +14 -2
- package/dist/fill/view.d.ts +55 -0
- package/dist/hole-shape.d.ts +2 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +43 -1
- package/holes-library/README.md +7 -1
- package/package.json +3 -3
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
111
|
-
import { dirname
|
|
112
|
-
import { performance
|
|
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 =
|
|
354
|
-
if (!
|
|
355
|
-
const first = checkSpec({ markdown:
|
|
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 =
|
|
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 =
|
|
416
|
-
const migrationsDir =
|
|
417
|
-
const existingMigrations =
|
|
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 =
|
|
432
|
-
const existingLogic =
|
|
433
|
-
const migrationPath =
|
|
434
|
-
const existingMigration =
|
|
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(
|
|
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 =
|
|
443
|
-
const exists =
|
|
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 &&
|
|
341
|
+
if (exists && readFileSync2(absolute, "utf8") === file.content) {
|
|
446
342
|
out.log(` identique ${file.path}`);
|
|
447
343
|
continue;
|
|
448
344
|
}
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
484
|
-
import { join as
|
|
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 (!
|
|
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 =
|
|
520
|
-
const composition =
|
|
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 =
|
|
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 =
|
|
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
|
|
537
|
-
import { dirname as
|
|
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 =
|
|
757
|
-
if (!
|
|
758
|
-
const scripts = JSON.parse(
|
|
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 =
|
|
769
|
-
if (!
|
|
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 =
|
|
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 =
|
|
787
|
-
if (
|
|
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
|
-
|
|
814
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
845
|
+
files.push(...await listFilesRecursively(join5(absoluteDir, entry.name), relativePath));
|
|
950
846
|
} else if (entry.isFile()) {
|
|
951
|
-
const stats = await stat(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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;
|
package/dist/fill/body.d.ts
CHANGED
|
@@ -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[];
|
package/dist/fill/fill.d.ts
CHANGED
|
@@ -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
|
-
|
|
58
|
-
|
|
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
|
|
82
|
+
/** What the file holds for this hole now. */
|
|
62
83
|
currentBody: string;
|
|
63
|
-
|
|
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
|
|
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
|
|
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
|
|
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;
|