@ogygie/generate 0.32.1 → 0.33.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.
@@ -423,6 +423,8 @@ var ReferenceSchema = z3.strictObject({
423
423
  /** A nullable integer field of the read element carrying the count of references. */
424
424
  countField: FieldNameSchema.optional()
425
425
  });
426
+ var SharedTableSchema = z3.strictObject({ name: TableNameSchema, schema: SchemaNameSchema });
427
+ var SharedTableDeclarationSchema = z3.union([TableNameSchema, SharedTableSchema]);
426
428
  var ContractsSchema = z3.strictObject({
427
429
  /** Path of the schemas module, relative to the feature folder. */
428
430
  module: z3.string().min(1),
@@ -435,9 +437,15 @@ var ContractsSchema = z3.strictObject({
435
437
  key: FieldNameSchema.optional(),
436
438
  slots: z3.record(IdentifierSchema, SlotSchema).optional(),
437
439
  references: z3.record(IdentifierSchema, ReferenceSchema).optional(),
438
- /** D32 precision (M3): tables of the product, outside the block, a view hole may read (`from`) — a referential projection, a shared table. Declared, never created nor written. */
439
- sharedTables: z3.array(TableNameSchema).optional()
440
+ /** D32 precision (M3): tables of the product, outside the block, a view hole may read (`from`) — a referential projection, a shared table. Declared, never created nor written. D60: declared WITH the schema of its rows, an aggregate hole reads it typed. */
441
+ sharedTables: z3.array(SharedTableDeclarationSchema).optional()
440
442
  });
443
+ function sharedTableNameOf(declaration) {
444
+ return typeof declaration === "string" ? declaration : declaration.name;
445
+ }
446
+ function typedSharedTablesOf(contracts) {
447
+ return (contracts.sharedTables ?? []).filter((declaration) => typeof declaration !== "string");
448
+ }
441
449
  var OrderDirectionSchema = z3.enum(["asc", "desc"]);
442
450
  var OrderTupleSchema = z3.tuple([FieldNameSchema, OrderDirectionSchema]);
443
451
  var OrderBySchema = z3.union([
@@ -582,12 +590,36 @@ var ViewHoleSchema = z3.strictObject({
582
590
  value: z3.unknown()
583
591
  })
584
592
  });
593
+ var AggregateHoleSchema = z3.strictObject({
594
+ id: IdentifierSchema,
595
+ kind: z3.literal("aggregate"),
596
+ /** The fields of the input the hole reads — its first parameter is `Pick<input, reads>`; may be empty. */
597
+ reads: z3.array(FieldNameSchema).default([]),
598
+ /** Shared tables declared WITH a schema under contracts.sharedTables, whose rows the hole receives, typed. */
599
+ from: z3.array(TableNameSchema).optional(),
600
+ /** The root field of the read's output the hole writes. */
601
+ writes: FieldNameSchema,
602
+ /** The clock is the only effect an aggregate may declare: it receives its rows, it never reads the database. */
603
+ effects: z3.array(z3.literal("clock")).default([]),
604
+ examples: z3.strictObject({
605
+ /** One complete case per example: the input (its `reads`), the resource rows, the shared rows by table, the value the hole must answer. */
606
+ accept: z3.array(
607
+ z3.strictObject({
608
+ input: ExampleRecordSchema.optional(),
609
+ rows: z3.array(ExampleRecordSchema),
610
+ shared: z3.record(TableNameSchema, z3.array(ExampleRecordSchema)).optional(),
611
+ value: z3.unknown()
612
+ })
613
+ ).min(1)
614
+ })
615
+ });
585
616
  var HoleSchema = z3.discriminatedUnion("kind", [
586
617
  ValidateHoleSchema,
587
618
  TransformHoleSchema,
588
619
  DeriveHoleSchema,
589
620
  ProjectHoleSchema,
590
- ViewHoleSchema
621
+ ViewHoleSchema,
622
+ AggregateHoleSchema
591
623
  ]);
592
624
  function acceptExamplesOf(hole) {
593
625
  return Array.isArray(hole.examples.accept) ? hole.examples.accept : [hole.examples.accept];
@@ -597,7 +629,8 @@ var HOLE_KIND_SERVES = {
597
629
  transform: ["import"],
598
630
  derive: ["create"],
599
631
  project: ["read"],
600
- view: ["read"]
632
+ view: ["read"],
633
+ aggregate: ["read"]
601
634
  };
602
635
  var GenerationBlockShapeSchema = z3.strictObject({
603
636
  contracts: ContractsSchema,
@@ -793,12 +826,28 @@ function checkCrossReferences(block, issue) {
793
826
  }
794
827
  });
795
828
  }
829
+ if (hole.kind === "aggregate") {
830
+ const typed = new Set(typedSharedTablesOf(block.contracts).map((table) => table.name));
831
+ for (const source of hole.from ?? []) {
832
+ if (!typed.has(source)) {
833
+ issue(["holes", index, "from"], `aggregate hole "${hole.id}" reads "${source}", which is not a shared table declared with a schema: declare it as contracts.sharedTables: [{ name: ${source}, schema: <RowSchema> }] (D60) \u2014 a bare name serves view holes only`);
834
+ }
835
+ }
836
+ const reads = new Set(hole.reads);
837
+ hole.examples.accept.forEach((example, position) => {
838
+ const outside = Object.keys(example.input ?? {}).filter((key) => !reads.has(key));
839
+ if (outside.length > 0) issue(["holes", index, "examples"], `examples.accept[${position}].input reads ${outside.join(", ")}, which is not in reads (${hole.reads.join(", ") || "none"})`);
840
+ const unknownTables = Object.keys(example.shared ?? {}).filter((table) => !(hole.from ?? []).includes(table));
841
+ if (unknownTables.length > 0) issue(["holes", index, "examples"], `examples.accept[${position}].shared carries ${unknownTables.join(", ")}, which is not in from (${(hole.from ?? []).join(", ") || "none"})`);
842
+ if (!("value" in example)) issue(["holes", index, "examples"], `examples.accept[${position}] has no value: an aggregate example says what the hole answers for its rows`);
843
+ });
844
+ }
796
845
  if (hole.kind === "view") {
797
846
  const known = /* @__PURE__ */ new Set([
798
847
  ...block.contracts.table === void 0 ? [] : [tableNameOf(block.contracts.table)],
799
848
  ...Object.values(slots).map((slot) => tableNameOf(slot.table)),
800
849
  ...Object.values(references).map((reference) => tableNameOf(reference.table)),
801
- ...block.contracts.sharedTables ?? [],
850
+ ...(block.contracts.sharedTables ?? []).map(sharedTableNameOf),
802
851
  ...viewNames
803
852
  ]);
804
853
  known.delete(hole.name);
@@ -1209,6 +1258,7 @@ function vocabularyOf(operation, block, guards = EMPTY_GUARD_REGISTRY, contractV
1209
1258
  namedValues.add(`${hole.writes} d\xE9riv\xE9`);
1210
1259
  if (hole.error !== void 0) codes.set(hole.error, 400);
1211
1260
  }
1261
+ if (hole?.kind === "aggregate") namedValues.add(`${hole.writes} d\xE9riv\xE9`);
1212
1262
  }
1213
1263
  return { cases, namedValues, phrases, codes, successStatus: SUCCESS_STATUS_BY_KIND[operation.kind] };
1214
1264
  }
@@ -2313,6 +2363,14 @@ function checkBlockAgainstSchemas(input) {
2313
2363
  }
2314
2364
  if (model !== null) checkAdoptedShape(tableRefOf(reference.table), `contracts.references.${name}.table`, model, [], issue);
2315
2365
  }
2366
+ const sharedModels = /* @__PURE__ */ new Map();
2367
+ (block.contracts.sharedTables ?? []).forEach((declaration, position) => {
2368
+ if (typeof declaration === "string") return;
2369
+ const model = exists(declaration.schema, `contracts.sharedTables.${position}.schema`);
2370
+ if (model === null) return;
2371
+ if (model.kind !== "object") issue(`contracts.sharedTables.${position}.schema`, `${declaration.schema} is not an object schema: a shared table's schema describes one row`, "Name a z.object() schema of one row of the shared table (D60).");
2372
+ else sharedModels.set(declaration.name, model);
2373
+ });
2316
2374
  const supersedesMotif = block.motifs.appendOnlySupersedes;
