@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.
@@ -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 [` return <p data-pattern="HoleUnfilled">{${quote(view.hole.id)}}</p>;`];
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 report = first;
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
- report = checkSpec({ markdown, specPath, guards, generation: input.generation, consumed });
9858
- if (report.ok && report.screen !== null && owner !== void 0 && folder !== void 0 && owner.appType === "front" && owner.appPath !== void 0 && input.generation !== null) {
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: report.screen,
9865
- behaviors: report.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
- report = checkSpec({ markdown, specPath, guards, generation: input.generation, schemas });
9888
- if (report.ok && report.block !== null && input.generation !== null && input.apiAppPath !== void 0 && (input.tenantColumn !== void 0 || input.generation.target.tenancy === "none")) {
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: report.behaviors,
9895
- block: report.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: report.generated, tally: report.tally });
9907
- problems.push(...report.problems);
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 BODY_BANS.filter((ban) => ban.pattern.test(code)).map((ban) => ban.rule);
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
- async function tryCandidate(deps, holeId, candidate, result) {
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 = forbiddenInBody(candidate.body);
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 { hole, plan } = input;
10022
- const result = { holeId: hole.id, outcome: "human", tried: 0, banned: 0, model: null, usage: [], lastRun: [] };
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: hole.id, repo: deps.repo, feature: plan.featureId, path, model: candidate.model, date: deps.today() };
10028
- await deps.recordLibrary(libraryEntryOf(hole, plan, candidate.body, provenance));
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 signatureHash = signatureHashOf(signatureTextOf(hole, plan));
10032
- const shapeHash = shapeHashOf(shapeOfHole(hole, plan));
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(hole.id, null);
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, hole.id, candidate, result)) return retain(candidate, round === 1 ? "draw:1" : "draw:2");
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, hole.id, escalated, result)) return retain(escalated, "escalate");
10053
- await deps.writeBody(hole.id, null);
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]?.plan.featureId ?? "",
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
- function createModelDrawer(client, models = DEFAULT_FILL_MODELS) {
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: HOLE_SYSTEM_PROMPT,
10243
+ system,
10095
10244
  messages: [{ role: "user", content: context }],
10096
10245
  maxTokens: 4e3,
10097
10246
  effort: "medium"
10098
10247
  });
10099
- return { body: extractCandidateBody(answer.text), origin, model: answer.model, usage: answer.usage };
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 report = checkSpec({ markdown, specPath: entry.spec, generation: project.generation });
10552
- const featureBlock = report.kind === "feature" ? report.block : null;
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 existsSync3, mkdirSync, readFileSync as readFileSync4, readdirSync as readdirSync2, writeFileSync } from "node:fs";
10568
- import { join as join4 } from "node:path";
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, holeId }) => {
10654
- const result = spawnSync("pnpm", ["exec", "vitest", "run", testPath, "-t", holeId], {
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 = readFileSync4(join4(project.root, target.specPath), "utf8");
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 report = checkSpec({ markdown, specPath: target.specPath, generation: target.generation, schemas });
10684
- if (!report.ok || report.block === null) {
10685
- for (const problem2 of report.problems) out.error(formatProblem(problem2));
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 = join4(project.root, target.appPath, target.generation.target.migrations);
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: report.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(existsSync3(migrationsDir) ? readdirSync2(migrationsDir) : [], report.block.contracts.table === void 0 ? featureId : tableNameOf(report.block.contracts.table)),
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(report.behaviors)
10958
+ behaviors: boundBehaviorsOf(report2.behaviors)
10701
10959
  });
10702
10960
  return { plan, folder: target.folder };
10703
10961
  }
10704
10962
  function recordLibraryEntry(libraryDir, entry) {
10705
- mkdirSync(libraryDir, { recursive: true });
10706
- const path = join4(libraryDir, libraryFileNameOf(entry));
10707
- if (!existsSync3(path)) {
10708
- writeFileSync(path, `${JSON.stringify(entry, null, 2)}
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(readFileSync4(path, "utf8")));
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
- writeFileSync(path, `${JSON.stringify({ ...existing, provenance: [...existing.provenance, origin] }, null, 2)}
10973
+ writeFileSync2(path, `${JSON.stringify({ ...existing, provenance: [...existing.provenance, origin] }, null, 2)}
10716
10974
  `, "utf8");
10717
10975
  return "extended";
10718
10976
  }
10719
- async function runFillCommand(project, featureId, argv, out, deps = {}) {
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
- const runner = deps.runner ?? vitestHoleRunner;
10757
- const libraryDir = deps.libraryDir ?? HOLES_LIBRARY_DIR;
10758
- const fillDeps = {
10759
- async writeBody(holeId, body) {
10760
- const logic = readFileSync4(logicPath, "utf8");
10761
- writeFileSync(logicPath, replaceHoleBody(logic, holeId, body ?? ` return todo(${JSON.stringify(holeId)});`), "utf8");
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
- const summary = await fillFeature(inputs, fillDeps, options);
11008
+ }
11009
+ function report(wiring, summary, label) {
11010
+ const { out, featureId } = wiring;
10783
11011
  for (const result of summary.results) {
10784
- const label = result.outcome === "already-filled" ? "d\xE9j\xE0 rempli" : result.outcome === "human" ? "HUMAIN (todo)" : result.outcome;
10785
- out.log(` ${result.holeId} \u2014 ${label}${result.model === null ? "" : ` par ${result.model}`}, ${result.tried} candidat(s) essay\xE9(s)${result.banned > 0 ? `, ${result.banned} interdit(s)` : ""}`);
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} trou(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)" : ""}.`
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
  };