@ogygie/generate 0.19.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
|
@@ -3994,14 +3994,14 @@ function normalizedTypeOf(model) {
|
|
|
3994
3994
|
return `${base}${model.nullable ? "?" : ""}`;
|
|
3995
3995
|
}
|
|
3996
3996
|
var HoleShapeSchema = z6.strictObject({
|
|
3997
|
-
kind: z6.enum(["validate", "transform", "derive", "project", "view"]),
|
|
3997
|
+
kind: z6.enum(["validate", "transform", "derive", "project", "view", "custom-view"]),
|
|
3998
3998
|
/** Normalized types of the fields read, by position. */
|
|
3999
3999
|
reads: z6.array(z6.string()),
|
|
4000
4000
|
/** Transform only: normalized types of the columns, by position. */
|
|
4001
4001
|
columns: z6.array(z6.string()),
|
|
4002
4002
|
/** What the code the hole answers IS for the operation (`none` when the hole never refuses). */
|
|
4003
4003
|
codeRole: z6.enum(["operation-refusal", "motif-rejection", "none"]),
|
|
4004
|
-
/** `void` for a validate hole, `rows:<columns>` for a transform, the written type for a derive, `output` for a project, `view:<shape>` for a view. */
|
|
4004
|
+
/** `void` for a validate hole, `rows:<columns>` for a transform, the written type for a derive, `output` for a project, `view:<shape>` for a view, `jsx` for the body of a custom view (D53). */
|
|
4005
4005
|
returns: z6.string()
|
|
4006
4006
|
});
|
|
4007
4007
|
var HoleProvenanceSchema = z6.strictObject({
|
|
@@ -8928,10 +8928,13 @@ function customImportsLines(view, existing) {
|
|
|
8928
8928
|
}
|
|
8929
8929
|
return [`// @hole-imports ${view.hole.id}`, ...kept, `// @end-hole-imports ${view.hole.id}`];
|
|
8930
8930
|
}
|
|
8931
|
+
function customViewPlaceholderLine(holeId) {
|
|
8932
|
+
return ` return <p data-pattern="HoleUnfilled">{${quote(holeId)}}</p>;`;
|
|
8933
|
+
}
|
|
8931
8934
|
function customBodyLines(view, existing) {
|
|
8932
8935
|
const known = existing.get(view.hole.id);
|
|
8933
8936
|
if (known !== void 0 && known.signature === customViewSignature(view) && known.body.trim() !== "") return known.body.split("\n");
|
|
8934
|
-
return [
|
|
8937
|
+
return [customViewPlaceholderLine(view.hole.id)];
|
|
8935
8938
|
}
|
|
8936
8939
|
function emitCustomView(plan, view, existing) {
|
|
8937
8940
|
const sections = existing === null ? /* @__PURE__ */ new Map() : parseLogicSections(existing);
|
|
@@ -8997,7 +9000,8 @@ function emitScreenHoleDoc(plan, view) {
|
|
|
8997
9000
|
"",
|
|
8998
9001
|
"- Le corps, et les imports entre `// @hole-imports` et `// @end-hole-imports` (patterns, hooks de la feature), sont les seules parties du fichier qu'on \xE9crit ; le reste est r\xE9g\xE9n\xE9r\xE9 et compar\xE9.",
|
|
8999
9002
|
"- Une \xE9criture passe par une action re\xE7ue (`MutationLike.mutate`), jamais par un hook ni une URL.",
|
|
9000
|
-
"- Un placeholder `HoleUnfilled` reste visible tant que le corps n'est pas \xE9crit : le test de la vue est rouge jusque-l\xE0."
|
|
9003
|
+
"- Un placeholder `HoleUnfilled` reste visible tant que le corps n'est pas \xE9crit : le test de la vue est rouge jusque-l\xE0.",
|
|
9004
|
+
"- Un corps tir\xE9 par un mod\xE8le (`ogygie generate fill`) n'importe que depuis `react`, `@lingui/react/macro`, `@ogygie/ui` et ses entr\xE9es, le dossier `patterns/` du produit, une vue voisine ou `../queries.gen` ; ses classes viennent des tokens, jamais d'une valeur arbitraire ni d'une couleur en dur ; il est jug\xE9 par le test d'exemple ci-dessus puis par le rendu statique de l'\xE9tat nominal."
|
|
9001
9005
|
);
|
|
9002
9006
|
}
|
|
9003
9007
|
|
|
@@ -9849,20 +9853,20 @@ async function verifyGeneration(input) {
|
|
|
9849
9853
|
const specPath = `${specsFolder}/${entry.path}`;
|
|
9850
9854
|
const markdown = await input.repo.readFile(specPath);
|
|
9851
9855
|
const first = checkSpec({ markdown, specPath, guards, generation: input.generation });
|
|
9852
|
-
let
|
|
9856
|
+
let report2 = first;
|
|
9853
9857
|
const owner = ownerOf.get(specPath);
|
|
9854
9858
|
const folder = owner?.folder;
|
|
9855
9859
|
if (first.kind === "screen" && first.screen !== null && input.loadConsumed !== void 0) {
|
|
9856
9860
|
const consumed = await input.loadConsumed(first.screen);
|
|
9857
|
-
|
|
9858
|
-
if (
|
|
9861
|
+
report2 = checkSpec({ markdown, specPath, guards, generation: input.generation, consumed });
|
|
9862
|
+
if (report2.ok && report2.screen !== null && owner !== void 0 && folder !== void 0 && owner.appType === "front" && owner.appPath !== void 0 && input.generation !== null) {
|
|
9859
9863
|
const regenerated = await regenerateScreenAndCompare(input, {
|
|
9860
9864
|
specPath,
|
|
9861
9865
|
folder,
|
|
9862
9866
|
appPath: owner.appPath,
|
|
9863
9867
|
markdown,
|
|
9864
|
-
block:
|
|
9865
|
-
behaviors:
|
|
9868
|
+
block: report2.screen,
|
|
9869
|
+
behaviors: report2.behaviors,
|
|
9866
9870
|
consumed,
|
|
9867
9871
|
generation: input.generation
|
|
9868
9872
|
});
|
|
@@ -9884,15 +9888,15 @@ async function verifyGeneration(input) {
|
|
|
9884
9888
|
})
|
|
9885
9889
|
);
|
|
9886
9890
|
} else {
|
|
9887
|
-
|
|
9888
|
-
if (
|
|
9891
|
+
report2 = checkSpec({ markdown, specPath, guards, generation: input.generation, schemas });
|
|
9892
|
+
if (report2.ok && report2.block !== null && input.generation !== null && input.apiAppPath !== void 0 && (input.tenantColumn !== void 0 || input.generation.target.tenancy === "none")) {
|
|
9889
9893
|
const regenerated = await regenerateAndCompare(input, {
|
|
9890
9894
|
specPath,
|
|
9891
9895
|
folder,
|
|
9892
9896
|
markdown,
|
|
9893
9897
|
owner,
|
|
9894
|
-
behaviors:
|
|
9895
|
-
block:
|
|
9898
|
+
behaviors: report2.behaviors,
|
|
9899
|
+
block: report2.block,
|
|
9896
9900
|
schemas,
|
|
9897
9901
|
generation: input.generation,
|
|
9898
9902
|
apiAppPath: input.apiAppPath,
|
|
@@ -9903,8 +9907,8 @@ async function verifyGeneration(input) {
|
|
|
9903
9907
|
}
|
|
9904
9908
|
}
|
|
9905
9909
|
}
|
|
9906
|
-
specs.push({ specPath, generated:
|
|
9907
|
-
problems.push(...
|
|
9910
|
+
specs.push({ specPath, generated: report2.generated, tally: report2.tally });
|
|
9911
|
+
problems.push(...report2.problems);
|
|
9908
9912
|
}
|
|
9909
9913
|
const generatedFiles = [];
|
|
9910
9914
|
for (const directory of input.scanDirs) {
|
|
@@ -9991,24 +9995,38 @@ var BODY_BANS = [
|
|
|
9991
9995
|
{ pattern: /\beval\s*\(|\bnew Function\s*\(/u, rule: "aucune \xE9valuation dynamique" },
|
|
9992
9996
|
{ pattern: /\bnew Date\s*\(\s*\)|\bDate\.now\s*\(/u, rule: "l'horloge est un effet d\xE9clar\xE9 (clock), jamais lue \xE0 nu" }
|
|
9993
9997
|
];
|
|
9994
|
-
function forbiddenInBody(body) {
|
|
9998
|
+
function forbiddenInBody(body, bans = BODY_BANS) {
|
|
9995
9999
|
const code = body.replace(/\/\*[\s\S]*?\*\//gu, "").replace(/(^|[^:])\/\/.*$/gmu, "$1");
|
|
9996
|
-
return
|
|
10000
|
+
return bans.filter((ban) => ban.pattern.test(code)).map((ban) => ban.rule);
|
|
9997
10001
|
}
|
|
9998
10002
|
|
|
9999
10003
|
// src/fill/fill.ts
|
|
10000
10004
|
var DEFAULT_FILL_OPTIONS = { candidates: 5, rounds: 2, libraryOnly: false };
|
|
10001
|
-
|
|
10005
|
+
function logicHoleRules(hole, plan, library) {
|
|
10006
|
+
const signatureHash = signatureHashOf(signatureTextOf(hole, plan));
|
|
10007
|
+
const shapeHash = shapeHashOf(shapeOfHole(hole, plan));
|
|
10008
|
+
return {
|
|
10009
|
+
isUnfilled: (body) => isTodoBody(body, hole.id),
|
|
10010
|
+
bans: (body) => forbiddenInBody(body),
|
|
10011
|
+
libraryCandidates() {
|
|
10012
|
+
const exact = library.filter((entry) => entry.signatureHash === signatureHash);
|
|
10013
|
+
const neighbours = neighboursOfShape(shapeHash, library, { hole: hole.id, feature: plan.featureId }).filter((entry) => !exact.includes(entry));
|
|
10014
|
+
return [...exact, ...neighbours].slice(0, 5).map((entry) => ({ body: instantiateBody(entry.bodyTemplate, hole), origin: `library:${entry.signatureHash}`, model: null, usage: null }));
|
|
10015
|
+
},
|
|
10016
|
+
libraryEntryOf: (body, provenance) => libraryEntryOf(hole, plan, body, provenance)
|
|
10017
|
+
};
|
|
10018
|
+
}
|
|
10019
|
+
async function tryCandidate(deps, input, candidate, result) {
|
|
10002
10020
|
result.tried += 1;
|
|
10003
|
-
const bans =
|
|
10021
|
+
const bans = input.rules.bans(candidate.body);
|
|
10004
10022
|
if (bans.length > 0) {
|
|
10005
10023
|
result.banned += 1;
|
|
10006
|
-
deps.log(JSON.stringify({ action: "fill", hole: holeId, candidate: candidate.origin, outcome: "banned", rules: bans }));
|
|
10024
|
+
deps.log(JSON.stringify({ action: "fill", hole: input.holeId, candidate: candidate.origin, outcome: "banned", rules: bans }));
|
|
10007
10025
|
return false;
|
|
10008
10026
|
}
|
|
10009
|
-
await deps.writeBody(holeId, candidate.body);
|
|
10010
|
-
const run = await deps.runHoleTest(holeId);
|
|
10011
|
-
deps.log(JSON.stringify({ action: "fill", hole: holeId, candidate: candidate.origin, outcome: run.green ? "green" : "red" }));
|
|
10027
|
+
await deps.writeBody(input.holeId, candidate.body);
|
|
10028
|
+
const run = await deps.runHoleTest(input.holeId);
|
|
10029
|
+
deps.log(JSON.stringify({ action: "fill", hole: input.holeId, candidate: candidate.origin, outcome: run.green ? "green" : "red" }));
|
|
10012
10030
|
if (!run.green) result.lastRun = run.lines;
|
|
10013
10031
|
return run.green;
|
|
10014
10032
|
}
|
|
@@ -10018,39 +10036,33 @@ function accountUsage(result, candidates) {
|
|
|
10018
10036
|
}
|
|
10019
10037
|
}
|
|
10020
10038
|
async function fillHole(input, deps, options = DEFAULT_FILL_OPTIONS) {
|
|
10021
|
-
const {
|
|
10022
|
-
|
|
10023
|
-
if (!isTodoBody(input.currentBody, hole.id)) {
|
|
10039
|
+
const result = { holeId: input.holeId, outcome: "human", tried: 0, banned: 0, model: null, usage: [], lastRun: [] };
|
|
10040
|
+
if (!input.rules.isUnfilled(input.currentBody)) {
|
|
10024
10041
|
return { ...result, outcome: "already-filled" };
|
|
10025
10042
|
}
|
|
10026
10043
|
const retain = async (candidate, path) => {
|
|
10027
|
-
const provenance = { hole:
|
|
10028
|
-
await deps.recordLibrary(libraryEntryOf(
|
|
10044
|
+
const provenance = { hole: input.holeId, repo: deps.repo, feature: input.featureId, path, model: candidate.model, date: deps.today() };
|
|
10045
|
+
await deps.recordLibrary(input.rules.libraryEntryOf(candidate.body, provenance));
|
|
10029
10046
|
return { ...result, outcome: path, model: candidate.model, lastRun: [] };
|
|
10030
10047
|
};
|
|
10031
|
-
const
|
|
10032
|
-
|
|
10033
|
-
const exact = input.library.filter((entry) => entry.signatureHash === signatureHash);
|
|
10034
|
-
const neighbours = neighboursOfShape(shapeHash, input.library, { hole: hole.id, feature: plan.featureId }).filter((entry) => !exact.includes(entry));
|
|
10035
|
-
for (const entry of [...exact, ...neighbours].slice(0, 5)) {
|
|
10036
|
-
const candidate = { body: instantiateBody(entry.bodyTemplate, hole), origin: `library:${entry.signatureHash}`, model: null, usage: null };
|
|
10037
|
-
if (await tryCandidate(deps, hole.id, candidate, result)) return retain(candidate, "library");
|
|
10048
|
+
for (const candidate of input.rules.libraryCandidates()) {
|
|
10049
|
+
if (await tryCandidate(deps, input, candidate, result)) return retain(candidate, "library");
|
|
10038
10050
|
}
|
|
10039
10051
|
if (options.libraryOnly) {
|
|
10040
|
-
await deps.writeBody(
|
|
10052
|
+
await deps.writeBody(input.holeId, null);
|
|
10041
10053
|
return result;
|
|
10042
10054
|
}
|
|
10043
10055
|
for (let round = 1; round <= options.rounds; round += 1) {
|
|
10044
10056
|
const candidates = await deps.draw(input.context, options.candidates, round);
|
|
10045
10057
|
accountUsage(result, candidates);
|
|
10046
10058
|
for (const candidate of candidates) {
|
|
10047
|
-
if (await tryCandidate(deps,
|
|
10059
|
+
if (await tryCandidate(deps, input, candidate, result)) return retain(candidate, round === 1 ? "draw:1" : "draw:2");
|
|
10048
10060
|
}
|
|
10049
10061
|
}
|
|
10050
10062
|
const escalated = await deps.escalate(input.context);
|
|
10051
10063
|
accountUsage(result, [escalated]);
|
|
10052
|
-
if (await tryCandidate(deps,
|
|
10053
|
-
await deps.writeBody(
|
|
10064
|
+
if (await tryCandidate(deps, input, escalated, result)) return retain(escalated, "escalate");
|
|
10065
|
+
await deps.writeBody(input.holeId, null);
|
|
10054
10066
|
return result;
|
|
10055
10067
|
}
|
|
10056
10068
|
async function fillFeature(inputs, deps, options = DEFAULT_FILL_OPTIONS) {
|
|
@@ -10059,7 +10071,7 @@ async function fillFeature(inputs, deps, options = DEFAULT_FILL_OPTIONS) {
|
|
|
10059
10071
|
const attempted = results.filter((result) => result.outcome !== "already-filled");
|
|
10060
10072
|
const escalated = attempted.filter((result) => result.outcome === "escalate" || result.outcome === "human").length;
|
|
10061
10073
|
return {
|
|
10062
|
-
featureId: inputs[0]?.
|
|
10074
|
+
featureId: inputs[0]?.featureId ?? "",
|
|
10063
10075
|
results,
|
|
10064
10076
|
filled: attempted.filter((result) => result.outcome !== "human").length,
|
|
10065
10077
|
unfilled: attempted.filter((result) => result.outcome === "human").length,
|
|
@@ -10068,6 +10080,124 @@ async function fillFeature(inputs, deps, options = DEFAULT_FILL_OPTIONS) {
|
|
|
10068
10080
|
};
|
|
10069
10081
|
}
|
|
10070
10082
|
|
|
10083
|
+
// src/fill/view.ts
|
|
10084
|
+
var VIEW_PLACEHOLDER = 'data-pattern="HoleUnfilled"';
|
|
10085
|
+
function isUnfilledViewBody(body) {
|
|
10086
|
+
return body.trim() === "" || body.includes(VIEW_PLACEHOLDER);
|
|
10087
|
+
}
|
|
10088
|
+
function splitViewCandidate(text) {
|
|
10089
|
+
const lines = text.replace(/\r\n/gu, "\n").split("\n");
|
|
10090
|
+
const imports = [];
|
|
10091
|
+
let index = 0;
|
|
10092
|
+
while (index < lines.length) {
|
|
10093
|
+
const line = (lines[index] ?? "").trim();
|
|
10094
|
+
if (line === "") {
|
|
10095
|
+
index += 1;
|
|
10096
|
+
continue;
|
|
10097
|
+
}
|
|
10098
|
+
if (!/^import\b/u.test(line)) break;
|
|
10099
|
+
const statement = [line];
|
|
10100
|
+
while (!(statement.at(-1) ?? "").trimEnd().endsWith(";") && index + 1 < lines.length && !/^import\b/u.test((lines[index + 1] ?? "").trim())) {
|
|
10101
|
+
index += 1;
|
|
10102
|
+
statement.push((lines[index] ?? "").trim());
|
|
10103
|
+
}
|
|
10104
|
+
imports.push(statement.join(" ").replace(/\s+/gu, " "));
|
|
10105
|
+
index += 1;
|
|
10106
|
+
}
|
|
10107
|
+
return { imports, body: lines.slice(index).join("\n") };
|
|
10108
|
+
}
|
|
10109
|
+
function viewCandidateTextOf(parts) {
|
|
10110
|
+
return parts.imports.length === 0 ? parts.body : `${parts.imports.join("\n")}
|
|
10111
|
+
${parts.body}`;
|
|
10112
|
+
}
|
|
10113
|
+
function extractViewCandidate(answer) {
|
|
10114
|
+
const fenced = /```(?:tsx|ts|jsx|typescript)?\s*\n([\s\S]*?)\n```/u.exec(answer);
|
|
10115
|
+
const parts = splitViewCandidate(fenced?.[1] ?? answer);
|
|
10116
|
+
return viewCandidateTextOf({ imports: parts.imports, body: extractCandidateBody(parts.body) });
|
|
10117
|
+
}
|
|
10118
|
+
function isAllowedViewImportSource(source) {
|
|
10119
|
+
if (source === "react" || source === "@lingui/react/macro" || source === "@ogygie/ui") return true;
|
|
10120
|
+
if (source.startsWith("@ogygie/ui/")) return true;
|
|
10121
|
+
if (/^\.{1,2}\/(?:\.\.\/)*patterns\/[^/]+$/u.test(source)) return true;
|
|
10122
|
+
if (/^\.\/[^/]+$/u.test(source)) return true;
|
|
10123
|
+
return source === "../queries.gen" || source === "../queries.gen.js";
|
|
10124
|
+
}
|
|
10125
|
+
function forbiddenInViewImports(imports) {
|
|
10126
|
+
const rules = [];
|
|
10127
|
+
for (const statement of imports) {
|
|
10128
|
+
if (/^import\s+type\b/u.test(statement)) continue;
|
|
10129
|
+
const source = /\bfrom\s+["']([^"']+)["']/u.exec(statement)?.[1] ?? /^import\s+["']([^"']+)["']/u.exec(statement)?.[1];
|
|
10130
|
+
if (source === void 0) {
|
|
10131
|
+
rules.push(`import illisible : ${statement}`);
|
|
10132
|
+
continue;
|
|
10133
|
+
}
|
|
10134
|
+
if (!/\bfrom\s+/u.test(statement)) {
|
|
10135
|
+
rules.push(`import d'effet interdit : ${source}`);
|
|
10136
|
+
continue;
|
|
10137
|
+
}
|
|
10138
|
+
if (!isAllowedViewImportSource(source)) rules.push(`import hors du catalogue : ${source}`);
|
|
10139
|
+
}
|
|
10140
|
+
return rules;
|
|
10141
|
+
}
|
|
10142
|
+
var VIEW_BODY_BANS = [
|
|
10143
|
+
...BODY_BANS,
|
|
10144
|
+
// Assembled from fragments: the repo's lint reads source text and would refuse the spelled-out word.
|
|
10145
|
+
{ pattern: new RegExp(["dangerously", "SetInnerHTML"].join(""), "u"), rule: "innerHTML interdit : l'\xE9chappement de React ne se d\xE9branche pas" },
|
|
10146
|
+
{ pattern: /\buse(?:Query|Mutation|QueryClient|SuspenseQuery|InfiniteQuery)\s*\(/u, rule: "aucun hook de donn\xE9es : la vue re\xE7oit ses donn\xE9es servies et ses actions en MutationLike" },
|
|
10147
|
+
{ pattern: /<(?:table|form)[\s>]/u, rule: "<table> et <form> nus interdits : DataTable et ContractForm sont les patterns" },
|
|
10148
|
+
{ pattern: /\bstyle=\{\{/u, rule: "style en ligne interdit : les tokens passent par les classes" },
|
|
10149
|
+
{ pattern: /\b[a-z][a-z0-9]*(?:-[a-z0-9]+)*-\[[^\]\n]+\]/u, rule: "valeur arbitraire interdite : les classes viennent des tokens" },
|
|
10150
|
+
{ pattern: /["'`][^"'`\n]*#[0-9a-fA-F]{3,8}\b[^"'`\n]*["'`]/u, rule: "couleur en dur interdite : les tokens passent par les classes" }
|
|
10151
|
+
];
|
|
10152
|
+
function forbiddenInViewCandidate(text) {
|
|
10153
|
+
const parts = splitViewCandidate(text);
|
|
10154
|
+
return [...forbiddenInViewImports(parts.imports), ...forbiddenInBody(parts.body, VIEW_BODY_BANS)];
|
|
10155
|
+
}
|
|
10156
|
+
function currentViewBody(file, holeId) {
|
|
10157
|
+
return parseLogicSections(file).get(holeId)?.body ?? "";
|
|
10158
|
+
}
|
|
10159
|
+
function replaceViewSections(file, holeId, candidate) {
|
|
10160
|
+
const parts = candidate === null ? { imports: [], body: customViewPlaceholderLine(holeId) } : splitViewCandidate(candidate);
|
|
10161
|
+
const lines = file.split("\n");
|
|
10162
|
+
const opening = lines.indexOf(`// @hole-imports ${holeId}`);
|
|
10163
|
+
const closing = lines.indexOf(`// @end-hole-imports ${holeId}`);
|
|
10164
|
+
if (opening === -1 || closing === -1 || closing < opening) {
|
|
10165
|
+
throw new Error(`replaceViewSections: view ${holeId} has no import markers`);
|
|
10166
|
+
}
|
|
10167
|
+
const withImports = [...lines.slice(0, opening + 1), ...parts.imports, ...lines.slice(closing)].join("\n");
|
|
10168
|
+
return replaceHoleBody(withImports, holeId, parts.body);
|
|
10169
|
+
}
|
|
10170
|
+
function customViewSignatureText(view) {
|
|
10171
|
+
return [...customViewPropsLines(view), customViewSignature(view)].join("\n");
|
|
10172
|
+
}
|
|
10173
|
+
function customViewShapeOf(view) {
|
|
10174
|
+
return {
|
|
10175
|
+
kind: "custom-view",
|
|
10176
|
+
reads: view.receives.map((field) => normalizedTypeOf(field.model)),
|
|
10177
|
+
columns: view.actions.map((action) => action.operation.kind),
|
|
10178
|
+
codeRole: "none",
|
|
10179
|
+
returns: "jsx"
|
|
10180
|
+
};
|
|
10181
|
+
}
|
|
10182
|
+
function customViewRules(view, plan, library) {
|
|
10183
|
+
const signature = customViewSignatureText(view);
|
|
10184
|
+
const signatureHash = signatureHashOf(signature);
|
|
10185
|
+
const shape = customViewShapeOf(view);
|
|
10186
|
+
const shapeHash = shapeHashOf(shape);
|
|
10187
|
+
return {
|
|
10188
|
+
isUnfilled: isUnfilledViewBody,
|
|
10189
|
+
bans: forbiddenInViewCandidate,
|
|
10190
|
+
libraryCandidates() {
|
|
10191
|
+
const exact = library.filter((entry) => entry.signatureHash === signatureHash);
|
|
10192
|
+
const neighbours = neighboursOfShape(shapeHash, library, { hole: view.hole.id, feature: plan.featureId }).filter((entry) => !exact.includes(entry));
|
|
10193
|
+
return [...exact, ...neighbours].slice(0, 5).map((entry) => ({ body: entry.bodyTemplate, origin: `library:${entry.signatureHash}`, model: null, usage: null }));
|
|
10194
|
+
},
|
|
10195
|
+
libraryEntryOf(body, provenance) {
|
|
10196
|
+
return { hole: view.hole.id, signature, signatureHash, shape, shapeHash, errorCode: "", bodyTemplate: body, provenance: [provenance] };
|
|
10197
|
+
}
|
|
10198
|
+
};
|
|
10199
|
+
}
|
|
10200
|
+
|
|
10071
10201
|
// src/fill/model-draw.ts
|
|
10072
10202
|
var DEFAULT_FILL_MODELS = {
|
|
10073
10203
|
spec: "claude-fable-5-1",
|
|
@@ -10087,16 +10217,35 @@ var HOLE_SYSTEM_PROMPT = [
|
|
|
10087
10217
|
// Assembled from fragments: the repo's lint reads source text and would refuse the spelled-out words.
|
|
10088
10218
|
`- no \`any\`, no \`@ts-${"ignore"}\`, no \`as unknown ${"as"}\`; identifiers and comments in English; two-space indentation.`
|
|
10089
10219
|
].join("\n");
|
|
10090
|
-
|
|
10220
|
+
var VIEW_SYSTEM_PROMPT = [
|
|
10221
|
+
"You write the BODY of ONE custom view of a React screen generated by the Ogygie framework. You receive the view's contract \u2014 its props (the served fields it receives, one MutationLike per action it may trigger), the example its test renders with the text fragments the render must carry, the spec lines it carries, conventions \u2014 and the catalogue of patterns the product owns. Nothing else.",
|
|
10222
|
+
"",
|
|
10223
|
+
"Answer in two parts, in this order, and nothing else: first the import statements the body needs (one per line, at column 0), then the body of the component \u2014 the lines between the signature's opening brace and the closing brace, two-space indented. No signature, no markers, no closing brace, no fences, no prose before or after.",
|
|
10224
|
+
"",
|
|
10225
|
+
"Imports come from a CLOSED catalogue: `react`, `@lingui/react/macro` (Trans, useLingui), `@ogygie/ui` and its entries (`@ogygie/ui/data-table`, `@ogygie/ui/contract-form`, `@ogygie/ui/variance-chart`), the product's patterns folder (a relative path ending in `patterns/<Name>`), a sibling view of the same screen (`./<Name>`), and the feature's own reads (`../queries.gen`). Any other source is refused before the test.",
|
|
10226
|
+
"",
|
|
10227
|
+
"Rules of a body:",
|
|
10228
|
+
"- the view composes patterns: never a raw `<table>` or `<form>`, never innerHTML, never an inline `style`;",
|
|
10229
|
+
"- it receives its data SERVED and its actions as MutationLike: no data hook, no fetch, no cache key, no `process`; a write goes through `mutate` of an action it received;",
|
|
10230
|
+
"- every text a human reads is a `<Trans>` or a `t` template of the i18n macro; identifiers and comments in English;",
|
|
10231
|
+
"- classes come from the product's tokens: no arbitrary value (`p-[13px]`), no hex colour;",
|
|
10232
|
+
'- the root element is `<section data-view="<view id>">`; the example must render every fragment the test expects, in its text;',
|
|
10233
|
+
// Assembled from fragments: the repo's lint reads source text and would refuse the spelled-out words.
|
|
10234
|
+
`- no \`any\`, no \`@ts-${"ignore"}\`, no \`as unknown ${"as"}\`; a date is formatted from the served string, never from a clock read in the view.`
|
|
10235
|
+
].join("\n");
|
|
10236
|
+
var VIEW_DRAWER_OPTIONS = { system: VIEW_SYSTEM_PROMPT, extract: extractViewCandidate };
|
|
10237
|
+
function createModelDrawer(client, models = DEFAULT_FILL_MODELS, options = {}) {
|
|
10238
|
+
const system = options.system ?? HOLE_SYSTEM_PROMPT;
|
|
10239
|
+
const extract = options.extract ?? extractCandidateBody;
|
|
10091
10240
|
async function one(model, context, origin) {
|
|
10092
10241
|
const answer = await client.complete({
|
|
10093
10242
|
model,
|
|
10094
|
-
system
|
|
10243
|
+
system,
|
|
10095
10244
|
messages: [{ role: "user", content: context }],
|
|
10096
10245
|
maxTokens: 4e3,
|
|
10097
10246
|
effort: "medium"
|
|
10098
10247
|
});
|
|
10099
|
-
return { body:
|
|
10248
|
+
return { body: extract(answer.text), origin, model: answer.model, usage: answer.usage };
|
|
10100
10249
|
}
|
|
10101
10250
|
return {
|
|
10102
10251
|
async draw(context, n, round) {
|
|
@@ -10548,8 +10697,8 @@ async function loadConsumedFeatures(project, block) {
|
|
|
10548
10697
|
if (owningApp === void 0) continue;
|
|
10549
10698
|
const specAbsolute = join3(project.root, entry.spec);
|
|
10550
10699
|
const markdown = existsSync2(specAbsolute) ? readFileSync3(specAbsolute, "utf8") : "";
|
|
10551
|
-
const
|
|
10552
|
-
const featureBlock =
|
|
10700
|
+
const report2 = checkSpec({ markdown, specPath: entry.spec, generation: project.generation });
|
|
10701
|
+
const featureBlock = report2.kind === "feature" ? report2.block : null;
|
|
10553
10702
|
const schemasModule = featureBlock === null ? "schemas.ts" : featureBlock.contracts.module.replace(/^\.\//u, "");
|
|
10554
10703
|
const schemas = await loader(`${owningApp.path}/${entry.folder}/${schemasModule}`);
|
|
10555
10704
|
loaded.set(consumed.feature, {
|
|
@@ -10562,10 +10711,113 @@ async function loadConsumedFeatures(project, block) {
|
|
|
10562
10711
|
return loaded;
|
|
10563
10712
|
}
|
|
10564
10713
|
|
|
10714
|
+
// src/cli/generate-screen-command.ts
|
|
10715
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "node:fs";
|
|
10716
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
10717
|
+
import { performance } from "node:perf_hooks";
|
|
10718
|
+
function consumedInputsOf(loaded) {
|
|
10719
|
+
const inputs = [];
|
|
10720
|
+
for (const feature of loaded.values()) {
|
|
10721
|
+
if (feature.schemas === null) continue;
|
|
10722
|
+
inputs.push({ featureId: feature.featureId, block: feature.block, schemas: feature.schemas, routes: feature.routes });
|
|
10723
|
+
}
|
|
10724
|
+
return inputs;
|
|
10725
|
+
}
|
|
10726
|
+
async function screenPlanOf(project, featureId, out, command = "ogygie generate") {
|
|
10727
|
+
const lookup = screenTargetOf(project, featureId);
|
|
10728
|
+
if (!lookup.ok) {
|
|
10729
|
+
out.error(formatProblem(lookup.problem));
|
|
10730
|
+
return null;
|
|
10731
|
+
}
|
|
10732
|
+
const { target } = lookup;
|
|
10733
|
+
const markdown = readFileSync4(join4(project.root, target.specPath), "utf8");
|
|
10734
|
+
const first = checkSpec({ markdown, specPath: target.specPath, generation: target.generation });
|
|
10735
|
+
if (first.kind !== "screen" || first.screen === null) {
|
|
10736
|
+
for (const problem2 of first.problems) out.error(formatProblem(problem2));
|
|
10737
|
+
out.error(`${command} \u2014 ${target.specPath} n'a pas de bloc # G\xE9n\xE9ration d'\xE9cran lisible (consumes, views, holes).`);
|
|
10738
|
+
return null;
|
|
10739
|
+
}
|
|
10740
|
+
const loaded = await loadConsumedFeatures(project, first.screen);
|
|
10741
|
+
const report2 = checkSpec({ markdown, specPath: target.specPath, generation: target.generation, consumed: loaded });
|
|
10742
|
+
if (!report2.ok || report2.screen === null) {
|
|
10743
|
+
for (const problem2 of report2.problems) out.error(formatProblem(problem2));
|
|
10744
|
+
out.error(`${command} \u2014 ${report2.problems.length} probl\xE8me(s), rien n'est \xE9crit.`);
|
|
10745
|
+
return null;
|
|
10746
|
+
}
|
|
10747
|
+
const plan = buildScreenPlan({
|
|
10748
|
+
featureId,
|
|
10749
|
+
folder: target.folder,
|
|
10750
|
+
appPath: target.appPath,
|
|
10751
|
+
specPath: target.specPath,
|
|
10752
|
+
block: report2.screen,
|
|
10753
|
+
consumed: consumedInputsOf(loaded),
|
|
10754
|
+
front: target.front,
|
|
10755
|
+
guards: target.generation.guards,
|
|
10756
|
+
contractViolationCode: contractViolationCodeOf(target.generation.target),
|
|
10757
|
+
generatorVersion: GENERATOR_VERSION,
|
|
10758
|
+
behaviors: boundScreenBehaviorsOf(report2.behaviors)
|
|
10759
|
+
});
|
|
10760
|
+
return { plan, target, markdown, report: report2 };
|
|
10761
|
+
}
|
|
10762
|
+
async function runGenerateScreenCommand(project, featureId, out) {
|
|
10763
|
+
const built = await screenPlanOf(project, featureId, out);
|
|
10764
|
+
if (built === null) return 1;
|
|
10765
|
+
const { plan, target, markdown, report: report2 } = built;
|
|
10766
|
+
const started = performance.now();
|
|
10767
|
+
const existingCustomViews = /* @__PURE__ */ new Map();
|
|
10768
|
+
for (const view of plan.views) {
|
|
10769
|
+
if (view.kind !== "custom") continue;
|
|
10770
|
+
const path = join4(project.root, plan.folder, "views", `${view.componentName}.tsx`);
|
|
10771
|
+
existingCustomViews.set(view.id, existsSync3(path) ? readFileSync4(path, "utf8") : null);
|
|
10772
|
+
}
|
|
10773
|
+
const files = generateScreen(plan, { existingCustomViews });
|
|
10774
|
+
const specContracts = applyGeneratedContracts(markdown, emitScreenSpecContracts(plan), GENERATOR_VERSION);
|
|
10775
|
+
const elapsedMs = Math.round(performance.now() - started);
|
|
10776
|
+
let written = 0;
|
|
10777
|
+
let emitted = 0;
|
|
10778
|
+
for (const file of files) {
|
|
10779
|
+
const absolute = join4(project.root, file.path);
|
|
10780
|
+
const exists = existsSync3(absolute);
|
|
10781
|
+
emitted += file.content.split("\n").length - 1;
|
|
10782
|
+
if (exists && readFileSync4(absolute, "utf8") === file.content) {
|
|
10783
|
+
out.log(` identique ${file.path}`);
|
|
10784
|
+
continue;
|
|
10785
|
+
}
|
|
10786
|
+
mkdirSync(dirname2(absolute), { recursive: true });
|
|
10787
|
+
writeFileSync(absolute, file.content, "utf8");
|
|
10788
|
+
written += 1;
|
|
10789
|
+
out.log(` ${exists ? "r\xE9\xE9crit " : "\xE9crit "} ${file.path}`);
|
|
10790
|
+
}
|
|
10791
|
+
const bodyPath = screenBodyPath(plan);
|
|
10792
|
+
const bodyAbsolute = join4(project.root, bodyPath);
|
|
10793
|
+
if (existsSync3(bodyAbsolute)) {
|
|
10794
|
+
out.log(` conserv\xE9 ${bodyPath} (corps \xE9crit, jamais compar\xE9 \u2014 D40)`);
|
|
10795
|
+
} else {
|
|
10796
|
+
mkdirSync(dirname2(bodyAbsolute), { recursive: true });
|
|
10797
|
+
writeFileSync(bodyAbsolute, screenBodyDefault(plan), "utf8");
|
|
10798
|
+
written += 1;
|
|
10799
|
+
out.log(` \xE9crit ${bodyPath} (corps par d\xE9faut, une fois : le v\xF4tre d\xE9sormais \u2014 D40)`);
|
|
10800
|
+
}
|
|
10801
|
+
if (specContracts.outcome === "section-missing") {
|
|
10802
|
+
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).`);
|
|
10803
|
+
return 1;
|
|
10804
|
+
}
|
|
10805
|
+
if (specContracts.outcome === "unchanged") {
|
|
10806
|
+
out.log(` identique ${target.specPath} \u203A bloc @generated de # Contrats de donn\xE9es`);
|
|
10807
|
+
} else {
|
|
10808
|
+
writeFileSync(join4(project.root, target.specPath), specContracts.markdown, "utf8");
|
|
10809
|
+
out.log(` ${specContracts.outcome === "inserted" ? "ins\xE9r\xE9 " : "r\xE9\xE9crit "} ${target.specPath} \u203A bloc @generated de # Contrats de donn\xE9es`);
|
|
10810
|
+
}
|
|
10811
|
+
out.log(
|
|
10812
|
+
`ogygie generate \u2014 ${featureId} (\xE9cran) : ${files.length} fichier(s) \xE9mis en ${elapsedMs} ms (${emitted} lignes \xE9mises, bloc de la spec compris), ${written} \xE9crit(s), ${report2.tally.derivable} ligne(s) li\xE9e(s), ${report2.tally.hole} port\xE9e(s) par un trou ; ${plan.views.filter((view) => view.kind === "custom").length} vue(s) custom sur ${plan.views.length}.`
|
|
10813
|
+
);
|
|
10814
|
+
return 0;
|
|
10815
|
+
}
|
|
10816
|
+
|
|
10565
10817
|
// src/cli/fill-command.ts
|
|
10566
10818
|
import { spawnSync } from "node:child_process";
|
|
10567
|
-
import { appendFileSync, existsSync as
|
|
10568
|
-
import { join as
|
|
10819
|
+
import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
10820
|
+
import { join as join5 } from "node:path";
|
|
10569
10821
|
|
|
10570
10822
|
// ../ogygie-core/src/model.ts
|
|
10571
10823
|
function createMessagesClient(deps) {
|
|
@@ -10650,8 +10902,8 @@ function scrubbedEnvironment(env) {
|
|
|
10650
10902
|
}
|
|
10651
10903
|
return kept;
|
|
10652
10904
|
}
|
|
10653
|
-
var vitestHoleRunner = ({ projectRoot, testPath,
|
|
10654
|
-
const result = spawnSync("pnpm", ["exec", "vitest", "run", testPath, "-t",
|
|
10905
|
+
var vitestHoleRunner = ({ projectRoot, testPath, testName }) => {
|
|
10906
|
+
const result = spawnSync("pnpm", ["exec", "vitest", "run", testPath, "-t", testName], {
|
|
10655
10907
|
cwd: projectRoot,
|
|
10656
10908
|
env: scrubbedEnvironment(process.env),
|
|
10657
10909
|
encoding: "utf8"
|
|
@@ -10660,6 +10912,12 @@ var vitestHoleRunner = ({ projectRoot, testPath, holeId }) => {
|
|
|
10660
10912
|
${result.stderr ?? ""}`.split("\n").filter((line) => line.trim() !== "");
|
|
10661
10913
|
return { green: result.status === 0, lines };
|
|
10662
10914
|
};
|
|
10915
|
+
function viewExampleTestName(view) {
|
|
10916
|
+
return `rend l'exemple de la vue custom ${view.id} (trou ${view.hole.id})`;
|
|
10917
|
+
}
|
|
10918
|
+
var NOMINAL_PREVIEW_TEST_NAME = "rend l'\xE9tat nominal";
|
|
10919
|
+
var FILL_NEEDS_OPERATOR_EXIT = 2;
|
|
10920
|
+
var PATTERN_CATALOGUE_PATH = "design/patterns.md";
|
|
10663
10921
|
async function planOf(project, featureId, out) {
|
|
10664
10922
|
const lookup = featureTargetOf(project, featureId);
|
|
10665
10923
|
if (!lookup.ok) {
|
|
@@ -10667,7 +10925,7 @@ async function planOf(project, featureId, out) {
|
|
|
10667
10925
|
return null;
|
|
10668
10926
|
}
|
|
10669
10927
|
const { target } = lookup;
|
|
10670
|
-
const markdown =
|
|
10928
|
+
const markdown = readFileSync5(join5(project.root, target.specPath), "utf8");
|
|
10671
10929
|
const firstPass = checkSpec({ markdown, specPath: target.specPath, generation: target.generation });
|
|
10672
10930
|
if (firstPass.block === null) {
|
|
10673
10931
|
for (const problem2 of firstPass.problems) out.error(formatProblem(problem2));
|
|
@@ -10680,89 +10938,57 @@ async function planOf(project, featureId, out) {
|
|
|
10680
10938
|
out.error(`ogygie generate fill \u2014 ${modulePath} n'existe pas (D19 : schemas.ts est le premier geste humain).`);
|
|
10681
10939
|
return null;
|
|
10682
10940
|
}
|
|
10683
|
-
const
|
|
10684
|
-
if (!
|
|
10685
|
-
for (const problem2 of
|
|
10941
|
+
const report2 = checkSpec({ markdown, specPath: target.specPath, generation: target.generation, schemas });
|
|
10942
|
+
if (!report2.ok || report2.block === null) {
|
|
10943
|
+
for (const problem2 of report2.problems) out.error(formatProblem(problem2));
|
|
10686
10944
|
return null;
|
|
10687
10945
|
}
|
|
10688
|
-
const migrationsDir =
|
|
10946
|
+
const migrationsDir = join5(project.root, target.appPath, target.generation.target.migrations);
|
|
10689
10947
|
const plan = buildFeaturePlan({
|
|
10690
10948
|
featureId,
|
|
10691
10949
|
folder: target.folder,
|
|
10692
10950
|
appPath: target.appPath,
|
|
10693
10951
|
specPath: target.specPath,
|
|
10694
|
-
block:
|
|
10952
|
+
block: report2.block,
|
|
10695
10953
|
schemas,
|
|
10696
10954
|
generation: target.generation,
|
|
10697
10955
|
...target.tenantColumn === void 0 ? {} : { tenantColumn: target.tenantColumn },
|
|
10698
|
-
migrationFileName: migrationFileNameFor(
|
|
10956
|
+
migrationFileName: migrationFileNameFor(existsSync4(migrationsDir) ? readdirSync2(migrationsDir) : [], report2.block.contracts.table === void 0 ? featureId : tableNameOf(report2.block.contracts.table)),
|
|
10699
10957
|
generatorVersion: GENERATOR_VERSION,
|
|
10700
|
-
behaviors: boundBehaviorsOf(
|
|
10958
|
+
behaviors: boundBehaviorsOf(report2.behaviors)
|
|
10701
10959
|
});
|
|
10702
10960
|
return { plan, folder: target.folder };
|
|
10703
10961
|
}
|
|
10704
10962
|
function recordLibraryEntry(libraryDir, entry) {
|
|
10705
|
-
|
|
10706
|
-
const path =
|
|
10707
|
-
if (!
|
|
10708
|
-
|
|
10963
|
+
mkdirSync2(libraryDir, { recursive: true });
|
|
10964
|
+
const path = join5(libraryDir, libraryFileNameOf(entry));
|
|
10965
|
+
if (!existsSync4(path)) {
|
|
10966
|
+
writeFileSync2(path, `${JSON.stringify(entry, null, 2)}
|
|
10709
10967
|
`, "utf8");
|
|
10710
10968
|
return "created";
|
|
10711
10969
|
}
|
|
10712
|
-
const existing = HoleLibraryEntrySchema.parse(JSON.parse(
|
|
10970
|
+
const existing = HoleLibraryEntrySchema.parse(JSON.parse(readFileSync5(path, "utf8")));
|
|
10713
10971
|
const origin = entry.provenance[0];
|
|
10714
10972
|
if (origin === void 0 || existing.provenance.some((known) => known.repo === origin.repo && known.feature === origin.feature && known.hole === origin.hole)) return "unchanged";
|
|
10715
|
-
|
|
10973
|
+
writeFileSync2(path, `${JSON.stringify({ ...existing, provenance: [...existing.provenance, origin] }, null, 2)}
|
|
10716
10974
|
`, "utf8");
|
|
10717
10975
|
return "extended";
|
|
10718
10976
|
}
|
|
10719
|
-
|
|
10720
|
-
if (featureId === void 0 || featureId === "") {
|
|
10721
|
-
out.error("usage: ogygie generate fill <feature-id> [--candidates 5] [--rounds 2] [--draw <model>] [--escalate <model>] [--library-only]");
|
|
10722
|
-
return 1;
|
|
10723
|
-
}
|
|
10977
|
+
function wiringOf(project, featureId, argv, out, deps, kind) {
|
|
10724
10978
|
const flags = parseFillFlags(argv);
|
|
10725
|
-
const built = await planOf(project, featureId, out);
|
|
10726
|
-
if (built === null) return 1;
|
|
10727
|
-
const { plan, folder } = built;
|
|
10728
|
-
const logicPath = join4(project.root, folder, "logic.ts");
|
|
10729
|
-
if (!existsSync3(logicPath)) {
|
|
10730
|
-
out.error(`ogygie generate fill \u2014 ${folder}/logic.ts n'existe pas : lancez \`ogygie generate generate ${featureId}\` d'abord.`);
|
|
10731
|
-
return 1;
|
|
10732
|
-
}
|
|
10733
|
-
const testPath = `${folder}/holes.gen.test.ts`;
|
|
10734
|
-
const sections = parseLogicSections(readFileSync4(logicPath, "utf8"));
|
|
10735
|
-
const library = loadHolesLibrary(deps.libraryDir ?? HOLES_LIBRARY_DIR);
|
|
10736
|
-
const inputs = plan.holes.filter((hole) => hole.kind !== "view").map((hole) => {
|
|
10737
|
-
const contextPath = join4(project.root, folder, "holes", `${hole.id}.gen.md`);
|
|
10738
|
-
return {
|
|
10739
|
-
hole,
|
|
10740
|
-
plan,
|
|
10741
|
-
context: existsSync3(contextPath) ? readFileSync4(contextPath, "utf8") : `# ${hole.id}`,
|
|
10742
|
-
currentBody: sections.get(hole.id)?.body ?? "",
|
|
10743
|
-
library
|
|
10744
|
-
};
|
|
10745
|
-
});
|
|
10746
|
-
if (inputs.length === 0) {
|
|
10747
|
-
out.log(`ogygie generate fill \u2014 ${featureId} n'a aucun trou TypeScript : rien \xE0 remplir.`);
|
|
10748
|
-
return 0;
|
|
10749
|
-
}
|
|
10750
10979
|
const modelApiKey = deps.modelApiKey ?? process.env["OGYGIE_MODEL_API_KEY"];
|
|
10751
10980
|
const options = modelApiKey === void 0 ? { ...flags.options, libraryOnly: true } : flags.options;
|
|
10752
10981
|
if (modelApiKey === void 0 && !flags.options.libraryOnly) {
|
|
10753
10982
|
out.log("ogygie generate fill \u2014 OGYGIE_MODEL_API_KEY absente : biblioth\xE8que seule, aucun mod\xE8le appel\xE9.");
|
|
10754
10983
|
}
|
|
10755
|
-
const drawer = modelApiKey === void 0 ? null : createModelDrawer(createMessagesClient({ apiKey: modelApiKey, fetchImpl: deps.fetchImpl ?? fetch, credentialHint: "OGYGIE_MODEL_API_KEY" }), flags.models);
|
|
10756
|
-
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
10762
|
-
|
|
10763
|
-
async runHoleTest(holeId) {
|
|
10764
|
-
return runner({ projectRoot: project.root, testPath, holeId });
|
|
10765
|
-
},
|
|
10984
|
+
const drawer = modelApiKey === void 0 ? null : createModelDrawer(createMessagesClient({ apiKey: modelApiKey, fetchImpl: deps.fetchImpl ?? fetch, credentialHint: "OGYGIE_MODEL_API_KEY" }), flags.models, kind === "view" ? VIEW_DRAWER_OPTIONS : {});
|
|
10985
|
+
void project;
|
|
10986
|
+
return { featureId, options, drawer, libraryDir: deps.libraryDir ?? HOLES_LIBRARY_DIR, deps, out };
|
|
10987
|
+
}
|
|
10988
|
+
function fillDepsOf(wiring, effects) {
|
|
10989
|
+
const { drawer, out } = wiring;
|
|
10990
|
+
return {
|
|
10991
|
+
...effects,
|
|
10766
10992
|
async draw(context, n, round) {
|
|
10767
10993
|
if (drawer === null) return [];
|
|
10768
10994
|
return drawer.draw(context, n, round);
|
|
@@ -10772,20 +10998,22 @@ async function runFillCommand(project, featureId, argv, out, deps = {}) {
|
|
|
10772
10998
|
return drawer.escalate(context);
|
|
10773
10999
|
},
|
|
10774
11000
|
async recordLibrary(entry) {
|
|
10775
|
-
const outcome = recordLibraryEntry(libraryDir, entry);
|
|
11001
|
+
const outcome = recordLibraryEntry(wiring.libraryDir, entry);
|
|
10776
11002
|
out.log(` biblioth\xE8que ${outcome === "created" ? "+" : outcome === "extended" ? "~" : "="} ${libraryFileNameOf(entry)} (${entry.hole})`);
|
|
10777
11003
|
},
|
|
10778
|
-
today: deps.today ?? (() => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)),
|
|
10779
|
-
repo: deps.repo ?? process.env["GITHUB_REPOSITORY"] ?? "local",
|
|
11004
|
+
today: wiring.deps.today ?? (() => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)),
|
|
11005
|
+
repo: wiring.deps.repo ?? process.env["GITHUB_REPOSITORY"] ?? "local",
|
|
10780
11006
|
log: (line) => out.log(` ${line}`)
|
|
10781
11007
|
};
|
|
10782
|
-
|
|
11008
|
+
}
|
|
11009
|
+
function report(wiring, summary, label) {
|
|
11010
|
+
const { out, featureId } = wiring;
|
|
10783
11011
|
for (const result of summary.results) {
|
|
10784
|
-
const
|
|
10785
|
-
out.log(` ${result.holeId} \u2014 ${
|
|
11012
|
+
const outcome = result.outcome === "already-filled" ? "d\xE9j\xE0 rempli" : result.outcome === "human" ? label.human : result.outcome;
|
|
11013
|
+
out.log(` ${result.holeId} \u2014 ${outcome}${result.model === null ? "" : ` par ${result.model}`}, ${result.tried} candidat(s) essay\xE9(s)${result.banned > 0 ? `, ${result.banned} interdit(s)` : ""}`);
|
|
10786
11014
|
if (result.outcome === "human") for (const line of result.lastRun.slice(-8)) out.log(` ${line}`);
|
|
10787
11015
|
}
|
|
10788
|
-
const summaryPath = deps.summaryPath ?? process.env["GITHUB_STEP_SUMMARY"];
|
|
11016
|
+
const summaryPath = wiring.deps.summaryPath ?? process.env["GITHUB_STEP_SUMMARY"];
|
|
10789
11017
|
if (summaryPath !== void 0 && summary.usage.length > 0) {
|
|
10790
11018
|
const rows = summary.results.flatMap((result) => result.usage.map((usage) => `| ${result.holeId} | ${result.outcome} | ${usage.model} | ${usage.inputTokens} | ${usage.outputTokens} |`));
|
|
10791
11019
|
appendFileSync(summaryPath, `
|
|
@@ -10799,10 +11027,119 @@ ${rows.join("\n")}
|
|
|
10799
11027
|
const totalInput = summary.usage.reduce((sum, usage) => sum + usage.inputTokens, 0);
|
|
10800
11028
|
const totalOutput = summary.usage.reduce((sum, usage) => sum + usage.outputTokens, 0);
|
|
10801
11029
|
out.log(
|
|
10802
|
-
`ogygie generate fill \u2014 ${featureId} : ${summary.filled}
|
|
11030
|
+
`ogygie generate fill \u2014 ${featureId} : ${summary.filled} ${label.unit}(s) rempli(s), ${summary.unfilled} laiss\xE9(s) \xE0 l'humain, ${summary.results.filter((result) => result.outcome === "already-filled").length} d\xE9j\xE0 rempli(s) ; ${summary.usage.length} appel(s) de mod\xE8le (${totalInput} jetons d'entr\xE9e, ${totalOutput} de sortie)${summary.holesTooBig ? " ; PLUS D'UN TROU SUR DIX a atteint l'escalade : trop gros, \xE0 scinder dans la spec (D24)" : ""}.`
|
|
10803
11031
|
);
|
|
11032
|
+
}
|
|
11033
|
+
async function fillLogicHoles(project, featureId, argv, out, deps) {
|
|
11034
|
+
const built = await planOf(project, featureId, out);
|
|
11035
|
+
if (built === null) return 1;
|
|
11036
|
+
const { plan, folder } = built;
|
|
11037
|
+
const logicPath = join5(project.root, folder, "logic.ts");
|
|
11038
|
+
if (!existsSync4(logicPath)) {
|
|
11039
|
+
out.error(`ogygie generate fill \u2014 ${folder}/logic.ts n'existe pas : lancez \`ogygie generate generate ${featureId}\` d'abord.`);
|
|
11040
|
+
return 1;
|
|
11041
|
+
}
|
|
11042
|
+
const testPath = `${folder}/holes.gen.test.ts`;
|
|
11043
|
+
const sections = parseLogicSections(readFileSync5(logicPath, "utf8"));
|
|
11044
|
+
const wiring = wiringOf(project, featureId, argv, out, deps, "logic");
|
|
11045
|
+
const library = loadHolesLibrary(wiring.libraryDir);
|
|
11046
|
+
const inputs = plan.holes.filter((hole) => hole.kind !== "view").map((hole) => {
|
|
11047
|
+
const contextPath = join5(project.root, folder, "holes", `${hole.id}.gen.md`);
|
|
11048
|
+
return {
|
|
11049
|
+
holeId: hole.id,
|
|
11050
|
+
featureId: plan.featureId,
|
|
11051
|
+
context: existsSync4(contextPath) ? readFileSync5(contextPath, "utf8") : `# ${hole.id}`,
|
|
11052
|
+
currentBody: sections.get(hole.id)?.body ?? "",
|
|
11053
|
+
rules: logicHoleRules(hole, plan, library)
|
|
11054
|
+
};
|
|
11055
|
+
});
|
|
11056
|
+
if (inputs.length === 0) {
|
|
11057
|
+
out.log(`ogygie generate fill \u2014 ${featureId} n'a aucun trou TypeScript : rien \xE0 remplir.`);
|
|
11058
|
+
return 0;
|
|
11059
|
+
}
|
|
11060
|
+
const runner = deps.runner ?? vitestHoleRunner;
|
|
11061
|
+
const fillDeps = fillDepsOf(wiring, {
|
|
11062
|
+
async writeBody(holeId, body) {
|
|
11063
|
+
const logic = readFileSync5(logicPath, "utf8");
|
|
11064
|
+
writeFileSync2(logicPath, replaceHoleBody(logic, holeId, body ?? ` return todo(${JSON.stringify(holeId)});`), "utf8");
|
|
11065
|
+
},
|
|
11066
|
+
async runHoleTest(holeId) {
|
|
11067
|
+
return runner({ projectRoot: project.root, testPath, testName: holeId });
|
|
11068
|
+
}
|
|
11069
|
+
});
|
|
11070
|
+
const summary = await fillFeature(inputs, fillDeps, wiring.options);
|
|
11071
|
+
report(wiring, summary, { unit: "trou", human: "HUMAIN (todo)" });
|
|
10804
11072
|
return summary.unfilled === 0 ? 0 : 1;
|
|
10805
11073
|
}
|
|
11074
|
+
function viewContextOf(projectRoot, plan, view) {
|
|
11075
|
+
const docPath = join5(projectRoot, plan.folder, "holes", `${view.hole.id}.gen.md`);
|
|
11076
|
+
const doc = existsSync4(docPath) ? readFileSync5(docPath, "utf8") : `# ${view.hole.id}`;
|
|
11077
|
+
const cataloguePath = join5(projectRoot, PATTERN_CATALOGUE_PATH);
|
|
11078
|
+
if (!existsSync4(cataloguePath)) return doc;
|
|
11079
|
+
return `${doc.trimEnd()}
|
|
11080
|
+
|
|
11081
|
+
## Catalogue des patterns du produit (${PATTERN_CATALOGUE_PATH})
|
|
11082
|
+
|
|
11083
|
+
${readFileSync5(cataloguePath, "utf8").trimEnd()}
|
|
11084
|
+
`;
|
|
11085
|
+
}
|
|
11086
|
+
async function fillCustomViews(project, featureId, argv, out, deps) {
|
|
11087
|
+
const built = await screenPlanOf(project, featureId, out, "ogygie generate fill");
|
|
11088
|
+
if (built === null) return 1;
|
|
11089
|
+
const { plan } = built;
|
|
11090
|
+
const views = plan.views.filter((view) => view.kind === "custom");
|
|
11091
|
+
if (views.length === 0) {
|
|
11092
|
+
out.log(`ogygie generate fill \u2014 ${featureId} (\xE9cran) n'a aucune vue custom : rien \xE0 remplir.`);
|
|
11093
|
+
return 0;
|
|
11094
|
+
}
|
|
11095
|
+
const fileOf2 = (view) => join5(project.root, plan.folder, "views", `${view.componentName}.tsx`);
|
|
11096
|
+
for (const view of views) {
|
|
11097
|
+
if (!existsSync4(fileOf2(view))) {
|
|
11098
|
+
out.error(`ogygie generate fill \u2014 ${plan.folder}/views/${view.componentName}.tsx n'existe pas : lancez \`ogygie generate generate ${featureId}\` d'abord.`);
|
|
11099
|
+
return 1;
|
|
11100
|
+
}
|
|
11101
|
+
}
|
|
11102
|
+
const testPath = `${plan.folder}/${plan.screenName}.gen.test.tsx`;
|
|
11103
|
+
const previewsPath = `${plan.folder}/${plan.screenName}${PREVIEWS_FILE_SUFFIX}`;
|
|
11104
|
+
const judgesPreview = plan.front.tests === "static-render" && existsSync4(join5(project.root, previewsPath));
|
|
11105
|
+
const wiring = wiringOf(project, featureId, argv, out, deps, "view");
|
|
11106
|
+
const library = loadHolesLibrary(wiring.libraryDir);
|
|
11107
|
+
const byHole = new Map(views.map((view) => [view.hole.id, view]));
|
|
11108
|
+
const inputs = views.map((view) => ({
|
|
11109
|
+
holeId: view.hole.id,
|
|
11110
|
+
featureId: plan.featureId,
|
|
11111
|
+
context: viewContextOf(project.root, plan, view),
|
|
11112
|
+
currentBody: currentViewBody(readFileSync5(fileOf2(view), "utf8"), view.hole.id),
|
|
11113
|
+
rules: customViewRules(view, plan, library)
|
|
11114
|
+
}));
|
|
11115
|
+
const runner = deps.runner ?? vitestHoleRunner;
|
|
11116
|
+
const fillDeps = fillDepsOf(wiring, {
|
|
11117
|
+
async writeBody(holeId, body) {
|
|
11118
|
+
const view = byHole.get(holeId);
|
|
11119
|
+
if (view === void 0) throw new Error(`fill: no custom view carries the hole ${holeId}`);
|
|
11120
|
+
writeFileSync2(fileOf2(view), replaceViewSections(readFileSync5(fileOf2(view), "utf8"), holeId, body), "utf8");
|
|
11121
|
+
},
|
|
11122
|
+
async runHoleTest(holeId) {
|
|
11123
|
+
const view = byHole.get(holeId);
|
|
11124
|
+
if (view === void 0) throw new Error(`fill: no custom view carries the hole ${holeId}`);
|
|
11125
|
+
const example = runner({ projectRoot: project.root, testPath, testName: viewExampleTestName(view) });
|
|
11126
|
+
if (!example.green || !judgesPreview) return example;
|
|
11127
|
+
const nominal = runner({ projectRoot: project.root, testPath: previewsPath, testName: NOMINAL_PREVIEW_TEST_NAME });
|
|
11128
|
+
return { green: nominal.green, lines: [...example.lines, ...nominal.lines] };
|
|
11129
|
+
}
|
|
11130
|
+
});
|
|
11131
|
+
out.log(`ogygie generate fill \u2014 ${featureId} (\xE9cran) : ${views.length} vue(s) custom, jug\xE9e(s) par le test d'exemple${judgesPreview ? " puis le rendu statique de l'\xE9tat nominal" : ""}.`);
|
|
11132
|
+
const summary = await fillFeature(inputs, fillDeps, wiring.options);
|
|
11133
|
+
report(wiring, summary, { unit: "corps de vue", human: "OP\xC9RATEUR (placeholder)" });
|
|
11134
|
+
return summary.unfilled === 0 ? 0 : FILL_NEEDS_OPERATOR_EXIT;
|
|
11135
|
+
}
|
|
11136
|
+
async function runFillCommand(project, featureId, argv, out, deps = {}) {
|
|
11137
|
+
if (featureId === void 0 || featureId === "") {
|
|
11138
|
+
out.error("usage: ogygie generate fill <feature-id> [--candidates 5] [--rounds 2] [--draw <model>] [--escalate <model>] [--library-only]");
|
|
11139
|
+
return 1;
|
|
11140
|
+
}
|
|
11141
|
+
return targetKindOf(project, featureId) === "screen" ? fillCustomViews(project, featureId, argv, out, deps) : fillLogicHoles(project, featureId, argv, out, deps);
|
|
11142
|
+
}
|
|
10806
11143
|
|
|
10807
11144
|
export {
|
|
10808
11145
|
ogygieError,
|
|
@@ -10969,10 +11306,27 @@ export {
|
|
|
10969
11306
|
BODY_BANS,
|
|
10970
11307
|
forbiddenInBody,
|
|
10971
11308
|
DEFAULT_FILL_OPTIONS,
|
|
11309
|
+
logicHoleRules,
|
|
10972
11310
|
fillHole,
|
|
10973
11311
|
fillFeature,
|
|
11312
|
+
VIEW_PLACEHOLDER,
|
|
11313
|
+
isUnfilledViewBody,
|
|
11314
|
+
splitViewCandidate,
|
|
11315
|
+
viewCandidateTextOf,
|
|
11316
|
+
extractViewCandidate,
|
|
11317
|
+
isAllowedViewImportSource,
|
|
11318
|
+
forbiddenInViewImports,
|
|
11319
|
+
VIEW_BODY_BANS,
|
|
11320
|
+
forbiddenInViewCandidate,
|
|
11321
|
+
currentViewBody,
|
|
11322
|
+
replaceViewSections,
|
|
11323
|
+
customViewSignatureText,
|
|
11324
|
+
customViewShapeOf,
|
|
11325
|
+
customViewRules,
|
|
10974
11326
|
DEFAULT_FILL_MODELS,
|
|
10975
11327
|
HOLE_SYSTEM_PROMPT,
|
|
11328
|
+
VIEW_SYSTEM_PROMPT,
|
|
11329
|
+
VIEW_DRAWER_OPTIONS,
|
|
10976
11330
|
createModelDrawer,
|
|
10977
11331
|
createMessagesClient,
|
|
10978
11332
|
FeatureNameSchema,
|
|
@@ -10985,8 +11339,13 @@ export {
|
|
|
10985
11339
|
targetKindOf,
|
|
10986
11340
|
screenTargetOf,
|
|
10987
11341
|
loadConsumedFeatures,
|
|
11342
|
+
screenPlanOf,
|
|
11343
|
+
runGenerateScreenCommand,
|
|
10988
11344
|
parseFillFlags,
|
|
10989
11345
|
scrubbedEnvironment,
|
|
11346
|
+
viewExampleTestName,
|
|
11347
|
+
NOMINAL_PREVIEW_TEST_NAME,
|
|
11348
|
+
FILL_NEEDS_OPERATOR_EXIT,
|
|
10990
11349
|
recordLibraryEntry,
|
|
10991
11350
|
runFillCommand
|
|
10992
11351
|
};
|