2317
2375
  if (supersedesMotif !== void 0 && resource !== null && resource.kind === "object") {
2318
2376
  const missing = ["supersedes", "supersededBy"].filter((field) => {
@@ -2416,6 +2474,22 @@ function checkBlockAgainstSchemas(input) {
2416
2474
  );
2417
2475
  }
2418
2476
  }
2477
+ if (hole.kind === "aggregate") {
2478
+ const outside = hole.reads.filter((field) => !inputFields.has(field));
2479
+ if (outside.length > 0) issue(`holes.${holeIndex}.reads`, `aggregate hole "${hole.id}" reads ${outside.join(", ")}, not field(s) of ${operation.input}`, "An aggregate hole reads fields of the input of the read it serves (D60).");
2480
+ if (inputFields.has(hole.writes)) issue(`holes.${holeIndex}.writes`, `aggregate hole "${hole.id}" writes "${hole.writes}", which ${operation.input} already carries: an echo of the input is served as is, nothing to compute`, "Write a field of the output the input does not carry (D60).");
2481
+ hole.examples.accept.forEach((example, position) => {
2482
+ const rowsOutside = [...new Set(example.rows.flatMap((row) => Object.keys(row).filter((key) => !resourceFields.has(key))))];
2483
+ if (rowsOutside.length > 0) issue(`holes.${holeIndex}.examples`, `examples.accept[${position}].rows carry ${rowsOutside.join(", ")}, not field(s) of ${block.contracts.resource ?? "the resource"}`, "The rows of an aggregate example are rows of the resource schema, partial allowed: the test completes them (D60).");
2484
+ for (const [table, sharedRows] of Object.entries(example.shared ?? {})) {
2485
+ const model = sharedModels.get(table);
2486
+ if (model === void 0) continue;
2487
+ const fields = new Set(fieldNamesOf(model));
2488
+ const sharedOutside = [...new Set(sharedRows.flatMap((row) => Object.keys(row).filter((key) => !fields.has(key))))];
2489
+ if (sharedOutside.length > 0) issue(`holes.${holeIndex}.examples`, `examples.accept[${position}].shared.${table} carries ${sharedOutside.join(", ")}, not field(s) of its schema`, "The shared rows of an aggregate example are rows of the shared table's schema (D60).");
2490
+ }
2491
+ });
2492
+ }
2419
2493
  if (hole.kind === "derive" && resource !== null) {
2420
2494
  if (!resourceFields.has(hole.writes)) {
2421
2495
  issue(`holes.${holeIndex}.writes`, `derive hole "${hole.id}" writes "${hole.writes}", which is not a field of ${block.contracts.resource}`, "A derive hole writes a field of the resource (D32).");
@@ -2497,12 +2571,29 @@ function checkOutputMapping(operation, index, output, inputFields, resource, blo
2497
2571
  const entries = Object.entries(output.fields);
2498
2572
  if (operation.kind === "read") {
2499
2573
  const collection = readCollectionOf(output, resource);
2574
+ const aggregates = (operation.holes ?? []).map((id) => block.holes.find((candidate) => candidate.id === id)).flatMap((hole) => hole?.kind === "aggregate" ? [hole] : []);
2575
+ const aggregateWrites = new Set(aggregates.map((hole) => hole.writes));
2576
+ for (const hole of aggregates) {
2577
+ if (!(hole.writes in output.fields)) {
2578
+ issue(`holes.${block.holes.indexOf(hole)}.writes`, `aggregate hole "${hole.id}" writes "${hole.writes}", which is not a root field of ${operation.output}`, "An aggregate hole writes a root field of the read's output (D60).");
2579
+ }
2580
+ }
2500
2581
  if (collection === null) {
2501
- issue(
2502
- path,
2503
- `${operation.output} must carry exactly one array whose element covers ${resourceLabel(resource)} \u2014 found ${entries.map(([name]) => name).join(", ")}`,
2504
- "Shape a read output as { <collection>: z.array(ResourceView), ...echoed input fields, ...project fields }."
2582
+ if (aggregates.length === 0) {
2583
+ issue(
2584
+ path,
2585
+ `${operation.output} must carry exactly one array whose element covers ${resourceLabel(resource)} \u2014 found ${entries.map(([name]) => name).join(", ")}`,
2586
+ "Shape a read output as { <collection>: z.array(ResourceView), ...echoed input fields, ...project fields } \u2014 or, for a report computed over the rows (an aggregate read, D60), carry no collection and write every other field with an aggregate hole: { id, kind: aggregate, reads: [input fields], writes: <field>, examples: { accept: [{ input, rows, value }] } }."
2587
+ );
2588
+ return;
2589
+ }
2590
+ const projectRootsOfReport = new Set(
2591
+ (operation.holes ?? []).map((id) => block.holes.find((candidate) => candidate.id === id)).flatMap((hole) => hole?.kind === "project" ? hole.writes.filter((write) => !write.includes("[]")) : [])
2505
2592
  );
2593
+ const others = entries.filter(([name]) => !inputFields.has(name) && !aggregateWrites.has(name) && !projectRootsOfReport.has(name));
2594
+ if (others.length > 0) {
2595
+ issue(path, `${operation.output} carries ${others.map(([name]) => name).join(", ")}: neither an echo of the input nor written by an aggregate hole of "${operation.id}"`, "In an aggregate read (D60) every root field of the output is an echo of the input (same name) or the writes of an aggregate hole the operation lists: declare a hole { kind: aggregate, writes: <field>, \u2026 } for each computed field.");
2596
+ }
2506
2597
  return;
2507
2598
  }
2508
2599
  const projectRoots = new Set(
@@ -2514,7 +2605,7 @@ function checkOutputMapping(operation, index, output, inputFields, resource, blo
2514
2605
  const viewFields2 = new Set(
2515
2606
  (operation.holes ?? []).map((id) => block.holes.find((candidate) => candidate.id === id)).flatMap((hole) => hole?.kind === "view" ? [hole.writes] : [])
2516
2607
  );
2517
- const rootOthers = entries.filter(([name]) => name !== collection.collection && !inputFields.has(name) && !projectRoots.has(name));
2608
+ const rootOthers = entries.filter(([name]) => name !== collection.collection && !inputFields.has(name) && !projectRoots.has(name) && !aggregateWrites.has(name));
2518
2609
  if (rootOthers.length > 0) {
2519
2610
  issue(path, `${operation.output} carries ${rootOthers.map(([name]) => name).join(", ")} at its root: neither an echo of the input nor a field a project hole writes`, "A read output's root holds the collection, echoes of the input and the fields of its project holes.");
2520
2611
  }
@@ -3378,6 +3469,22 @@ function buildFeaturePlan(input) {
3378
3469
  }
3379
3470
  return null;
3380
3471
  };
3472
+ const sharedTables = typedSharedTablesOf(block.contracts).map((declaration) => {
3473
+ const model = modelNamed2(schemas, declaration.schema);
3474
+ const fields = objectFields(model, declaration.schema);
3475
+ const sharedColumns = [...tenantColumns(), ...Object.entries(fields).map(([name, fieldModel2]) => columnOf(name, fieldModel2, void 0))];
3476
+ const typeName = typeNameOfSchema(declaration.schema);
3477
+ return {
3478
+ sqlName: declaration.name,
3479
+ tsName: camelOfSnake(declaration.name),
3480
+ schemaName: declaration.schema,
3481
+ typeName,
3482
+ rowType: `${typeName}Row`,
3483
+ model,
3484
+ fields: Object.entries(fields).map(([name, fieldModel2]) => ({ name, model: fieldModel2, dtoExpression: dtoExpressionOf(name, fieldModel2, sharedColumns.find((column) => column.key === name)?.sqlType, generation.target.database) })),
3485
+ columns: sharedColumns
3486
+ };
3487
+ });
3381
3488
  const holes = block.holes.map((hole) => {
3382
3489
  const carrier = block.operations.find((operation) => isHttpOperation(operation) && (operation.holes ?? []).includes(hole.id));
3383
3490
  invariant(carrier !== void 0 && isHttpOperation(carrier), `hole ${hole.id} has no carrier`);
@@ -3424,6 +3531,29 @@ function buildFeaturePlan(input) {
3424
3531
  if (hole.kind === "project") {
3425
3532
  return { kind: "project", id: hole.id, hole, reads: hole.reads, writes: hole.writes, outputTypeName: typeNameOfSchema(carrier.output), accepts: hole.examples.accept };
3426
3533
  }
3534
+ if (hole.kind === "aggregate") {
3535
+ const outputModel = modelNamed2(schemas, carrier.output);
3536
+ const writesModel2 = outputModel.kind === "object" ? outputModel.fields[hole.writes] : void 0;
3537
+ invariant(writesModel2 !== void 0, `aggregate hole ${hole.id} writes an unknown output field`);
3538
+ const from = (hole.from ?? []).map((name) => {
3539
+ const table = sharedTables.find((candidate) => candidate.sqlName === name);
3540
+ invariant(table !== void 0, `aggregate hole ${hole.id} reads an undeclared shared table ${name}`);
3541
+ return table;
3542
+ });
3543
+ return {
3544
+ kind: "aggregate",
3545
+ id: hole.id,
3546
+ hole,
3547
+ reads: hole.reads,
3548
+ inputTypeName: typeNameOfSchema(carrier.input),
3549
+ resourceTypeName: resourceType,
3550
+ outputTypeName: typeNameOfSchema(carrier.output),
3551
+ writes: hole.writes,
3552
+ writesModel: writesModel2,
3553
+ from,
3554
+ accepts: hole.examples.accept.map((example) => ({ input: example.input ?? {}, rows: example.rows, shared: example.shared ?? {}, value: example.value }))
3555
+ };
3556
+ }
3427
3557
  const element = elementOf(carrier);
3428
3558
  invariant(element !== null && element.model.kind === "object", `view hole ${hole.id}: the read has no element`);
3429
3559
  const writesModel = element.model.fields[hole.writes];
@@ -3473,6 +3603,9 @@ function buildFeaturePlan(input) {
3473
3603
  else if (operation.kind === "import" && model.kind === "number") outputField = name;
3474
3604
  else if (name in inputFields) echoFields.push(name);
3475
3605
  }
3606
+ const aggregateIds = new Set(block.holes.filter((hole) => hole.kind === "aggregate").map((hole) => hole.id));
3607
+ const report2 = operation.kind === "read" && element === null && (operation.holes ?? []).some((id) => aggregateIds.has(id));
3608
+ if (report2) outputField = "";
3476
3609
  invariant(outputField !== void 0, `${operation.output} has no mappable field`);
3477
3610
  const opHoles = (operation.holes ?? []).map((id) => {
3478
3611
  const hole = holeById.get(id);
@@ -3513,6 +3646,8 @@ function buildFeaturePlan(input) {
3513
3646
  deriveHoles: opHoles.filter((hole) => hole.kind === "derive"),
3514
3647
  projectHoles: opHoles.filter((hole) => hole.kind === "project"),
3515
3648
  viewHoles: opHoles.filter((hole) => hole.kind === "view"),
3649
+ aggregateHoles: opHoles.filter((hole) => hole.kind === "aggregate"),
3650
+ report: report2,
3516
3651
  order: orderPlanOf(operation, schemas, resourceFields),
3517
3652
  limit: operation.kind === "read" ? operation.limit ?? null : null,
3518
3653
  range: operation.kind === "read" ? operation.range ?? null : null,
@@ -3585,6 +3720,7 @@ function buildFeaturePlan(input) {
3585
3720
  key: keyField === null || keyColumn === void 0 || keyUniqueName === null ? null : { field: keyField, column: keyColumn, uniqueName: keyUniqueName },
3586
3721
  slots,
3587
3722
  references,
3723
+ sharedTables,
3588
3724
  signed: block.motifs.signedWrite ?? null,
3589
3725
  unique,
3590
3726
  referenced: (() => {
@@ -3724,6 +3860,19 @@ function holeSignatureLines(hole, plan) {
3724
3860
  ];
3725
3861
  }
3726
3862
  if (hole.kind === "view") return [];
3863
+ if (hole.kind === "aggregate") {
3864
+ const picks = hole.reads.length === 0 ? "Record<never, never>" : `Pick<${hole.inputTypeName}, ${hole.reads.map((field) => quote(field)).join(" | ")}>`;
3865
+ const shared = hole.from.length === 0 ? [] : [` shared: { ${hole.from.map((table) => `${table.tsName}: readonly ${table.typeName}[]`).join("; ")} },`];
3866
+ return [
3867
+ `// @hole ${hole.id}`,
3868
+ `${prefix} ${hole.id}(`,
3869
+ ` input: ${picks},`,
3870
+ ` rows: readonly ${hole.resourceTypeName}[],`,
3871
+ ...shared,
3872
+ ...effects,
3873
+ `): ${wrapResult(`${hole.outputTypeName}[${quote(hole.writes)}]`)} {`
3874
+ ];
3875
+ }
3727
3876
  return [
3728
3877
  `// @hole ${hole.id}`,
3729
3878
  `${prefix} ${hole.id}(input: { text: string }${effects.length === 0 ? "" : `, ${effects.map((line) => line.trim().replace(/,$/u, "")).join(", ")}`}): ${wrapResult(`HoleResult<${hole.rowTypeName}[], ${quote(hole.errorCode)}>`)} {`
@@ -3778,8 +3927,14 @@ function emitLogic(plan, existingLogic = null) {
3778
3927
  for (const hole of plan.holes) {
3779
3928
  if (hole.kind === "validate" || hole.kind === "derive") inputTypes.add(hole.inputTypeName);
3780
3929
  if (hole.kind === "project") inputTypes.add(hole.outputTypeName);
3930
+ if (hole.kind === "aggregate") {
3931
+ if (hole.reads.length > 0) inputTypes.add(hole.inputTypeName);
3932
+ inputTypes.add(hole.resourceTypeName);
3933
+ inputTypes.add(hole.outputTypeName);
3934
+ for (const table of hole.from) inputTypes.add(table.typeName);
3935
+ }
3781
3936
  }
3782
- const needsClock = plan.holes.some((hole) => (hole.kind === "validate" || hole.kind === "derive" || hole.kind === "transform") && hole.hole.effects.includes("clock"));
3937
+ const needsClock = plan.holes.some((hole) => (hole.kind === "validate" || hole.kind === "derive" || hole.kind === "transform" || hole.kind === "aggregate") && hole.hole.effects.includes("clock"));
3783
3938
  const needsDb = plan.holes.some((hole) => (hole.kind === "validate" || hole.kind === "derive" || hole.kind === "transform") && hole.hole.effects.includes("db"));
3784
3939
  const hasProject = plan.holes.some((hole) => hole.kind === "project");
3785
3940
  const sections = [];
@@ -3808,7 +3963,8 @@ function emitLogic(plan, existingLogic = null) {
3808
3963
  needsDb ? plan.tenant === null ? `import type { ${plan.databasePort?.typeName ?? "Database"} } from ${quote(plan.databasePort?.importPath ?? "../../db.js")};` : 'import type { TenantDb } from "@ogygie/core/tenant";' : null,
3809
3964
  hasProject ? `import type { ${plan.context.typeName} } from ${quote(plan.context.importPath)};` : null,
3810
3965
  inputTypes.size === 0 ? null : `import type { ${[...inputTypes].sort().join(", ")} } from "./schemas.js";`,
3811
- 'import { accept, refuse, todo, type HoleResult } from "./holes.runtime.gen.js";',
3966
+ // A hole answering a HoleResult needs accept and refuse; an aggregate or a project answers a value — todo alone (D60).
3967
+ plan.holes.some((hole) => hole.kind === "validate" || hole.kind === "derive" || hole.kind === "transform") ? 'import { accept, refuse, todo, type HoleResult } from "./holes.runtime.gen.js";' : 'import { todo } from "./holes.runtime.gen.js";',
3812
3968
  ...hasProject ? [
3813
3969
  "",
3814
3970
  "/** What a project hole may read of the caller: the guard's context without its database (D32). */",
@@ -3902,6 +4058,16 @@ function drizzleViewLines(view, builders) {
3902
4058
  `export type ${view.rowType} = typeof ${view.tsName}.$inferSelect;`
3903
4059
  ];
3904
4060
  }
4061
+ function drizzleSharedLines(table, builders) {
4062
+ for (const column of table.columns) builders.add(builderOf(column));
4063
+ return [
4064
+ `/** READ-ONLY mapping of the shared table ${table.sqlName} (D60): a table of the product outside this feature, read by its aggregate holes through tenantDb; never created nor written here. */`,
4065
+ `export const ${table.tsName} = pgTable(${quote(table.sqlName)}, {`,
4066
+ ...table.columns.map((column) => ` ${column.key}: ${column.drizzle},`),
4067
+ "});",
4068
+ `export type ${table.rowType} = typeof ${table.tsName}.$inferSelect;`
4069
+ ];
4070
+ }
3905
4071
  function tablesOf(plan) {
3906
4072
  return [plan.table, ...plan.slots.map((slot) => slot.table), ...plan.references.flatMap((reference) => reference.table === null ? [] : [reference.table])];
3907
4073
  }
@@ -3945,6 +4111,7 @@ function emitDbSchema(plan) {
3945
4111
  const sections = [];
3946
4112
  for (const table of tables) sections.push("", ...drizzleTableLines(table, builders, tablesByName));
3947
4113
  for (const view of views) sections.push("", ...drizzleViewLines(view, builders));
4114
+ for (const table of plan.sharedTables) sections.push("", ...drizzleSharedLines(table, builders));
3948
4115
  const needsSql = tables.some((table) => table.uniques.some((unique) => unique.where !== null));
3949
4116
  const intent = wrap(
3950
4117
  `Drizzle tables owned by the ${plan.featureId} feature, derived from ${plan.resource.schemaName} (${plan.specPath}, section G\xE9n\xE9ration): tenant-scoped by ${tenantOf(plan).column} (TS key \`${tenantOf(plan).columnKey}\`, the column tenantDb injects and filters).${plan.key === null ? "" : ` The business key ${plan.key.field} is unique per tenant (${plan.key.uniqueName}), the target of every slot and reference foreign key.`}${plan.slots.length === 0 ? "" : ` Slot tables: ${plan.slots.map((slot) => slot.table.sqlName).join(", ")} (cascade with the resource row).`}${plan.references.some((reference) => reference.table !== null) ? ` Reference tables: ${plan.references.flatMap((reference) => reference.table === null ? [] : [reference.table.sqlName]).join(", ")} (a referenced row cannot be deleted).` : ""}${views.length === 0 ? "" : ` Views ${views.map((view) => view.name).join(", ")} are read-only mappings: their SQL is in the migration.`}${plan.appendOnly === null ? "" : ` The append-only key (${plan.appendOnly.key.join(", ")}) is unique among CURRENT rows (superseded_by is null).`} SQL migration: ${plan.migrationPath}.`
@@ -4011,6 +4178,160 @@ function emitMigration(plan, existingMigration = null) {
4011
4178
  return fileOf(sqlGeneratedHeader(plan.generatorVersion), ...intent, "", ...statements);
4012
4179
  }
4013
4180
 
4181
+ // src/emit/hole-tests.ts
4182
+ function deepOverlay(base, overlay) {
4183
+ if (Array.isArray(overlay)) {
4184
+ const baseItems = Array.isArray(base) ? base : [];
4185
+ return overlay.map((item, index) => deepOverlay(baseItems[index] ?? baseItems[0], item));
4186
+ }
4187
+ if (overlay !== null && typeof overlay === "object") {
4188
+ const result = base !== null && typeof base === "object" && !Array.isArray(base) ? { ...base } : {};
4189
+ for (const [key, value] of Object.entries(overlay)) result[key] = deepOverlay(result[key], value);
4190
+ return result;
4191
+ }
4192
+ return overlay;
4193
+ }
4194
+ function sampleUnder(model, overlay) {
4195
+ if (model.kind === "object" && overlay !== null && typeof overlay === "object" && !Array.isArray(overlay)) {
4196
+ const given = overlay;
4197
+ const value = {};
4198
+ for (const [name, field] of Object.entries(model.fields)) {
4199
+ if (name in given) value[name] = sampleUnder(field, given[name]);
4200
+ else if (!field.optional) {
4201
+ const sample = sampleOf(field, [name]);
4202
+ value[name] = sample.ok ? sample.value : null;
4203
+ }
4204
+ }
4205
+ return value;
4206
+ }
4207
+ if (model.kind === "array" && Array.isArray(overlay)) return overlay.map((item) => sampleUnder(model.element, item));
4208
+ return overlay;
4209
+ }
4210
+ function projectInputOf(plan, carrier, partial) {
4211
+ void plan;
4212
+ return sampleUnder(carrier.outputModel, partial);
4213
+ }
4214
+ var HOLE_TESTS_CLOCK_INSTANT = "2026-01-01T00:00:00.000Z";
4215
+ function holeDeclaresClock(hole) {
4216
+ return (hole.hole.effects ?? []).includes("clock");
4217
+ }
4218
+ function constantNameOf(identifier) {
4219
+ return identifier.replace(/([a-z0-9])([A-Z])/gu, "$1_$2").toUpperCase();
4220
+ }
4221
+ function emitHoleTests(plan) {
4222
+ const blocks = [];
4223
+ const testedHoles = plan.holes.filter((hole) => hole.kind !== "view");
4224
+ const needsOutputs = /* @__PURE__ */ new Set();
4225
+ const needsExamples = /* @__PURE__ */ new Set();
4226
+ const needsClock = testedHoles.some((hole) => hole.kind !== "project" && holeDeclaresClock(hole));
4227
+ for (const hole of testedHoles) {
4228
+ const withEffects = (argument) => hole.kind !== "project" && holeDeclaresClock(hole) ? `${argument}, CLOCK` : argument;
4229
+ const call = (argument) => isAsyncHole(hole) ? `await ${hole.id}(${withEffects(argument)})` : `${hole.id}(${withEffects(argument)})`;
4230
+ blocks.push("", ` describe(${quote(hole.id)}, () => {`);
4231
+ if (hole.kind === "validate") {
4232
+ hole.accepts.forEach((accept, index) => {
4233
+ blocks.push(
4234
+ ` it(${quote(`accepte l'exemple ${index + 1} : ${JSON.stringify(accept)}`)}, async () => {`,
4235
+ ` expect((${call(JSON.stringify(accept))}).ok).toBe(true);`,
4236
+ " });"
4237
+ );
4238
+ });
4239
+ blocks.push(
4240
+ ` it(${quote(`refuse ${JSON.stringify(hole.reject)} avec ${hole.errorCode}${hole.hole.suggestedFix === true ? " et un suggestedFix" : ""}`)}, async () => {`,
4241
+ ` const result = ${call(JSON.stringify(hole.reject))};`,
4242
+ " expect(result.ok).toBe(false);",
4243
+ " if (result.ok) return;",
4244
+ ` expect(result.code).toBe(${quote(hole.errorCode)});`,
4245
+ " expect(result.suggestedFix.length).toBeGreaterThan(0);",
4246
+ " });"
4247
+ );
4248
+ } else if (hole.kind === "derive") {
4249
+ hole.accepts.forEach((accept, index) => {
4250
+ blocks.push(
4251
+ ` it(${quote(`d\xE9rive ${JSON.stringify(accept.output)} de l'exemple ${index + 1} : ${JSON.stringify(accept.input)}`)}, async () => {`,
4252
+ ` const result = ${call(JSON.stringify(accept.input))};`,
4253
+ " expect(result.ok).toBe(true);",
4254
+ " if (!result.ok) return;",
4255
+ ` expect(result.value).toEqual(${JSON.stringify(accept.output)});`,
4256
+ " });"
4257
+ );
4258
+ });
4259
+ if (hole.reject !== null && hole.errorCode !== null) {
4260
+ blocks.push(
4261
+ ` it(${quote(`refuse ${JSON.stringify(hole.reject)} avec ${hole.errorCode}`)}, async () => {`,
4262
+ ` const result = ${call(JSON.stringify(hole.reject))};`,
4263
+ " expect(result.ok).toBe(false);",
4264
+ " if (result.ok) return;",
4265
+ ` expect(result.code).toBe(${quote(hole.errorCode)});`,
4266
+ " expect(result.suggestedFix.length).toBeGreaterThan(0);",
4267
+ " });"
4268
+ );
4269
+ }
4270
+ } else if (hole.kind === "project") {
4271
+ const carrier = plan.operations.find((operation) => operation.projectHoles.some((candidate) => candidate.id === hole.id));
4272
+ if (carrier === void 0) throw new Error(`generation plan: project hole ${hole.id} has no carrier`);
4273
+ needsOutputs.add(carrier.outputSchema);
4274
+ hole.accepts.forEach((accept, index) => {
4275
+ blocks.push(
4276
+ ` it(${quote(`forme la sortie pour le contexte ${JSON.stringify(accept.context)} (exemple ${index + 1})`)}, () => {`,
4277
+ ` const input = ${carrier.outputSchema}.parse(${JSON.stringify(projectInputOf(plan, carrier, accept.input))});`,
4278
+ ` expect(${hole.id}(${JSON.stringify(accept.context)}, input)).toMatchObject(${JSON.stringify(accept.output)});`,
4279
+ " });"
4280
+ );
4281
+ });
4282
+ } else if (hole.kind === "aggregate") {
4283
+ const constant = `EXAMPLES_OF_${constantNameOf(hole.id)}`;
4284
+ needsExamples.add(constant);
4285
+ hole.accepts.forEach((accept, index) => {
4286
+ blocks.push(
4287
+ ` it(${quote(`rend ${JSON.stringify(accept.value)} pour l'exemple ${index + 1} (${accept.rows.length} ligne(s))`)}, async () => {`,
4288
+ ` const example = ${constant}[${index}];`,
4289
+ ' if (example === undefined) throw new Error("example missing");',
4290
+ ` expect(${call(`example.input, example.rows${hole.from.length === 0 ? "" : ", example.shared"}`)}).toEqual(example.value);`,
4291
+ " });"
4292
+ );
4293
+ });
4294
+ } else {
4295
+ blocks.push(
4296
+ ` it("accepte le fichier exemple et rend ses lignes", async () => {`,
4297
+ ` const result = ${call(`{ text: ${JSON.stringify(hole.hole.examples.accept.input)} }`)};`,
4298
+ " expect(result.ok).toBe(true);",
4299
+ " if (!result.ok) return;",
4300
+ ` expect(result.value).toEqual(${JSON.stringify(hole.hole.examples.accept.output)});`,
4301
+ " });",
4302
+ ` it(${quote(`refuse le fichier illisible avec ${hole.errorCode} et sa ligne`)}, async () => {`,
4303
+ ` const result = ${call(`{ text: ${JSON.stringify(hole.hole.examples.reject.input)} }`)};`,
4304
+ " expect(result.ok).toBe(false);",
4305
+ " if (result.ok) return;",
4306
+ ` expect(result.code).toBe(${quote(hole.errorCode)});`,
4307
+ ` expect(result.line).toBe(${JSON.stringify(hole.hole.examples.reject.line ?? 1)});`,
4308
+ " expect(result.suggestedFix.length).toBeGreaterThan(0);",
4309
+ " });"
4310
+ );
4311
+ }
4312
+ blocks.push(" });");
4313
+ }
4314
+ return fileOf(
4315
+ tsGeneratedHeader(plan.generatorVersion),
4316
+ intentBlock(
4317
+ wrap(
4318
+ `Pure tests of the holes of ${plan.featureId}, derived from their examples (D23, D32): each accept example passes, the reject example answers the hole's code. Red while a body reads todo; what a candidate body is run against (D24).${plan.holes.length !== testedHoles.length ? " The view holes are proven on the bench, not here." : ""}`
4319
+ )
4320
+ ),
4321
+ 'import { describe, expect, it } from "vitest";',
4322
+ needsClock ? 'import { fixedClock } from "@ogygie/core/clock";' : null,
4323
+ `import { ${testedHoles.map((hole) => hole.id).sort().join(", ")} } from "./logic.js";`,
4324
+ needsOutputs.size === 0 ? null : `import { ${[...needsOutputs].sort().join(", ")} } from "./schemas.js";`,
4325
+ needsExamples.size === 0 ? null : `import { ${[...needsExamples].sort().join(", ")} } from "./factories.gen.js";`,
4326
+ needsClock ? "" : null,
4327
+ needsClock ? `const CLOCK = fixedClock(${quote(HOLE_TESTS_CLOCK_INSTANT)});` : null,
4328
+ "",
4329
+ `describe(${quote(`${plan.featureId} \u2014 trous`)}, () => {`,
4330
+ ...blocks,
4331
+ "});"
4332
+ );
4333
+ }
4334
+
4014
4335
  // src/hole-shape.ts
4015
4336
  import { createHash } from "node:crypto";
4016
4337
  import { readdirSync, readFileSync as readFileSync2 } from "node:fs";
@@ -4073,7 +4394,7 @@ function normalizedTypeOf(model) {
4073
4394
  return `${base}${model.nullable ? "?" : ""}`;
4074
4395
  }
4075
4396
  var HoleShapeSchema = z6.strictObject({
4076
- kind: z6.enum(["validate", "transform", "derive", "project", "view", "custom-view"]),
4397
+ kind: z6.enum(["validate", "transform", "derive", "project", "view", "aggregate", "custom-view"]),
4077
4398
  /** Normalized types of the fields read, by position. */
4078
4399
  reads: z6.array(z6.string()),
4079
4400
  /** Transform only: normalized types of the columns, by position. */
@@ -4107,7 +4428,7 @@ function hash16(text) {
4107
4428
  }
4108
4429
  function rowModelOf2(hole, plan) {
4109
4430
  const carrier = plan.operations.find(
4110
- (operation) => operation.validateHoles.some((candidate) => candidate.id === hole.id) || operation.transformHole?.id === hole.id || operation.deriveHoles.some((candidate) => candidate.id === hole.id)
4431
+ (operation) => operation.validateHoles.some((candidate) => candidate.id === hole.id) || operation.transformHole?.id === hole.id || operation.deriveHoles.some((candidate) => candidate.id === hole.id) || operation.aggregateHoles.some((candidate) => candidate.id === hole.id)
4111
4432
  );
4112
4433
  if (carrier === void 0) return null;
4113
4434
  if (carrier.kind === "import") {
@@ -4143,6 +4464,9 @@ function shapeOfHole(hole, plan) {
4143
4464
  if (hole.kind === "view") {
4144
4465
  return { kind: "view", reads: hole.from.map(() => "table"), columns: hole.columns.map((column) => column.sqlType), codeRole: "none", returns: `view:${hole.shape}` };
4145
4466
  }
4467
+ if (hole.kind === "aggregate") {
4468
+ return { kind: "aggregate", reads: readTypes(hole.reads), columns: ["rows", ...hole.from.map(() => "table")], codeRole: "none", returns: normalizedTypeOf(hole.writesModel) };
4469
+ }
4146
4470
  return {
4147
4471
  kind: "transform",
4148
4472
  reads: ["string:text"],
@@ -4166,7 +4490,7 @@ function signatureHashOf(signature) {
4166
4490
  }
4167
4491
  function bodyTemplateOf(body, hole) {
4168
4492
  const replacements = [];
4169
- if (hole.kind === "validate" || hole.kind === "derive") {
4493
+ if (hole.kind === "validate" || hole.kind === "derive" || hole.kind === "aggregate") {
4170
4494
  hole.reads.forEach((name, index) => replacements.push([name, `{{read:${index}}}`]));
4171
4495
  } else if (hole.kind === "transform") {
4172
4496
  hole.columns.forEach((name, index) => replacements.push([name, `{{column:${index}}}`]));
@@ -4185,7 +4509,7 @@ function bodyTemplateOf(body, hole) {
4185
4509
  }
4186
4510
  function instantiateBody(template, hole) {
4187
4511
  let body = template;
4188
- if (hole.kind === "validate" || hole.kind === "derive") {
4512
+ if (hole.kind === "validate" || hole.kind === "derive" || hole.kind === "aggregate") {
4189
4513
  hole.reads.forEach((name, index) => {
4190
4514
  body = body.replaceAll(`{{read:${index}}}`, name);
4191
4515
  });
@@ -4235,143 +4559,6 @@ function libraryFileNameOf(entry) {
4235
4559
  return `${entry.signatureHash}.json`;
4236
4560
  }
4237
4561
 
4238
- // src/emit/hole-tests.ts
4239
- function deepOverlay(base, overlay) {
4240
- if (Array.isArray(overlay)) {
4241
- const baseItems = Array.isArray(base) ? base : [];
4242
- return overlay.map((item, index) => deepOverlay(baseItems[index] ?? baseItems[0], item));
4243
- }
4244
- if (overlay !== null && typeof overlay === "object") {
4245
- const result = base !== null && typeof base === "object" && !Array.isArray(base) ? { ...base } : {};
4246
- for (const [key, value] of Object.entries(overlay)) result[key] = deepOverlay(result[key], value);
4247
- return result;
4248
- }
4249
- return overlay;
4250
- }
4251
- function sampleUnder(model, overlay) {
4252
- if (model.kind === "object" && overlay !== null && typeof overlay === "object" && !Array.isArray(overlay)) {
4253
- const given = overlay;
4254
- const value = {};
4255
- for (const [name, field] of Object.entries(model.fields)) {
4256
- if (name in given) value[name] = sampleUnder(field, given[name]);
4257
- else if (!field.optional) {
4258
- const sample = sampleOf(field, [name]);
4259
- value[name] = sample.ok ? sample.value : null;
4260
- }
4261
- }
4262
- return value;
4263
- }
4264
- if (model.kind === "array" && Array.isArray(overlay)) return overlay.map((item) => sampleUnder(model.element, item));
4265
- return overlay;
4266
- }
4267
- function projectInputOf(plan, carrier, partial) {
4268
- void plan;
4269
- return sampleUnder(carrier.outputModel, partial);
4270
- }
4271
- var HOLE_TESTS_CLOCK_INSTANT = "2026-01-01T00:00:00.000Z";
4272
- function holeDeclaresClock(hole) {
4273
- return (hole.hole.effects ?? []).includes("clock");
4274
- }
4275
- function emitHoleTests(plan) {
4276
- const blocks = [];
4277
- const testedHoles = plan.holes.filter((hole) => hole.kind !== "view");
4278
- const needsOutputs = /* @__PURE__ */ new Set();
4279
- const needsClock = testedHoles.some((hole) => hole.kind !== "project" && holeDeclaresClock(hole));
4280
- for (const hole of testedHoles) {
4281
- const withEffects = (argument) => hole.kind !== "project" && holeDeclaresClock(hole) ? `${argument}, CLOCK` : argument;
4282
- const call = (argument) => isAsyncHole(hole) ? `await ${hole.id}(${withEffects(argument)})` : `${hole.id}(${withEffects(argument)})`;
4283
- blocks.push("", ` describe(${quote(hole.id)}, () => {`);
4284
- if (hole.kind === "validate") {
4285
- hole.accepts.forEach((accept, index) => {
4286
- blocks.push(
4287
- ` it(${quote(`accepte l'exemple ${index + 1} : ${JSON.stringify(accept)}`)}, async () => {`,
4288
- ` expect((${call(JSON.stringify(accept))}).ok).toBe(true);`,
4289
- " });"
4290
- );
4291
- });
4292
- blocks.push(
4293
- ` it(${quote(`refuse ${JSON.stringify(hole.reject)} avec ${hole.errorCode}${hole.hole.suggestedFix === true ? " et un suggestedFix" : ""}`)}, async () => {`,
4294
- ` const result = ${call(JSON.stringify(hole.reject))};`,
4295
- " expect(result.ok).toBe(false);",
4296
- " if (result.ok) return;",
4297
- ` expect(result.code).toBe(${quote(hole.errorCode)});`,
4298
- " expect(result.suggestedFix.length).toBeGreaterThan(0);",
4299
- " });"
4300
- );
4301
- } else if (hole.kind === "derive") {
4302
- hole.accepts.forEach((accept, index) => {
4303
- blocks.push(
4304
- ` it(${quote(`d\xE9rive ${JSON.stringify(accept.output)} de l'exemple ${index + 1} : ${JSON.stringify(accept.input)}`)}, async () => {`,
4305
- ` const result = ${call(JSON.stringify(accept.input))};`,
4306
- " expect(result.ok).toBe(true);",
4307
- " if (!result.ok) return;",
4308
- ` expect(result.value).toEqual(${JSON.stringify(accept.output)});`,
4309
- " });"
4310
- );
4311
- });
4312
- if (hole.reject !== null && hole.errorCode !== null) {
4313
- blocks.push(
4314
- ` it(${quote(`refuse ${JSON.stringify(hole.reject)} avec ${hole.errorCode}`)}, async () => {`,
4315
- ` const result = ${call(JSON.stringify(hole.reject))};`,
4316
- " expect(result.ok).toBe(false);",
4317
- " if (result.ok) return;",
4318
- ` expect(result.code).toBe(${quote(hole.errorCode)});`,
4319
- " expect(result.suggestedFix.length).toBeGreaterThan(0);",
4320
- " });"
4321
- );
4322
- }
4323
- } else if (hole.kind === "project") {
4324
- const carrier = plan.operations.find((operation) => operation.projectHoles.some((candidate) => candidate.id === hole.id));
4325
- if (carrier === void 0) throw new Error(`generation plan: project hole ${hole.id} has no carrier`);
4326
- needsOutputs.add(carrier.outputSchema);
4327
- hole.accepts.forEach((accept, index) => {
4328
- blocks.push(
4329
- ` it(${quote(`forme la sortie pour le contexte ${JSON.stringify(accept.context)} (exemple ${index + 1})`)}, () => {`,
4330
- ` const input = ${carrier.outputSchema}.parse(${JSON.stringify(projectInputOf(plan, carrier, accept.input))});`,
4331
- ` expect(${hole.id}(${JSON.stringify(accept.context)}, input)).toMatchObject(${JSON.stringify(accept.output)});`,
4332
- " });"
4333
- );
4334
- });
4335
- } else {
4336
- blocks.push(
4337
- ` it("accepte le fichier exemple et rend ses lignes", async () => {`,
4338
- ` const result = ${call(`{ text: ${JSON.stringify(hole.hole.examples.accept.input)} }`)};`,
4339
- " expect(result.ok).toBe(true);",
4340
- " if (!result.ok) return;",
4341
- ` expect(result.value).toEqual(${JSON.stringify(hole.hole.examples.accept.output)});`,
4342
- " });",
4343
- ` it(${quote(`refuse le fichier illisible avec ${hole.errorCode} et sa ligne`)}, async () => {`,
4344
- ` const result = ${call(`{ text: ${JSON.stringify(hole.hole.examples.reject.input)} }`)};`,
4345
- " expect(result.ok).toBe(false);",
4346
- " if (result.ok) return;",
4347
- ` expect(result.code).toBe(${quote(hole.errorCode)});`,
4348
- ` expect(result.line).toBe(${JSON.stringify(hole.hole.examples.reject.line ?? 1)});`,
4349
- " expect(result.suggestedFix.length).toBeGreaterThan(0);",
4350
- " });"
4351
- );
4352
- }
4353
- blocks.push(" });");
4354
- }
4355
- return fileOf(
4356
- tsGeneratedHeader(plan.generatorVersion),
4357
- intentBlock(
4358
- wrap(
4359
- `Pure tests of the holes of ${plan.featureId}, derived from their examples (D23, D32): each accept example passes, the reject example answers the hole's code. Red while a body reads todo; what a candidate body is run against (D24).${plan.holes.length !== testedHoles.length ? " The view holes are proven on the bench, not here." : ""}`
4360
- )
4361
- ),
4362
- 'import { describe, expect, it } from "vitest";',
4363
- needsClock ? 'import { fixedClock } from "@ogygie/core/clock";' : null,
4364
- `import { ${testedHoles.map((hole) => hole.id).sort().join(", ")} } from "./logic.js";`,
4365
- needsOutputs.size === 0 ? null : `import { ${[...needsOutputs].sort().join(", ")} } from "./schemas.js";`,
4366
- needsClock ? "" : null,
4367
- needsClock ? `const CLOCK = fixedClock(${quote(HOLE_TESTS_CLOCK_INSTANT)});` : null,
4368
- "",
4369
- `describe(${quote(`${plan.featureId} \u2014 trous`)}, () => {`,
4370
- ...blocks,
4371
- "});"
4372
- );
4373
- }
4374
-
4375
4562
  // src/emit/mssql/sql-types.ts
4376
4563
  function sqlColumnName(plan, column) {
4377
4564
  return plan.target.naming === "pascal" ? column.columnName.charAt(0).toUpperCase() + column.columnName.slice(1) : column.columnName;
@@ -4647,6 +4834,33 @@ function mssqlStatements(plan) {
4647
4834
  parameters: []
4648
4835
  });
4649
4836
  }
4837
+ for (const table of plan.sharedTables) {
4838
+ statements.push({
4839
+ key: `select${table.typeName}Rows`,
4840
+ kind: "selectShared",
4841
+ text: `SELECT ${selectList(plan, "t", table.columns)} FROM ${qualifiedTable(plan, table.sqlName)} AS t`,
4842
+ table: table.sqlName,
4843
+ rowType: table.rowType,
4844
+ parameters: []
4845
+ });
4846
+ }
4847
+ const arrangingRows = plan.operations.every((operation) => operation.kind !== "create") && plan.operations.some((operation) => operation.aggregateHoles.length > 0);
4848
+ if (arrangingRows) {
4849
+ const arrangeOf = (sqlName, columns, rowType, typeName) => {
4850
+ const written = [...columns.filter((column) => column.key === "id"), ...columns.filter((column) => column.key !== "id")];
4851
+ return {
4852
+ key: `arrange${typeName}Row`,
4853
+ kind: "insert",
4854
+ text: `INSERT INTO ${qualifiedTable(plan, sqlName)} (${written.map((column) => sqlColumnName(plan, column)).join(", ")}) OUTPUT ${outputList(plan, "INSERTED", columns)} VALUES (${written.map((column) => `@${column.key}`).join(", ")})`,
4855
+ table: sqlName,
4856
+ rowType,
4857
+ parameters: written.map((column) => column.key),
4858
+ written: written.map((column) => column.key)
4859
+ };
4860
+ };
4861
+ statements.push(arrangeOf(resource.sqlName, resource.columns, resource.rowType, plan.resource.typeName));
4862
+ for (const table of plan.sharedTables) statements.push(arrangeOf(table.sqlName, table.columns, table.rowType, table.typeName));
4863
+ }
4650
4864
  if (keyField !== null && keyColumn !== null) {
4651
4865
  const keyName = sqlColumnName(plan, keyColumn);
4652
4866
  statements.push({
@@ -4812,6 +5026,10 @@ function rowInterfaceLines(name, columns) {
4812
5026
  function parametersLiteral(entries) {
4813
5027
  return `{ ${entries.map(([name, expression]) => `${name}: ${expression}`).join(", ")} }`;
4814
5028
  }
5029
+ function parameterValue(column, expression) {
5030
+ if (column.sqlType === "timestamptz") return expression;
5031
+ return expression;
5032
+ }
4815
5033
  function notFoundLines(plan, codeExpression, indent) {
4816
5034
  const key = plan.key?.field ?? "id";
4817
5035
  return [
@@ -4851,6 +5069,34 @@ function viewReadLines(plan, view, statement) {
4851
5069
  "}"
4852
5070
  ];
4853
5071
  }
5072
+ function sharedReadLines(plan, table, statement) {
5073
+ return [
5074
+ `/** The rows of the shared table ${table.sqlName} (D60) \u2014 read-only, the product owns it. */`,
5075
+ `export async function ${statement.key}(db: ${plan.databasePort?.typeName ?? "Database"}): Promise<${table.rowType}[]> {`,
5076
+ ` return db.query<${table.rowType}>(SQL.${statement.key});`,
5077
+ "}"
5078
+ ];
5079
+ }
5080
+ function arrangeLines(plan, statement, typeName, columns, withId) {
5081
+ const written = statement.written ?? [];
5082
+ const valueOf = (field) => {
5083
+ if (field === "id") return "crypto.randomUUID()";
5084
+ const column = columns.find((candidate) => candidate.key === field);
5085
+ return column === void 0 ? `row.${field}` : parameterValue(column, `row.${field}`);
5086
+ };
5087
+ return [
5088
+ `/** TESTS ONLY (D60): rows of ${statement.table} arranged at their schema \u2014 the feature never writes them, they come from elsewhere. */`,
5089
+ `export async function arrange${typeName}Rows(db: ${plan.databasePort?.typeName ?? "Database"}, rows: readonly ${withId ? `Omit<${typeName}, "id">` : typeName}[]): Promise<${statement.rowType}[]> {`,
5090
+ ` const inserted: ${statement.rowType}[] = [];`,
5091
+ " for (const row of rows) {",
5092
+ ` const [written] = await db.query<${statement.rowType}>(SQL.${statement.key}, ${parametersLiteral(written.map((field) => [field, valueOf(field)]))});`,
5093
+ " if (written === undefined) throwOgygieError(insertReturnedNothing());",
5094
+ " inserted.push(written);",
5095
+ " }",
5096
+ " return inserted;",
5097
+ "}"
5098
+ ];
5099
+ }
4854
5100
  function referenceLines(plan, table, column, countStatement, insertStatement) {
4855
5101
  const database = plan.databasePort?.typeName ?? "Database";
4856
5102
  const typeName = table.rowType.replace(/Row$/u, "");
@@ -5061,6 +5307,7 @@ function emitMssqlRepository(plan) {
5061
5307
  for (const slot of plan.slots) rows.push("", ...rowInterfaceLines(slot.table.rowType, slot.table.columns));
5062
5308
  for (const reference of plan.references) if (reference.table !== null) rows.push("", ...rowInterfaceLines(reference.table.rowType, reference.table.columns));
5063
5309
  for (const view of views) rows.push("", ...rowInterfaceLines(view.rowType, view.columns));
5310
+ for (const table of plan.sharedTables) rows.push("", ...rowInterfaceLines(table.rowType, table.columns));
5064
5311
  const sqlLines = ["/** Every statement of this feature, `@named` parameters, read back under camelCase aliases \u2014 the stub answers these very texts (D33). */", "export const SQL = {", ...statements.map((statement) => ` ${statement.key}: ${quote(statement.text)},`), "} as const;"];
5065
5312
  const sections = [];
5066
5313
  for (const operation of plan.operations.filter((candidate) => candidate.kind === "read")) sections.push("", ...readFunctionLines(plan, operation, statementNamed(statements, `select${operation.pascalId}`)));
@@ -5071,8 +5318,14 @@ function emitMssqlRepository(plan) {
5071
5318
  sections.push("", ...referenceLines(plan, reference.table, reference.column, statementNamed(statements, `count${typeName}`), statementNamed(statements, `insert${typeName}`)));
5072
5319
  }
5073
5320
  for (const view of views) sections.push("", ...viewReadLines(plan, view, statementNamed(statements, `select${view.rowType.replace(/Row$/u, "")}`)));
5074
- if (plan.key !== null) sections.push("", ...loadByKeyLines(plan, statementNamed(statements, `load${plan.resource.typeName}ByKey`)));
5321
+ for (const table of plan.sharedTables) sections.push("", ...sharedReadLines(plan, table, statementNamed(statements, `select${table.typeName}Rows`)));
5075
5322
  const create = plan.operations.find((operation) => operation.kind === "create");
5323
+ const arranging = create === void 0 && plan.operations.some((operation) => operation.aggregateHoles.length > 0);
5324
+ if (arranging) {
5325
+ sections.push("", ...arrangeLines(plan, statementNamed(statements, `arrange${plan.resource.typeName}Row`), plan.resource.typeName, plan.table.columns, true));
5326
+ for (const table of plan.sharedTables) sections.push("", ...arrangeLines(plan, statementNamed(statements, `arrange${table.typeName}Row`), table.typeName, table.columns, false));
5327
+ }
5328
+ if (plan.key !== null) sections.push("", ...loadByKeyLines(plan, statementNamed(statements, `load${plan.resource.typeName}ByKey`)));
5076
5329
  const update = plan.operations.find((operation) => operation.kind === "update");
5077
5330
  const setOrClear = plan.operations.find((operation) => operation.kind === "setOrClear");
5078
5331
  const remove = plan.operations.find((operation) => operation.kind === "delete");
@@ -5081,7 +5334,7 @@ function emitMssqlRepository(plan) {
5081
5334
  if (setOrClear !== void 0) sections.push("", ...setOrClearFunctionLines(plan, setOrClear, statements));
5082
5335
  if (remove !== void 0) sections.push("", ...deleteFunctionLines(plan, remove, statements));
5083
5336
  const inputTypes = [...new Set([create?.inputType, update?.inputType].filter((name) => name !== void 0))].sort();
5084
- const resourceTypes = [...new Set([plan.key === null ? null : plan.resource.typeName, ...setOrClear?.slot === null || setOrClear?.slot === void 0 ? [] : [setOrClear.slot.typeName]].filter((name) => name !== null))].sort();
5337
+ const resourceTypes = [...new Set([plan.key === null ? null : plan.resource.typeName, ...arranging ? [plan.resource.typeName, ...plan.sharedTables.map((table) => table.typeName)] : [], ...setOrClear?.slot === null || setOrClear?.slot === void 0 ? [] : [setOrClear.slot.typeName]].filter((name) => name !== null))].sort();
5085
5338
  return fileOf(
5086
5339
  tsGeneratedHeader(plan.generatorVersion),
5087
5340
  intentBlock(
@@ -5162,6 +5415,8 @@ function emitMssqlStub(plan) {
5162
5415
  const tables = tablesOf(plan);
5163
5416
  const views = plan.holes.filter((hole) => hole.kind === "view");
5164
5417
  const tableOf = (sqlName) => {
5418
+ const shared = plan.sharedTables.find((candidate) => candidate.sqlName === sqlName);
5419
+ if (shared !== void 0) return { sqlName: shared.sqlName, tsName: shared.tsName, rowType: shared.rowType, newType: `New${shared.typeName}`, mode: "adopted", hasId: false, primaryKey: [], alter: [], columns: shared.columns, indexes: [], uniques: [], foreignKeys: [] };
5165
5420
  const table = tables.find((candidate) => candidate.sqlName === sqlName);
5166
5421
  if (table === void 0) throw new Error(`generation plan: stub has no table ${sqlName}`);
5167
5422
  return table;
@@ -5181,7 +5436,8 @@ function emitMssqlStub(plan) {
5181
5436
  cases.push(` const rows = tables.${statement.table};`, ...compareLines(statement));
5182
5437
  break;
5183
5438
  }
5184
- case "selectView": {
5439
+ case "selectView":
5440
+ case "selectShared": {
5185
5441
  rowTypes.add(statement.rowType);
5186
5442
  cases.push(` return [...tables.${statement.table}];`);
5187
5443
  break;
@@ -5316,9 +5572,9 @@ function emitMssqlStub(plan) {
5316
5572
  }
5317
5573
  cases.push(" }");
5318
5574
  }
5319
- const tableFields = [...tables.map((table) => ` ${table.sqlName}: ${table.rowType}[];`), ...views.map((view) => ` ${view.name}: ${view.rowType}[];`)];
5320
- const emptyFields = [...tables.map((table) => `${table.sqlName}: []`), ...views.map((view) => `${view.name}: []`)];
5321
- const importedRowTypes = [.../* @__PURE__ */ new Set([...tables.map((table) => table.rowType), ...views.map((view) => view.rowType)])].sort();
5575
+ const tableFields = [...tables.map((table) => ` ${table.sqlName}: ${table.rowType}[];`), ...views.map((view) => ` ${view.name}: ${view.rowType}[];`), ...plan.sharedTables.map((table) => ` ${table.sqlName}: ${table.rowType}[];`)];
5576
+ const emptyFields = [...tables.map((table) => `${table.sqlName}: []`), ...views.map((view) => `${view.name}: []`), ...plan.sharedTables.map((table) => `${table.sqlName}: []`)];
5577
+ const importedRowTypes = [.../* @__PURE__ */ new Set([...tables.map((table) => table.rowType), ...views.map((view) => view.rowType), ...plan.sharedTables.map((table) => table.rowType)])].sort();
5322
5578
  return fileOf(
5323
5579
  tsGeneratedHeader(plan.generatorVersion),
5324
5580
  intentBlock(
@@ -5428,8 +5684,9 @@ function sampleOrThrow(model, variant, label) {
5428
5684
  }
5429
5685
  return sample.value;
5430
5686
  }
5431
- function acceptOverlay(holes, derives, variant) {
5687
+ function acceptOverlay(holes, derives, variant, aggregates = []) {
5432
5688
  const overlay = {};
5689
+ for (const hole of aggregates) Object.assign(overlay, hole.accepts[Math.min(variant, hole.accepts.length - 1)]?.input ?? {});
5433
5690
  for (const hole of holes) {
5434
5691
  const example = hole.accepts[Math.min(variant, hole.accepts.length - 1)] ?? {};
5435
5692
  Object.assign(overlay, example);
@@ -5443,7 +5700,8 @@ function acceptOverlay(holes, derives, variant) {
5443
5700
  function validInputsOf(plan, operation) {
5444
5701
  const holes = operation.kind === "import" ? [] : operation.validateHoles;
5445
5702
  const derives = operation.deriveHoles;
5446
- const variants = Math.max(1, ...holes.map((hole) => hole.accepts.length), ...derives.map((hole) => hole.accepts.length));
5703
+ const aggregates = operation.aggregateHoles;
5704
+ const variants = Math.max(1, ...holes.map((hole) => hole.accepts.length), ...derives.map((hole) => hole.accepts.length), ...aggregates.map((hole) => hole.accepts.length));
5447
5705
  if (operation.kind === "update" && operation.inputModel.kind === "object") {
5448
5706
  return Array.from({ length: variants }, (_, variant) => {
5449
5707
  const body = {};
@@ -5455,7 +5713,7 @@ function validInputsOf(plan, operation) {
5455
5713
  });
5456
5714
  }
5457
5715
  const base = sampleOrThrow(operation.inputModel, 0, operation.inputSchema);
5458
- return Array.from({ length: variants }, (_, variant) => ({ ...base, ...acceptOverlay(holes, derives, variant) }));
5716
+ return Array.from({ length: variants }, (_, variant) => ({ ...base, ...acceptOverlay(holes, derives, variant, aggregates) }));
5459
5717
  }
5460
5718
  function derivedOf(operation, variant = 0) {
5461
5719
  const derived = {};
@@ -5490,7 +5748,7 @@ function sampleOfSqlType(sqlType) {
5490
5748
  if (sqlType === "uuid") return "00000000-0000-4000-8000-000000000001";
5491
5749
  return "arranged";
5492
5750
  }
5493
- function constantNameOf(typeName) {
5751
+ function constantNameOf2(typeName) {
5494
5752
  return typeName.replace(/([a-z0-9])([A-Z])/gu, "$1_$2").toUpperCase();
5495
5753
  }
5496
5754
  function sampleValueOf(model, variant = 0) {
@@ -5511,13 +5769,13 @@ function emitFactories(plan) {
5511
5769
  sections.push(
5512
5770
  "",
5513
5771
  `/** Structurally invalid ${operation.inputType}: the required field \`${invalidImport.removedField}\` is missing. */`,
5514
- `export const INVALID_${constantNameOf(operation.inputType)}: Record<string, unknown> = ${JSON.stringify(invalidImport.value)};`
5772
+ `export const INVALID_${constantNameOf2(operation.inputType)}: Record<string, unknown> = ${JSON.stringify(invalidImport.value)};`
5515
5773
  );
5516
5774
  }
5517
5775
  continue;
5518
5776
  }
5519
5777
  typeImports.add(operation.inputType);
5520
- const constant = `VALID_${constantNameOf(operation.inputType)}S`;
5778
+ const constant = `VALID_${constantNameOf2(operation.inputType)}S`;
5521
5779
  const factory = `valid${operation.inputType}`;
5522
5780
  sections.push(
5523
5781
  "",
@@ -5537,13 +5795,13 @@ function emitFactories(plan) {
5537
5795
  sections.push(
5538
5796
  "",
5539
5797
  `/** Structurally invalid ${operation.inputType}: the required field \`${invalid.removedField}\` is missing. */`,
5540
- `export const INVALID_${constantNameOf(operation.inputType)}: Record<string, unknown> = ${JSON.stringify(invalid.value)};`
5798
+ `export const INVALID_${constantNameOf2(operation.inputType)}: Record<string, unknown> = ${JSON.stringify(invalid.value)};`
5541
5799
  );
5542
5800
  } else if (operation.kind === "update") {
5543
5801
  sections.push(
5544
5802
  "",
5545
5803
  `/** Structurally invalid ${operation.inputType}: a partial body with no field at all \u2014 the contract refuses it. */`,
5546
- `export const INVALID_${constantNameOf(operation.inputType)}: Record<string, unknown> = {};`
5804
+ `export const INVALID_${constantNameOf2(operation.inputType)}: Record<string, unknown> = {};`
5547
5805
  );
5548
5806
  }
5549
5807
  }
@@ -5575,6 +5833,31 @@ function emitFactories(plan) {
5575
5833
  } else if (create !== null) {
5576
5834
  sections.push("", "/** No derive hole: an arranged row carries nothing beyond its input. */", "export const DERIVED_OF_VALID_CREATES: readonly Record<string, unknown>[] = [{}];");
5577
5835
  }
5836
+ for (const hole of plan.holes) {
5837
+ if (hole.kind !== "aggregate") continue;
5838
+ const carrier = plan.operations.find((operation) => operation.aggregateHoles.some((candidate) => candidate.id === hole.id));
5839
+ if (carrier === void 0) continue;
5840
+ typeImports.add(hole.resourceTypeName);
5841
+ typeImports.add(hole.outputTypeName);
5842
+ if (hole.reads.length > 0) typeImports.add(hole.inputTypeName);
5843
+ for (const table of hole.from) typeImports.add(table.typeName);
5844
+ const inputType = hole.reads.length === 0 ? "Record<never, never>" : `Pick<${hole.inputTypeName}, ${hole.reads.map(quote).join(" | ")}>`;
5845
+ const sharedType = hole.from.length === 0 ? "Record<never, never>" : `{ ${hole.from.map((table) => `${table.tsName}: readonly ${table.typeName}[]`).join("; ")} }`;
5846
+ const examples = hole.accepts.map((accept) => {
5847
+ const fullInput = sampleUnder(carrier.inputModel, accept.input);
5848
+ const input = Object.fromEntries(hole.reads.map((field) => [field, fullInput[field]]));
5849
+ const rows = accept.rows.map((row) => sampleUnder(plan.resource.model, row));
5850
+ const shared = Object.fromEntries(hole.from.map((table) => [table.tsName, (accept.shared[table.sqlName] ?? []).map((row) => sampleUnder(table.model, row))]));
5851
+ return { input, rows, shared, value: accept.value };
5852
+ });
5853
+ sections.push(
5854
+ "",
5855
+ `/** The complete examples of the aggregate hole ${hole.id} (D60): input pick, rows and shared rows completed under their schemas, the value expected \u2014 one fixture for the pure test and the behaviour test. */`,
5856
+ `export const EXAMPLES_OF_${constantNameOf(hole.id)}: readonly { input: ${inputType}; rows: readonly ${hole.resourceTypeName}[]; shared: ${sharedType}; value: ${hole.outputTypeName}[${quote(hole.writes)}] }[] = [`,
5857
+ ...examples.map((example) => ` ${JSON.stringify(example)},`),
5858
+ "];"
5859
+ );
5860
+ }
5578
5861
  for (const reference of plan.references) {
5579
5862
  if (reference.table === null) continue;
5580
5863
  const model = reference.schemaName === null ? void 0 : plan.schemaModels[reference.schemaName];
@@ -5640,14 +5923,14 @@ function emitFactories(plan) {
5640
5923
  sections.push(
5641
5924
  "",
5642
5925
  `/** The input ${hole.id} rejects (its reject example), as an overlay of a valid input. */`,
5643
- `export const REJECT_EXAMPLE_OF_${constantNameOf(hole.id)} = ${JSON.stringify(hole.reject)} as const;`
5926
+ `export const REJECT_EXAMPLE_OF_${constantNameOf2(hole.id)} = ${JSON.stringify(hole.reject)} as const;`
5644
5927
  );
5645
5928
  }
5646
5929
  if (hole.kind === "derive" && hole.reject !== null) {
5647
5930
  sections.push(
5648
5931
  "",
5649
5932
  `/** The input ${hole.id} refuses to derive from (its reject example), as an overlay of a valid input. */`,
5650
- `export const REJECT_EXAMPLE_OF_${constantNameOf(hole.id)} = ${JSON.stringify(hole.reject)} as const;`
5933
+ `export const REJECT_EXAMPLE_OF_${constantNameOf2(hole.id)} = ${JSON.stringify(hole.reject)} as const;`
5651
5934
  );
5652
5935
  }
5653
5936
  }
@@ -5670,7 +5953,7 @@ function markdownGeneratedHeader(version) {
5670
5953
  }
5671
5954
  function carriersOf(hole, plan) {
5672
5955
  return plan.operations.filter(
5673
- (operation) => operation.validateHoles.some((candidate) => candidate.id === hole.id) || operation.transformHole?.id === hole.id || operation.deriveHoles.some((candidate) => candidate.id === hole.id) || operation.projectHoles.some((candidate) => candidate.id === hole.id) || operation.viewHoles.some((candidate) => candidate.id === hole.id)
5956
+ (operation) => operation.validateHoles.some((candidate) => candidate.id === hole.id) || operation.transformHole?.id === hole.id || operation.deriveHoles.some((candidate) => candidate.id === hole.id) || operation.projectHoles.some((candidate) => candidate.id === hole.id) || operation.viewHoles.some((candidate) => candidate.id === hole.id) || operation.aggregateHoles.some((candidate) => candidate.id === hole.id)
5674
5957
  ).map((operation) => `\`${operation.id}\` (${operation.method} ${operation.path})`);
5675
5958
  }
5676
5959
  function tick(name) {
@@ -5712,6 +5995,17 @@ function contractLines(hole, plan, shape) {
5712
5995
  "## Champs",
5713
5996
  ...hole.writes.map((path) => `- ${tick(path)} : \xE9crit par le trou`)
5714
5997
  ];
5998
+ case "aggregate":
5999
+ return [
6000
+ `- lit : ${hole.reads.length === 0 ? "rien de l'entr\xE9e" : `${hole.reads.map(tick).join(", ")} de ${tick(hole.inputTypeName)}`}, les lignes de la ressource (${tick(`readonly ${hole.resourceTypeName}[]`)} : TOUTES celles du tenant, au sch\xE9ma, dans l'ordre de la lecture)${hole.from.length === 0 ? "" : ` et les lignes des tables partag\xE9es ${hole.from.map((table) => `${tick(table.sqlName)} (${tick(`shared.${table.tsName}: readonly ${table.typeName}[]`)})`).join(", ")}`}`,
6001
+ `- rend : ${tick(hole.writes)} de la sortie \u2014 ${tick(`${hole.outputTypeName}["${hole.writes}"]`)}, la VALEUR, jamais un ${tick("HoleResult")} : une agr\xE9gation ne refuse pas (D60)`,
6002
+ `- effets : ${effects(hole.hole.effects)}`,
6003
+ "- filtre : c'est le trou qui garde les lignes que l'entr\xE9e demande (une p\xE9riode, un site) ; le d\xE9p\xF4t ne filtre pas",
6004
+ "",
6005
+ "## Champs",
6006
+ ...hole.reads.map((name, index) => `- ${tick(name)} : ${shape.reads[index] ?? "unknown"}`),
6007
+ `- ${tick(hole.writes)} (\xE9crit) : ${shape.returns}`
6008
+ ];
5715
6009
  case "view":
5716
6010
  return [
5717
6011
  `- corps : du SQL, dans la migration ${tick(plan.migrationPath)} entre ${tick(`-- @hole ${hole.id}`)} et ${tick(`-- @end-hole ${hole.id}`)} \u2014 ${tick(`create or replace view "${hole.name}" as \u2026`)}`,
@@ -5748,6 +6042,10 @@ function exampleLines(hole, plan) {
5748
6042
  ];
5749
6043
  case "project":
5750
6044
  return hole.accepts.map((accept) => `- contexte ${tick(JSON.stringify(accept.context))}, entr\xE9e ${tick(JSON.stringify(accept.input))} \u2192 ${tick(JSON.stringify(accept.output))}`);
6045
+ case "aggregate":
6046
+ return hole.accepts.map(
6047
+ (accept, index) => `- exemple ${index + 1} : entr\xE9e ${tick(JSON.stringify(accept.input))}, lignes ${tick(JSON.stringify(accept.rows))}${Object.keys(accept.shared).length === 0 ? "" : `, partag\xE9es ${tick(JSON.stringify(accept.shared))}`} \u2192 ${tick(hole.writes)} = ${tick(JSON.stringify(accept.value))} (les lignes sont compl\xE9t\xE9es sous leur sch\xE9ma par le test)`
6048
+ );
5751
6049
  case "view":
5752
6050
  return [
5753
6051
  `- lignes de case d'UNE ligne de ressource : ${tick(JSON.stringify(hole.hole.examples.rows))}`,
@@ -5786,7 +6084,7 @@ function emitHoleDoc(plan, hole, library) {
5786
6084
  "## Contrat",
5787
6085
  `- appel\xE9 par : ${carriersOf(hole, plan).join(", ")}`,
5788
6086
  ...contractLines(hole, plan, shape),
5789
- `- forme normalis\xE9e : ${shape.kind} [${(hole.kind === "transform" || hole.kind === "project" || hole.kind === "view" ? shape.columns : shape.reads).join(", ")}] \u2192 ${shape.returns}, code ${shape.codeRole} \u2014 shapeHash \`${shapeHash}\``,
6087
+ `- forme normalis\xE9e : ${shape.kind} [${(hole.kind === "transform" || hole.kind === "project" || hole.kind === "view" || hole.kind === "aggregate" ? shape.columns : shape.reads).join(", ")}] \u2192 ${shape.returns}, code ${shape.codeRole} \u2014 shapeHash \`${shapeHash}\``,
5790
6088
  "",
5791
6089
  hole.kind === "view" ? "## Colonnes (g\xE9n\xE9r\xE9es, le SQL doit les produire)" : "## Signature (g\xE9n\xE9r\xE9e)",
5792
6090
  hole.kind === "view" ? "```sql" : "```ts",
@@ -5810,8 +6108,8 @@ function emitHoleDoc(plan, hole, library) {
5810
6108
  "- aucune table hors de celles list\xE9es en `lit` ; aucune \xE9criture ; `null` quand rien n'est pos\xE9 (un crit\xE8re sans cible ne compte pas pour z\xE9ro)",
5811
6109
  "- ne touche rien hors des marqueurs : le reste de la migration est r\xE9g\xE9n\xE9r\xE9"
5812
6110
  ] : [
5813
- hole.kind === "project" ? "- pur : lit le contexte re\xE7u et la sortie re\xE7ue, rend une sortie du m\xEAme type ; jamais la base ni l'horloge" : "- pur hors de ses effets d\xE9clar\xE9s : ne lit ni horloge ni base ni r\xE9seau au-del\xE0 des param\xE8tres re\xE7us (AGENTS.md principe 5)",
5814
- hole.kind === "project" ? "- rend la sortie, jamais une exception" : "- r\xE9pond par `accept(...)` ou `refuse(code, cause, suggestedFix[, line])` du runtime ; jamais par une exception",
6111
+ hole.kind === "project" ? "- pur : lit le contexte re\xE7u et la sortie re\xE7ue, rend une sortie du m\xEAme type ; jamais la base ni l'horloge" : hole.kind === "aggregate" ? "- pur hors de l'horloge quand elle est d\xE9clar\xE9e : lit l'entr\xE9e re\xE7ue, les lignes re\xE7ues et les lignes partag\xE9es re\xE7ues, rien d'autre (AGENTS.md principe 5) ; les dates sont des cha\xEEnes ISO, \xE0 comparer comme telles" : "- pur hors de ses effets d\xE9clar\xE9s : ne lit ni horloge ni base ni r\xE9seau au-del\xE0 des param\xE8tres re\xE7us (AGENTS.md principe 5)",
6112
+ hole.kind === "project" ? "- rend la sortie, jamais une exception" : hole.kind === "aggregate" ? "- rend la valeur, jamais une exception ; une agr\xE9gation sur z\xE9ro ligne rend la valeur neutre que la spec dit (0, [], null)" : "- r\xE9pond par `accept(...)` ou `refuse(code, cause, suggestedFix[, line])` du runtime ; jamais par une exception",
5815
6113
  "- aucun `any`, aucune d\xE9pendance nouvelle, identifiants et commentaires en anglais",
5816
6114
  "- ne touche rien hors du corps : signature, marqueurs et imports sont r\xE9g\xE9n\xE9r\xE9s"
5817
6115
  ]
@@ -5821,7 +6119,7 @@ function emitHoleDoc(plan, hole, library) {
5821
6119
 
5822
6120
  // src/emit/tests.ts
5823
6121
  var UUID_PATTERN = "/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u";
5824
- function constantNameOf2(typeName) {
6122
+ function constantNameOf3(typeName) {
5825
6123
  return typeName.replace(/([a-z0-9])([A-Z])/gu, "$1_$2").toUpperCase();
5826
6124
  }
5827
6125
  function invariant2(condition, message) {
@@ -5868,7 +6166,7 @@ function harnessLines(plan, options) {
5868
6166
  const create = createOperation(plan);
5869
6167
  const imports = [
5870
6168
  `import { ${plan.tenant === null ? "afterEach, beforeEach" : "afterAll, beforeAll"}, describe, expect, it } from "vitest";`,
5871
- ...options.arranging ? ['import { fixedClock } from "@ogygie/core/clock";'] : [],
6169
+ ...options.arranging && create !== null ? ['import { fixedClock } from "@ogygie/core/clock";'] : [],
5872
6170
  `import type { ${plan.context.typeName} } from ${quote(plan.context.importPath)};`,
5873
6171
  `import { ${plan.testBench.exportName} } from ${quote(plan.testBench.importPath)};`,
5874
6172
  `import { ${[...fixtureNames].sort().join(", ")} } from ${quote(fixturesImportPath ?? "")};`
@@ -5936,6 +6234,33 @@ function harnessLines(plan, options) {
5936
6234
  " return String(a) < String(b) ? -1 : 1;",
5937
6235
  "}"
5938
6236
  ];
6237
+ const arrangingReport = options.arranging && options.report === true && create === null && plan.operations.some((operation) => operation.aggregateHoles.length > 0);
6238
+ if (arrangingReport) {
6239
+ const handleType = plan.tenant === null ? `db: ${plan.databasePort?.typeName ?? "Database"}` : `context: ${plan.context.typeName}`;
6240
+ const handleName = plan.tenant === null ? "db" : "context";
6241
+ imports.push(
6242
+ `import type { ${plan.table.rowType} } from ${plan.target.database === "mssql-raw" ? '"./repository.gen.js"' : '"./db.schema.gen.js"'};`,
6243
+ `import { ${["arrange" + plan.resource.typeName + "Rows", ...plan.sharedTables.map((table) => `arrange${table.typeName}Rows`)].sort().join(", ")} } from "./repository.gen.js";`,
6244
+ `import type { ${[plan.resource.typeName, ...plan.sharedTables.map((table) => table.typeName)].sort().join(", ")} } from "./schemas.js";`
6245
+ );
6246
+ if (plan.tenant === null && plan.databasePort !== null) imports.push(`import type { ${plan.databasePort.typeName} } from ${quote(plan.databasePort.importPath)};`);
6247
+ helpers.push(
6248
+ "",
6249
+ "/** Rows arranged at their schema (D60): the block has no create, the tests put their rows on the bench through the repository's arrangement. */",
6250
+ `async function arrangeRows(${handleType}, rows: readonly Omit<${plan.resource.typeName}, "id">[]): Promise<${plan.table.rowType}[]> {`,
6251
+ ` return arrange${plan.resource.typeName}Rows(${handleName}, rows);`,
6252
+ "}"
6253
+ );
6254
+ if (plan.sharedTables.length > 0) {
6255
+ helpers.push(
6256
+ "",
6257
+ "/** The shared rows of an aggregate example (D60), arranged in their tables before the request. */",
6258
+ `async function arrangeShared(${handleType}, shared: { ${plan.sharedTables.map((table) => `${table.tsName}?: readonly ${table.typeName}[]`).join("; ")} }): Promise<void> {`,
6259
+ ...plan.sharedTables.map((table) => ` if (shared.${table.tsName} !== undefined) await arrange${table.typeName}Rows(${handleName}, shared.${table.tsName});`),
6260
+ "}"
6261
+ );
6262
+ }
6263
+ }
5939
6264
  if (options.arranging && create !== null) {
5940
6265
  imports.push(
5941
6266
  plan.target.database === "mssql-raw" ? `import { insert${plan.resource.typeName}, type ${plan.table.rowType} } from "./repository.gen.js";` : `import type { ${plan.table.rowType} } from "./db.schema.gen.js";`,
@@ -6034,7 +6359,7 @@ function emitContractTests(plan) {
6034
6359
  );
6035
6360
  }
6036
6361
  const invalid = operation.kind === "delete" ? { ok: false, reason: "no body" } : invalidSampleOf(operation.inputModel);
6037
- const invalidConstant = `INVALID_${constantNameOf2(operation.inputType)}`;
6362
+ const invalidConstant = `INVALID_${constantNameOf3(operation.inputType)}`;
6038
6363
  const hasInvalid = invalid.ok || operation.kind === "update";
6039
6364
  if (hasInvalid) {
6040
6365
  factoryImports.add(invalidConstant);
@@ -6148,7 +6473,14 @@ function behaviorTestLines(plan, item, factoryImports, schemaImports, repository
6148
6473
  referenced: false,
6149
6474
  key: "ARRANGED_KEY",
6150
6475
  snapshotBefore: behavior.expectations.some((expectation) => expectation.raw === "rien n'est \xE9crit"),
6151
- needsReplacement: false
6476
+ needsReplacement: false,
6477
+ sharedExample: null,
6478
+ bindArranged: !operation.report
6479
+ };
6480
+ const examplesOf = (aggregate) => {
6481
+ const constant = `EXAMPLES_OF_${constantNameOf(aggregate.id)}`;
6482
+ useFactory(constant);
6483
+ return constant;
6152
6484
  };
6153
6485
  const useFactory = (name) => {
6154
6486
  factoryImports.add(name);
@@ -6175,11 +6507,21 @@ function behaviorTestLines(plan, item, factoryImports, schemaImports, repository
6175
6507
  if (key === `with:${guard.entry.valid.case}`) continue;
6176
6508
  if (key === "with:entr\xE9e valide") continue;
6177
6509
  if (key === "with:entr\xE9e invalide") {
6178
- const constant = `INVALID_${constantNameOf2(operation.inputType)}`;
6510
+ const constant = `INVALID_${constantNameOf3(operation.inputType)}`;
6179
6511
  useFactory(constant);
6180
6512
  derivation.input = constant;
6181
6513
  continue;
6182
6514
  }
6515
+ const firstAggregate = operation.aggregateHoles[0] ?? null;
6516
+ if (operation.kind === "read" && operation.motif === null && createFactory === null && firstAggregate !== null) {
6517
+ if (key === "with:lignes") {
6518
+ const constant = examplesOf(firstAggregate);
6519
+ derivation.arrangements.push(`...(${constant}[0]?.rows ?? [])`);
6520
+ if (firstAggregate.from.length > 0) derivation.sharedExample = `${constant}[0]?.shared ?? {}`;
6521
+ continue;
6522
+ }
6523
+ if (key === "without:ligne") continue;
6524
+ }
6183
6525
  if (operation.kind === "read" && operation.motif === null && createFactory !== null) {
6184
6526
  if (key === "with:lignes") {
6185
6527
  derivation.arrangements.push(`${createFactory}(0)`, `${createFactory}(1)`);
@@ -6294,7 +6636,7 @@ function behaviorTestLines(plan, item, factoryImports, schemaImports, repository
6294
6636
  const refusedByHole = hole !== null && behavior.code !== null && ((hole.kind === "validate" || hole.kind === "transform" || hole.kind === "derive") && hole.errorCode === behavior.code);
6295
6637
  if (hole !== null && refusedByHole) {
6296
6638
  if ((hole.kind === "validate" || hole.kind === "derive" && hole.reject !== null) && inputFactory !== null) {
6297
- const constant = `REJECT_EXAMPLE_OF_${constantNameOf2(hole.id)}`;
6639
+ const constant = `REJECT_EXAMPLE_OF_${constantNameOf3(hole.id)}`;
6298
6640
  useFactory(constant);
6299
6641
  derivation.overrides.push(`...${constant}`);
6300
6642
  } else if (hole.kind === "transform") {
@@ -6314,6 +6656,16 @@ function behaviorTestLines(plan, item, factoryImports, schemaImports, repository
6314
6656
  derivation.needsReplacement = true;
6315
6657
  derivation.expectedIds = "[arranged[0]?.id, replacement.id]";
6316
6658
  }
6659
+ if (hole !== null && hole.kind === "aggregate") {
6660
+ const constant = examplesOf(hole);
6661
+ derivation.arrangements = [`...(${constant}[0]?.rows ?? [])`];
6662
+ derivation.sharedExample = hole.from.length === 0 ? null : `${constant}[0]?.shared ?? {}`;
6663
+ if (inputFactory === null) derivation.input = `(${constant}[0]?.input ?? {})`;
6664
+ else {
6665
+ useFactory(inputFactory);
6666
+ derivation.input = `{ ...${inputFactory}(), ...(${constant}[0]?.input ?? {}) }`;
6667
+ }
6668
+ }
6317
6669
  if (createFactory !== null && derivation.arrangements.length > 0) useFactory(createFactory);
6318
6670
  if (inputFactory !== null && derivation.input !== null && derivation.input.startsWith(inputFactory)) useFactory(inputFactory);
6319
6671
  if (inputFactory !== null && (derivation.overrides.length > 0 || derivation.queryOverrides.length > 0)) {
@@ -6330,8 +6682,9 @@ function behaviorTestLines(plan, item, factoryImports, schemaImports, repository
6330
6682
  }
6331
6683
  const handle = arrangingHandle(plan, "member");
6332
6684
  if (derivation.arrangements.length > 0) {
6333
- lines.push(` const arranged = await arrangeRows(${handle}, [${derivation.arrangements.join(", ")}]);`);
6685
+ lines.push(` ${derivation.bindArranged ? "const arranged = " : ""}await arrangeRows(${handle}, [${derivation.arrangements.join(", ")}]);`);
6334
6686
  }
6687
+ if (derivation.sharedExample !== null) lines.push(` await arrangeShared(${handle}, ${derivation.sharedExample});`);
6335
6688
  if (derivation.needsReplacement && createFactory !== null) {
6336
6689
  lines.push(` const replacement = await insert${plan.resource.typeName}(${handle}, ${createFactory}(0, { supersedes: arranged[0]?.id })${plan.signed === null ? "" : ", ARRANGED_SIGNATURE"}, CLOCK);`);
6337
6690
  }
@@ -6442,6 +6795,9 @@ function behaviorTestLines(plan, item, factoryImports, schemaImports, repository
6442
6795
  const target = signatureTarget();
6443
6796
  const expected = value === "renouvel\xE9e" ? `${plan.signature?.exportName ?? "signerOf"}(${derivation.session}.context)` : "ARRANGED_SIGNATURE.by";
6444
6797
  lines.push(` expect(${target}?.${plan.signed.by}).toBe(${expected});`);
6798
+ } else if (value.endsWith(" d\xE9riv\xE9") && operation.aggregateHoles.some((candidate) => value === `${candidate.writes} d\xE9riv\xE9`)) {
6799
+ const writer = operation.aggregateHoles.find((candidate) => value === `${candidate.writes} d\xE9riv\xE9`);
6800
+ if (writer !== void 0) lines.push(` expect(output.${subPath}).toEqual(${examplesOf(writer)}[0]?.value);`);
6445
6801
  } else if (value.endsWith(" d\xE9riv\xE9") && operation.deriveHoles.some((candidate) => value === `${candidate.writes} d\xE9riv\xE9`)) {
6446
6802
  const field = value.slice(0, -" d\xE9riv\xE9".length);
6447
6803
  lines.push(` expect(valuesAt(${root}, ${quote(subPath)})).toEqual([DERIVED_OF_VALID_CREATES[0]?.[${quote(field)}]]);`);
@@ -6502,7 +6858,7 @@ function behaviorTestLines(plan, item, factoryImports, schemaImports, repository
6502
6858
  return lines;
6503
6859
  }
6504
6860
  function emitBehaviorTests(plan) {
6505
- const { imports, helpers } = harnessLines(plan, { arranging: true });
6861
+ const { imports, helpers } = harnessLines(plan, { arranging: true, report: true });
6506
6862
  const factoryImports = /* @__PURE__ */ new Set();
6507
6863
  const schemaImports = /* @__PURE__ */ new Set();
6508
6864
  const repositoryImports = /* @__PURE__ */ new Set();
@@ -7034,6 +7390,39 @@ function referenceLines2(plan, reference) {
7034
7390
  "}"
7035
7391
  ];
7036
7392
  }
7393
+ function sharedReadLines2(plan, table) {
7394
+ return [
7395
+ `/** The rows of the shared table ${table.sqlName} (D60) for the tenant, through its read-only mapping \u2014 what the aggregate holes receive, at their schema. */`,
7396
+ `export async function select${table.typeName}Rows(ctx: ${plan.context.typeName}): Promise<${table.rowType}[]> {`,
7397
+ ` return tenantDb(ctx).select(${table.tsName}).all();`,
7398
+ "}"
7399
+ ];
7400
+ }
7401
+ function insertExpressionOf(field) {
7402
+ if (field.model.kind === "string" && field.model.format === "datetime") {
7403
+ return field.model.nullable ? `row.${field.name} === null ? null : new Date(row.${field.name})` : `new Date(row.${field.name})`;
7404
+ }
7405
+ return `row.${field.name}`;
7406
+ }
7407
+ function arrangeLines2(plan, target) {
7408
+ const valuesType = target.newType === null ? `typeof ${target.tsName}.$inferInsert` : target.newType;
7409
+ return [
7410
+ `/** TESTS ONLY (D60): rows of ${target.sqlTable} arranged at their schema \u2014 the feature never writes them, they come from elsewhere; the tests of a block without create put their rows on the bench here. */`,
7411
+ `export async function arrange${target.typeName}Rows(ctx: ${plan.context.typeName}, rows: readonly ${target.withId ? `Omit<${target.typeName}, "id">` : target.typeName}[]): Promise<${target.rowType}[]> {`,
7412
+ ` const inserted: ${target.rowType}[] = [];`,
7413
+ " for (const row of rows) {",
7414
+ ` const values: Omit<${valuesType}, ${quote(tenantOf(plan).columnKey)}> = {`,
7415
+ ...target.withId ? [" id: crypto.randomUUID(),"] : [],
7416
+ ...target.fields.filter((field) => !(target.withId && field.name === "id")).map((field) => ` ${field.name}: ${insertExpressionOf(field)},`),
7417
+ " };",
7418
+ ` const [written] = await tenantDb(ctx).insert(${target.tsName}).values(values).returning();`,
7419
+ " if (written === undefined) throwOgygieError(insertReturnedNothing());",
7420
+ " inserted.push(written);",
7421
+ " }",
7422
+ " return inserted;",
7423
+ "}"
7424
+ ];
7425
+ }
7037
7426
  function viewReadLines2(plan, view) {
7038
7427
  return [
7039
7428
  `/** The rows of the view ${view.name} (hole ${view.id}, D32) for the tenant, through its read-only mapping. */`,
@@ -7064,16 +7453,23 @@ function emitRepository(plan) {
7064
7453
  }
7065
7454
  const queryTypes = reads.filter((operation) => operation.motif?.method === "currentByKey").map((operation) => operation.inputType);
7066
7455
  const inputTypes = [...new Set([create?.inputType, update?.inputType, plan.importMotif?.rowType, ...queryTypes].filter((name) => name !== void 0))].sort();
7067
- const resourceTypes = [...new Set([plan.key === null ? null : plan.resource.typeName, ...setOrClear?.slot === null || setOrClear?.slot === void 0 ? [] : [setOrClear.slot.typeName]].filter((name) => name !== null))].sort();
7456
+ const arranging = create === void 0 && plan.operations.some((operation) => operation.aggregateHoles.length > 0);
7457
+ const resourceTypes = [...new Set([plan.key === null ? null : plan.resource.typeName, ...arranging ? [plan.resource.typeName, ...plan.sharedTables.map((table) => table.typeName)] : [], ...setOrClear?.slot === null || setOrClear?.slot === void 0 ? [] : [setOrClear.slot.typeName]].filter((name) => name !== null))].sort();
7068
7458
  const tableImports = [`${plan.table.tsName}, type ${plan.table.newType}, type ${plan.table.rowType}`];
7069
7459
  for (const slot of plan.slots) tableImports.push(`${slot.table.tsName}, type ${slot.table.newType}, type ${slot.table.rowType}`);
7070
7460
  for (const reference of plan.references) if (reference.table !== null) tableImports.push(`${reference.table.tsName}, type ${reference.table.newType}, type ${reference.table.rowType}`);
7071
7461
  for (const view of views) tableImports.push(`${view.tsName}, type ${view.rowType}`);
7462
+ for (const table of plan.sharedTables) tableImports.push(`${table.tsName}, type ${table.rowType}`);
7072
7463
  const sections = [];
7073
7464
  for (const operation of reads) sections.push("", ...readFunctionLines2(plan, operation));
7074
7465
  for (const slot of plan.slots) sections.push("", ...slotReadLines2(plan, slot));
7075
7466
  for (const reference of plan.references) sections.push("", ...referenceLines2(plan, reference));
7076
7467
  for (const view of views) sections.push("", ...viewReadLines2(plan, view));
7468
+ for (const table of plan.sharedTables) sections.push("", ...sharedReadLines2(plan, table));
7469
+ if (create === void 0 && plan.operations.some((operation) => operation.aggregateHoles.length > 0)) {
7470
+ sections.push("", ...arrangeLines2(plan, { sqlTable: plan.table.sqlName, tsName: plan.table.tsName, rowType: plan.table.rowType, newType: plan.table.newType, typeName: plan.resource.typeName, fields: plan.resource.fields, withId: true }));
7471
+ for (const table of plan.sharedTables) sections.push("", ...arrangeLines2(plan, { sqlTable: table.sqlName, tsName: table.tsName, rowType: table.rowType, newType: null, typeName: table.typeName, fields: table.fields, withId: false }));
7472
+ }
7077
7473
  if (plan.key !== null) sections.push("", ...loadByKeyLines2(plan));
7078
7474
  if (create !== void 0) sections.push("", ...insertFunctionLines2(plan, create));
7079
7475
  if (update !== void 0) sections.push("", ...updateFunctionLines2(plan, update));
@@ -7340,7 +7736,51 @@ function emitService(plan) {
7340
7736
  const signatureLine = plan.signed === null ? [] : [` const signature: WriteSignature = { by: ${plan.signature?.exportName ?? "signerOf"}(deps.ctx), at: deps.clock.now() };`];
7341
7737
  const signatureArgument = plan.signed === null ? "" : ", signature";
7342
7738
  if (plan.signed !== null) repositoryImports.add("type WriteSignature");
7739
+ const projectionLines = (operation) => {
7740
+ if (operation.projectHoles.length === 0) return [" return computed;"];
7741
+ const lines = [];
7742
+ if (plan.tenant === null) lines.push(" const projection = deps.ctx;");
7743
+ else lines.push(" const { db: _db, ...projection } = deps.ctx;", " void _db;");
7744
+ let current = "computed";
7745
+ for (const hole of operation.projectHoles) {
7746
+ lines.push(` const ${hole.id}Output = ${hole.id}(projection, ${current});`);
7747
+ current = `${hole.id}Output`;
7748
+ }
7749
+ lines.push(` return ${current};`);
7750
+ return lines;
7751
+ };
7752
+ const reportBodyLines = (operation) => {
7753
+ const selectName = `select${operation.pascalId}`;
7754
+ repositoryImports.add(selectName);
7755
+ const lines = [` const rows = (await ${selectName}(${handle})).map(${dto});`];
7756
+ const sharedUsed = /* @__PURE__ */ new Map();
7757
+ for (const hole of operation.aggregateHoles) for (const table of hole.from) sharedUsed.set(table.sqlName, table);
7758
+ for (const table of sharedUsed.values()) {
7759
+ repositoryImports.add(`select${table.typeName}Rows`);
7760
+ dbImports.add(`type ${table.rowType}`);
7761
+ typeImports.add(table.typeName);
7762
+ lines.push(` const ${table.tsName}Rows = (await select${table.typeName}Rows(${handle})).map(to${table.typeName}Dto);`);
7763
+ }
7764
+ for (const hole of operation.aggregateHoles) {
7765
+ const picked = hole.reads.map((field) => `${field}: input.${field}`).join(", ");
7766
+ const shared = hole.from.length === 0 ? "" : `, { ${hole.from.map((table) => `${table.tsName}: ${table.tsName}Rows`).join(", ")} }`;
7767
+ const call = `${hole.id}({ ${picked} }, rows${shared}${effectArguments(plan, hole)})`;
7768
+ lines.push(` const ${hole.id}Value = ${isAsyncHole(hole) ? `await ${call}` : call};`);
7769
+ }
7770
+ const rootFields = [];
7771
+ if (operation.outputModel.kind === "object") {
7772
+ for (const [name, model] of Object.entries(operation.outputModel.fields)) {
7773
+ const hole = operation.aggregateHoles.find((candidate) => candidate.writes === name);
7774
+ if (hole !== void 0) rootFields.push(` ${name}: ${hole.id}Value,`);
7775
+ else if (operation.echoFields.includes(name)) rootFields.push(` ${name}: input.${name},`);
7776
+ else rootFields.push(` ${name}: ${neutralValueOf(model)},`);
7777
+ }
7778
+ }
7779
+ lines.push(` const computed: ${operation.outputType} = {`, ...rootFields, " };", ...projectionLines(operation));
7780
+ return lines;
7781
+ };
7343
7782
  const readBodyLines = (operation) => {
7783
+ if (operation.report) return reportBodyLines(operation);
7344
7784
  const element = operation.element;
7345
7785
  if (element === null) {
7346
7786
  const selectName2 = `select${operation.pascalId}`;
@@ -7400,20 +7840,9 @@ function emitService(plan) {
7400
7840
  ...elementFields,
7401
7841
  " })),",
7402
7842
  ...rootFields,
7403
- " };"
7843
+ " };",
7844
+ ...projectionLines(operation)
7404
7845
  );
7405
- if (operation.projectHoles.length === 0) {
7406
- lines.push(" return computed;");
7407
- } else {
7408
- if (plan.tenant === null) lines.push(" const projection = deps.ctx;");
7409
- else lines.push(" const { db: _db, ...projection } = deps.ctx;", " void _db;");
7410
- let current = "computed";
7411
- for (const hole of operation.projectHoles) {
7412
- lines.push(` const ${hole.id}Output = ${hole.id}(projection, ${current});`);
7413
- current = `${hole.id}Output`;
7414
- }
7415
- lines.push(` return ${current};`);
7416
- }
7417
7846
  return lines;
7418
7847
  };
7419
7848
  const respondLines = (operation) => {
@@ -7429,7 +7858,7 @@ function emitService(plan) {
7429
7858
  "",
7430
7859
  `/** ${operation.method} ${operation.path} \u2014 ${operation.id}. */`,
7431
7860
  `export async function ${operation.serviceName}(deps: ${deps}, input: ${operation.inputType}): Promise<${operation.outputType}> {`,
7432
- ...operation.echoFields.length === 0 && operation.motif?.method !== "currentByKey" ? [" void input;"] : [],
7861
+ ...operation.echoFields.length === 0 && operation.motif?.method !== "currentByKey" && !operation.aggregateHoles.some((hole) => hole.reads.length > 0) ? [" void input;"] : [],
7433
7862
  ...readBodyLines(operation),
7434
7863
  "}"
7435
7864
  );
@@ -7588,6 +8017,16 @@ function emitService(plan) {
7588
8017
  " };",
7589
8018
  "}"
7590
8019
  ];
8020
+ const sharedUsedByHoles = plan.sharedTables.filter((table) => plan.holes.some((hole) => hole.kind === "aggregate" && hole.from.some((candidate) => candidate.sqlName === table.sqlName)));
8021
+ const sharedDtoLines = sharedUsedByHoles.flatMap((table) => [
8022
+ "",
8023
+ `/** One row of the shared table ${table.sqlName} at its schema (D60): what the aggregate holes receive. */`,
8024
+ `function to${table.typeName}Dto(row: ${table.rowType}): ${table.typeName} {`,
8025
+ " return {",
8026
+ ...table.fields.map((field) => ` ${field.name}: ${field.dtoExpression},`),
8027
+ " };",
8028
+ "}"
8029
+ ]);
7591
8030
  const slotHelpers = plan.slots.flatMap((slot) => {
7592
8031
  const dtoFields = slot.fields.map((field) => `${field.name}: ${field.dtoExpression}`).join(", ");
7593
8032
  const keyValues = slot.keyValues === null ? null : `KEY_VALUES_OF_${slot.name.toUpperCase()}`;
@@ -7636,6 +8075,7 @@ ${[...typeImports].sort().map((name) => ` type ${name},`).join("\n")}
7636
8075
  "}",
7637
8076
  "",
7638
8077
  ...dtoLines,
8078
+ ...sharedDtoLines,
7639
8079
  ...slotHelpers,
7640
8080
  "",
7641
8081
  "/** A hole's refusal becomes the operation's 400 OgygieError. */",
@@ -7758,7 +8198,8 @@ function manifestEntryOf(plan) {
7758
8198
  })),
7759
8199
  // The views of the view holes are read through Drizzle like tables: what the repository reads is on the map (D32).
7760
8200
  // D33 precision (M3): the manifest gate proves a declared table by its pgTable — a SQL Server product has none, so its entry declares no table; the generated block of its spec is the map.
7761
- dbTables: plan.target.database === "mssql-raw" ? [] : [...tablesOf(plan).map((table) => table.sqlName), ...plan.holes.flatMap((hole) => hole.kind === "view" ? [hole.name] : [])]
8201
+ // D60: a shared table read through its Drizzle mapping is on the map like a view — the manifest gate asks every pgTable of the folder to be declared.
8202
+ dbTables: plan.target.database === "mssql-raw" ? [] : [...tablesOf(plan).map((table) => table.sqlName), ...plan.holes.flatMap((hole) => hole.kind === "view" ? [hole.name] : []), ...plan.sharedTables.map((table) => table.sqlName)]
7762
8203
  };
7763
8204
  }
7764
8205
  function canonicalJson(value) {
@@ -7990,6 +8431,8 @@ function holeLine(hole, plan) {
7990
8431
  return `- ${tick3(hole.id)} \u2014 ${tick3("validate")}, lit ${hole.reads.map(tick3).join(", ")} de ${tick3(hole.inputTypeName)}, refuse en ${tick3(hole.errorCode)} (400) ; appel\xE9 par ${carriers} ; ${context}`;
7991
8432
  case "derive":
7992
8433
  return `- ${tick3(hole.id)} \u2014 ${tick3("derive")}, lit ${hole.reads.map(tick3).join(", ")} de ${tick3(hole.inputTypeName)}, \xE9crit ${tick3(hole.writes)}${hole.errorCode === null ? "" : `, refuse en ${tick3(hole.errorCode)} (400)`} ; appel\xE9 par ${carriers} ; ${context}`;
8434
+ case "aggregate":
8435
+ return `- ${tick3(hole.id)} \u2014 ${tick3("aggregate")} (D60), lit ${hole.reads.length === 0 ? "rien de l'entr\xE9e" : `${hole.reads.map(tick3).join(", ")} de ${tick3(hole.inputTypeName)}`}, les lignes de ${tick3(hole.resourceTypeName)}${hole.from.length === 0 ? "" : ` et ${hole.from.map((table) => tick3(table.sqlName)).join(", ")}`}, \xE9crit ${tick3(hole.writes)} de la sortie ; appel\xE9 par ${carriers} ; ${context}`;
7993
8436
  case "project":
7994
8437
  return `- ${tick3(hole.id)} \u2014 ${tick3("project")}, lit ${hole.reads.map(tick3).join(", ")} du contexte de l'appelant, \xE9crit ${hole.writes.map(tick3).join(", ")} ; appel\xE9 par ${carriers} ; ${context}`;
7995
8438
  case "view":
@@ -8037,6 +8480,7 @@ function emitSpecContracts(plan) {
8037
8480
  ...tableSection(plan.table, plan, indexes.length === 0 ? [] : [`index ${indexes.join(" ; ")}`]),
8038
8481
  ...detailTables.flatMap((table) => ["", ...tableSection(table, plan, [])]),
8039
8482
  ...views.flatMap((view) => ["", ...viewSection(view)]),
8483
+ ...plan.sharedTables.flatMap((table) => ["", `${tick3(table.sqlName)} \u2014 table PARTAG\xC9E (D60) : table du produit hors du bloc, lue par les trous aggregate qui la nomment, typ\xE9e par ${tick3(table.schemaName)}, jamais cr\xE9\xE9e ni \xE9crite ici ; mapping en lecture seule dans ${tick3("db.schema.gen.ts")}.`]),
8040
8484
  "",
8041
8485
  "## Trous (D23)",
8042
8486
  "",
@@ -11504,10 +11948,14 @@ export {
11504
11948
  tableNameOf,
11505
11949
  tableRefOf,
11506
11950
  parentKeyFieldOf,
11951
+ SharedTableSchema,
11952
+ sharedTableNameOf,
11953
+ typedSharedTablesOf,
11507
11954
  OperationSchema,
11508
11955
  isHttpOperation,
11509
11956
  isWriteOperation,
11510
11957
  isKeyedOperation,
11958
+ AggregateHoleSchema,
11511
11959
  HoleSchema,
11512
11960
  acceptExamplesOf,
11513
11961
  HOLE_KIND_SERVES,
@@ -11588,6 +12036,7 @@ export {
11588
12036
  tablesOf,
11589
12037
  ownedTables,
11590
12038
  viewBodiesOf,
12039
+ deepOverlay,
11591
12040
  normalizedTypeOf,
11592
12041
  HoleShapeSchema,
11593
12042
  HoleProvenanceSchema,
@@ -11603,7 +12052,6 @@ export {
11603
12052
  HOLES_LIBRARY_DIR,
11604
12053
  loadHolesLibrary,
11605
12054
  libraryFileNameOf,
11606
- deepOverlay,
11607
12055
  GENERATED_FEATURE_FILES,
11608
12056
  generateFeature,
11609
12057
  migrationFileNameFor,