@ogygie/generate 0.32.1 → 0.34.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
  }
@@ -3091,10 +3182,6 @@ function orderPlanOf(operation, schemas, resourceFields) {
3091
3182
  const tuples = typeof operation.orderBy[0] === "string" ? [operation.orderBy] : operation.orderBy;
3092
3183
  return { enumOrder: null, tuples: tuples.map(([field, direction]) => ({ field, direction })) };
3093
3184
  }
3094
- function tenantOf(plan) {
3095
- if (plan.tenant === null) throw new Error("generation plan: this emitter needs a tenant column (target.tenancy column) \u2014 a target without tenant is emitted by the mssql emitters");
3096
- return plan.tenant;
3097
- }
3098
3185
  function schemaNameOfModel(model, schemas, except) {
3099
3186
  const wanted = JSON.stringify({ ...model, optional: false, nullable: model.nullable, examples: [] });
3100
3187
  for (const [name, entry] of Object.entries(schemas)) {
@@ -3378,6 +3465,22 @@ function buildFeaturePlan(input) {
3378
3465
  }
3379
3466
  return null;
3380
3467
  };
3468
+ const sharedTables = typedSharedTablesOf(block.contracts).map((declaration) => {
3469
+ const model = modelNamed2(schemas, declaration.schema);
3470
+ const fields = objectFields(model, declaration.schema);
3471
+ const sharedColumns = [...tenantColumns(), ...Object.entries(fields).map(([name, fieldModel2]) => columnOf(name, fieldModel2, void 0))];
3472
+ const typeName = typeNameOfSchema(declaration.schema);
3473
+ return {
3474
+ sqlName: declaration.name,
3475
+ tsName: camelOfSnake(declaration.name),
3476
+ schemaName: declaration.schema,
3477
+ typeName,
3478
+ rowType: `${typeName}Row`,
3479
+ model,
3480
+ fields: Object.entries(fields).map(([name, fieldModel2]) => ({ name, model: fieldModel2, dtoExpression: dtoExpressionOf(name, fieldModel2, sharedColumns.find((column) => column.key === name)?.sqlType, generation.target.database) })),
3481
+ columns: sharedColumns
3482
+ };
3483
+ });
3381
3484
  const holes = block.holes.map((hole) => {
3382
3485
  const carrier = block.operations.find((operation) => isHttpOperation(operation) && (operation.holes ?? []).includes(hole.id));
3383
3486
  invariant(carrier !== void 0 && isHttpOperation(carrier), `hole ${hole.id} has no carrier`);
@@ -3424,6 +3527,29 @@ function buildFeaturePlan(input) {
3424
3527
  if (hole.kind === "project") {
3425
3528
  return { kind: "project", id: hole.id, hole, reads: hole.reads, writes: hole.writes, outputTypeName: typeNameOfSchema(carrier.output), accepts: hole.examples.accept };
3426
3529
  }
3530
+ if (hole.kind === "aggregate") {
3531
+ const outputModel = modelNamed2(schemas, carrier.output);
3532
+ const writesModel2 = outputModel.kind === "object" ? outputModel.fields[hole.writes] : void 0;
3533
+ invariant(writesModel2 !== void 0, `aggregate hole ${hole.id} writes an unknown output field`);
3534
+ const from = (hole.from ?? []).map((name) => {
3535
+ const table = sharedTables.find((candidate) => candidate.sqlName === name);
3536
+ invariant(table !== void 0, `aggregate hole ${hole.id} reads an undeclared shared table ${name}`);
3537
+ return table;
3538
+ });
3539
+ return {
3540
+ kind: "aggregate",
3541
+ id: hole.id,
3542
+ hole,
3543
+ reads: hole.reads,
3544
+ inputTypeName: typeNameOfSchema(carrier.input),
3545
+ resourceTypeName: resourceType,
3546
+ outputTypeName: typeNameOfSchema(carrier.output),
3547
+ writes: hole.writes,
3548
+ writesModel: writesModel2,
3549
+ from,
3550
+ accepts: hole.examples.accept.map((example) => ({ input: example.input ?? {}, rows: example.rows, shared: example.shared ?? {}, value: example.value }))
3551
+ };
3552
+ }
3427
3553
  const element = elementOf(carrier);
3428
3554
  invariant(element !== null && element.model.kind === "object", `view hole ${hole.id}: the read has no element`);
3429
3555
  const writesModel = element.model.fields[hole.writes];
@@ -3473,6 +3599,9 @@ function buildFeaturePlan(input) {
3473
3599
  else if (operation.kind === "import" && model.kind === "number") outputField = name;
3474
3600
  else if (name in inputFields) echoFields.push(name);
3475
3601
  }
3602
+ const aggregateIds = new Set(block.holes.filter((hole) => hole.kind === "aggregate").map((hole) => hole.id));
3603
+ const report2 = operation.kind === "read" && element === null && (operation.holes ?? []).some((id) => aggregateIds.has(id));
3604
+ if (report2) outputField = "";
3476
3605
  invariant(outputField !== void 0, `${operation.output} has no mappable field`);
3477
3606
  const opHoles = (operation.holes ?? []).map((id) => {
3478
3607
  const hole = holeById.get(id);
@@ -3513,6 +3642,8 @@ function buildFeaturePlan(input) {
3513
3642
  deriveHoles: opHoles.filter((hole) => hole.kind === "derive"),
3514
3643
  projectHoles: opHoles.filter((hole) => hole.kind === "project"),
3515
3644
  viewHoles: opHoles.filter((hole) => hole.kind === "view"),
3645
+ aggregateHoles: opHoles.filter((hole) => hole.kind === "aggregate"),
3646
+ report: report2,
3516
3647
  order: orderPlanOf(operation, schemas, resourceFields),
3517
3648
  limit: operation.kind === "read" ? operation.limit ?? null : null,
3518
3649
  range: operation.kind === "read" ? operation.range ?? null : null,
@@ -3585,6 +3716,7 @@ function buildFeaturePlan(input) {
3585
3716
  key: keyField === null || keyColumn === void 0 || keyUniqueName === null ? null : { field: keyField, column: keyColumn, uniqueName: keyUniqueName },
3586
3717
  slots,
3587
3718
  references,
3719
+ sharedTables,
3588
3720
  signed: block.motifs.signedWrite ?? null,
3589
3721
  unique,
3590
3722
  referenced: (() => {
@@ -3724,6 +3856,19 @@ function holeSignatureLines(hole, plan) {
3724
3856
  ];
3725
3857
  }
3726
3858
  if (hole.kind === "view") return [];
3859
+ if (hole.kind === "aggregate") {
3860
+ const picks = hole.reads.length === 0 ? "Record<never, never>" : `Pick<${hole.inputTypeName}, ${hole.reads.map((field) => quote(field)).join(" | ")}>`;
3861
+ const shared = hole.from.length === 0 ? [] : [` shared: { ${hole.from.map((table) => `${table.tsName}: readonly ${table.typeName}[]`).join("; ")} },`];
3862
+ return [
3863
+ `// @hole ${hole.id}`,
3864
+ `${prefix} ${hole.id}(`,
3865
+ ` input: ${picks},`,
3866
+ ` rows: readonly ${hole.resourceTypeName}[],`,
3867
+ ...shared,
3868
+ ...effects,
3869
+ `): ${wrapResult(`${hole.outputTypeName}[${quote(hole.writes)}]`)} {`
3870
+ ];
3871
+ }
3727
3872
  return [
3728
3873
  `// @hole ${hole.id}`,
3729
3874
  `${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 +3923,14 @@ function emitLogic(plan, existingLogic = null) {
3778
3923
  for (const hole of plan.holes) {
3779
3924
  if (hole.kind === "validate" || hole.kind === "derive") inputTypes.add(hole.inputTypeName);
3780
3925
  if (hole.kind === "project") inputTypes.add(hole.outputTypeName);
3926
+ if (hole.kind === "aggregate") {
3927
+ if (hole.reads.length > 0) inputTypes.add(hole.inputTypeName);
3928
+ inputTypes.add(hole.resourceTypeName);
3929
+ inputTypes.add(hole.outputTypeName);
3930
+ for (const table of hole.from) inputTypes.add(table.typeName);
3931
+ }
3781
3932
  }
3782
- const needsClock = plan.holes.some((hole) => (hole.kind === "validate" || hole.kind === "derive" || hole.kind === "transform") && hole.hole.effects.includes("clock"));
3933
+ const needsClock = plan.holes.some((hole) => (hole.kind === "validate" || hole.kind === "derive" || hole.kind === "transform" || hole.kind === "aggregate") && hole.hole.effects.includes("clock"));
3783
3934
  const needsDb = plan.holes.some((hole) => (hole.kind === "validate" || hole.kind === "derive" || hole.kind === "transform") && hole.hole.effects.includes("db"));
3784
3935
  const hasProject = plan.holes.some((hole) => hole.kind === "project");
3785
3936
  const sections = [];
@@ -3808,7 +3959,8 @@ function emitLogic(plan, existingLogic = null) {
3808
3959
  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
3960
  hasProject ? `import type { ${plan.context.typeName} } from ${quote(plan.context.importPath)};` : null,
3810
3961
  inputTypes.size === 0 ? null : `import type { ${[...inputTypes].sort().join(", ")} } from "./schemas.js";`,
3811
- 'import { accept, refuse, todo, type HoleResult } from "./holes.runtime.gen.js";',
3962
+ // A hole answering a HoleResult needs accept and refuse; an aggregate or a project answers a value — todo alone (D60).
3963
+ 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
3964
  ...hasProject ? [
3813
3965
  "",
3814
3966
  "/** What a project hole may read of the caller: the guard's context without its database (D32). */",
@@ -3895,13 +4047,23 @@ function drizzleTableLines(table, builders, tablesByName) {
3895
4047
  function drizzleViewLines(view, builders) {
3896
4048
  for (const column of view.columns) builders.add(builderOf(column));
3897
4049
  return [
3898
- `/** READ-ONLY mapping of the view ${view.name} (hole ${view.id}, D32): the SQL lives in the migration, this table shape is what tenantDb reads; nothing is ever written through it. */`,
4050
+ `/** READ-ONLY mapping of the view ${view.name} (hole ${view.id}, D32): the SQL lives in the migration, this table shape is what the repository's facade reads; nothing is ever written through it. */`,
3899
4051
  `export const ${view.tsName} = pgTable(${quote(view.name)}, {`,
3900
4052
  ...view.columns.map((column) => ` ${column.key}: ${column.drizzle},`),
3901
4053
  "});",
3902
4054
  `export type ${view.rowType} = typeof ${view.tsName}.$inferSelect;`
3903
4055
  ];
3904
4056
  }
4057
+ function drizzleSharedLines(table, builders) {
4058
+ for (const column of table.columns) builders.add(builderOf(column));
4059
+ return [
4060
+ `/** READ-ONLY mapping of the shared table ${table.sqlName} (D60): a table of the product outside this feature, read by its aggregate holes through the repository's facade; never created nor written here. */`,
4061
+ `export const ${table.tsName} = pgTable(${quote(table.sqlName)}, {`,
4062
+ ...table.columns.map((column) => ` ${column.key}: ${column.drizzle},`),
4063
+ "});",
4064
+ `export type ${table.rowType} = typeof ${table.tsName}.$inferSelect;`
4065
+ ];
4066
+ }
3905
4067
  function tablesOf(plan) {
3906
4068
  return [plan.table, ...plan.slots.map((slot) => slot.table), ...plan.references.flatMap((reference) => reference.table === null ? [] : [reference.table])];
3907
4069
  }
@@ -3945,9 +4107,10 @@ function emitDbSchema(plan) {
3945
4107
  const sections = [];
3946
4108
  for (const table of tables) sections.push("", ...drizzleTableLines(table, builders, tablesByName));
3947
4109
  for (const view of views) sections.push("", ...drizzleViewLines(view, builders));
4110
+ for (const table of plan.sharedTables) sections.push("", ...drizzleSharedLines(table, builders));
3948
4111
  const needsSql = tables.some((table) => table.uniques.some((unique) => unique.where !== null));
3949
4112
  const intent = wrap(
3950
- `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}.`
4113
+ `Drizzle tables owned by the ${plan.featureId} feature, derived from ${plan.resource.schemaName} (${plan.specPath}, section G\xE9n\xE9ration): ${plan.tenant === null ? "without tenant column (target.tenancy none, D33: the product is single-tenant, the repository reads whole tables through plainDb)" : `tenant-scoped by ${plan.tenant.column} (TS key \`${plan.tenant.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}.`
3951
4114
  );
3952
4115
  return fileOf(
3953
4116
  tsGeneratedHeader(plan.generatorVersion),
@@ -3975,6 +4138,8 @@ function createTableStatements(table) {
3975
4138
  for (const unique of table.uniques.filter((candidate) => candidate.where !== null)) {
3976
4139
  statements.push(`create unique index ${quote(unique.name)} on ${quote(table.sqlName)} (${unique.columns.map(quote).join(", ")}) where ${unique.where ?? ""};`);
3977
4140
  }
4141
+ const tenantColumn = table.columns.find((column) => column.model === null && column.key !== "id");
4142
+ if (tenantColumn === void 0) return statements;
3978
4143
  statements.push(
3979
4144
  "",
3980
4145
  `alter table ${quote(table.sqlName)} enable row level security;`,
@@ -3993,7 +4158,7 @@ function emitMigration(plan, existingMigration = null) {
3993
4158
  const bodies = viewBodiesOf(existingMigration);
3994
4159
  const adopted = tablesOf(plan).filter((candidate) => candidate.mode === "adopted");
3995
4160
  const intent = wrap(
3996
- `${plan.featureId} feature: the ${plan.table.sqlName} table${tables.length > 1 ? ` and ${tables.slice(1).map((table) => table.sqlName).join(", ")}` : ""}, tenant-scoped by ${tenantOf(plan).column} (derived from ${plan.resource.schemaName}, ${plan.specPath}).${adopted.length === 0 ? "" : ` ${adopted.map((table) => table.sqlName).join(", ")} ${adopted.length > 1 ? "are ADOPTED tables" : "is an ADOPTED table"} (D36): declared in the block, created by the product, altered here within the closed list only.`} Same rails as the socle: foreign key to ${tenantOf(plan).table}, Row-Level Security on every table (AGENTS.md \xA7Multi-tenant 3 \u2014 second rampart, tenantDb remains the first)${plan.appendOnly === null ? "" : ", plus the append-only motif: a partial unique index on the current rows and a DEFERRABLE superseded_by foreign key, so the old row is marked before the new one is inserted"}${views.length === 0 ? "." : `. The views ${views.map((view) => view.name).join(", ")} are HOLES (D32): their SQL stands between the @hole markers below, written by a human, kept at every regeneration; everything else in this file is generated.`}`
4161
+ `${plan.featureId} feature: the ${plan.table.sqlName} table${tables.length > 1 ? ` and ${tables.slice(1).map((table) => table.sqlName).join(", ")}` : ""}, ${plan.tenant === null ? "without tenant column (target.tenancy none, D33)" : `tenant-scoped by ${plan.tenant.column}`} (derived from ${plan.resource.schemaName}, ${plan.specPath}).${adopted.length === 0 ? "" : ` ${adopted.map((table) => table.sqlName).join(", ")} ${adopted.length > 1 ? "are ADOPTED tables" : "is an ADOPTED table"} (D36): declared in the block, created by the product, altered here within the closed list only.`} ${plan.tenant === null ? "No tenant column, no row-level security: the product is single-tenant (D33)" : `Same rails as the socle: foreign key to ${plan.tenant.table}, Row-Level Security on every table (AGENTS.md \xA7Multi-tenant 3 \u2014 second rampart, tenantDb remains the first)`}${plan.appendOnly === null ? "" : ", plus the append-only motif: a partial unique index on the current rows and a DEFERRABLE superseded_by foreign key, so the old row is marked before the new one is inserted"}${views.length === 0 ? "." : `. The views ${views.map((view) => view.name).join(", ")} are HOLES (D32): their SQL stands between the @hole markers below, written by a human, kept at every regeneration; everything else in this file is generated.`}`
3997
4162
  ).map((line, index) => `-- ${index === 0 ? "@intent " : ""}${line}`);
3998
4163
  const statements = [];
3999
4164
  for (const table of tables) statements.push(...statements.length === 0 ? [] : [""], ...createTableStatements(table));
@@ -4011,6 +4176,160 @@ function emitMigration(plan, existingMigration = null) {
4011
4176
  return fileOf(sqlGeneratedHeader(plan.generatorVersion), ...intent, "", ...statements);
4012
4177
  }
4013
4178
 
4179
+ // src/emit/hole-tests.ts
4180
+ function deepOverlay(base, overlay) {
4181
+ if (Array.isArray(overlay)) {
4182
+ const baseItems = Array.isArray(base) ? base : [];
4183
+ return overlay.map((item, index) => deepOverlay(baseItems[index] ?? baseItems[0], item));
4184
+ }
4185
+ if (overlay !== null && typeof overlay === "object") {
4186
+ const result = base !== null && typeof base === "object" && !Array.isArray(base) ? { ...base } : {};
4187
+ for (const [key, value] of Object.entries(overlay)) result[key] = deepOverlay(result[key], value);
4188
+ return result;
4189
+ }
4190
+ return overlay;
4191
+ }
4192
+ function sampleUnder(model, overlay) {
4193
+ if (model.kind === "object" && overlay !== null && typeof overlay === "object" && !Array.isArray(overlay)) {
4194
+ const given = overlay;
4195
+ const value = {};
4196
+ for (const [name, field] of Object.entries(model.fields)) {
4197
+ if (name in given) value[name] = sampleUnder(field, given[name]);
4198
+ else if (!field.optional) {
4199
+ const sample = sampleOf(field, [name]);
4200
+ value[name] = sample.ok ? sample.value : null;
4201
+ }
4202
+ }
4203
+ return value;
4204
+ }
4205
+ if (model.kind === "array" && Array.isArray(overlay)) return overlay.map((item) => sampleUnder(model.element, item));
4206
+ return overlay;
4207
+ }
4208
+ function projectInputOf(plan, carrier, partial) {
4209
+ void plan;
4210
+ return sampleUnder(carrier.outputModel, partial);
4211
+ }
4212
+ var HOLE_TESTS_CLOCK_INSTANT = "2026-01-01T00:00:00.000Z";
4213
+ function holeDeclaresClock(hole) {
4214
+ return (hole.hole.effects ?? []).includes("clock");
4215
+ }
4216
+ function constantNameOf(identifier) {
4217
+ return identifier.replace(/([a-z0-9])([A-Z])/gu, "$1_$2").toUpperCase();
4218
+ }
4219
+ function emitHoleTests(plan) {
4220
+ const blocks = [];
4221
+ const testedHoles = plan.holes.filter((hole) => hole.kind !== "view");
4222
+ const needsOutputs = /* @__PURE__ */ new Set();
4223
+ const needsExamples = /* @__PURE__ */ new Set();
4224
+ const needsClock = testedHoles.some((hole) => hole.kind !== "project" && holeDeclaresClock(hole));
4225
+ for (const hole of testedHoles) {
4226
+ const withEffects = (argument) => hole.kind !== "project" && holeDeclaresClock(hole) ? `${argument}, CLOCK` : argument;
4227
+ const call = (argument) => isAsyncHole(hole) ? `await ${hole.id}(${withEffects(argument)})` : `${hole.id}(${withEffects(argument)})`;
4228
+ blocks.push("", ` describe(${quote(hole.id)}, () => {`);
4229
+ if (hole.kind === "validate") {
4230
+ hole.accepts.forEach((accept, index) => {
4231
+ blocks.push(
4232
+ ` it(${quote(`accepte l'exemple ${index + 1} : ${JSON.stringify(accept)}`)}, async () => {`,
4233
+ ` expect((${call(JSON.stringify(accept))}).ok).toBe(true);`,
4234
+ " });"
4235
+ );
4236
+ });
4237
+ blocks.push(
4238
+ ` it(${quote(`refuse ${JSON.stringify(hole.reject)} avec ${hole.errorCode}${hole.hole.suggestedFix === true ? " et un suggestedFix" : ""}`)}, async () => {`,
4239
+ ` const result = ${call(JSON.stringify(hole.reject))};`,
4240
+ " expect(result.ok).toBe(false);",
4241
+ " if (result.ok) return;",
4242
+ ` expect(result.code).toBe(${quote(hole.errorCode)});`,
4243
+ " expect(result.suggestedFix.length).toBeGreaterThan(0);",
4244
+ " });"
4245
+ );
4246
+ } else if (hole.kind === "derive") {
4247
+ hole.accepts.forEach((accept, index) => {
4248
+ blocks.push(
4249
+ ` it(${quote(`d\xE9rive ${JSON.stringify(accept.output)} de l'exemple ${index + 1} : ${JSON.stringify(accept.input)}`)}, async () => {`,
4250
+ ` const result = ${call(JSON.stringify(accept.input))};`,
4251
+ " expect(result.ok).toBe(true);",
4252
+ " if (!result.ok) return;",
4253
+ ` expect(result.value).toEqual(${JSON.stringify(accept.output)});`,
4254
+ " });"
4255
+ );
4256
+ });
4257
+ if (hole.reject !== null && hole.errorCode !== null) {
4258
+ blocks.push(
4259
+ ` it(${quote(`refuse ${JSON.stringify(hole.reject)} avec ${hole.errorCode}`)}, async () => {`,
4260
+ ` const result = ${call(JSON.stringify(hole.reject))};`,
4261
+ " expect(result.ok).toBe(false);",
4262
+ " if (result.ok) return;",
4263
+ ` expect(result.code).toBe(${quote(hole.errorCode)});`,
4264
+ " expect(result.suggestedFix.length).toBeGreaterThan(0);",
4265
+ " });"
4266
+ );
4267
+ }
4268
+ } else if (hole.kind === "project") {
4269
+ const carrier = plan.operations.find((operation) => operation.projectHoles.some((candidate) => candidate.id === hole.id));
4270
+ if (carrier === void 0) throw new Error(`generation plan: project hole ${hole.id} has no carrier`);
4271
+ needsOutputs.add(carrier.outputSchema);
4272
+ hole.accepts.forEach((accept, index) => {
4273
+ blocks.push(
4274
+ ` it(${quote(`forme la sortie pour le contexte ${JSON.stringify(accept.context)} (exemple ${index + 1})`)}, () => {`,
4275
+ ` const input = ${carrier.outputSchema}.parse(${JSON.stringify(projectInputOf(plan, carrier, accept.input))});`,
4276
+ ` expect(${hole.id}(${JSON.stringify(accept.context)}, input)).toMatchObject(${JSON.stringify(accept.output)});`,
4277
+ " });"
4278
+ );
4279
+ });
4280
+ } else if (hole.kind === "aggregate") {
4281
+ const constant = `EXAMPLES_OF_${constantNameOf(hole.id)}`;
4282
+ needsExamples.add(constant);
4283
+ hole.accepts.forEach((accept, index) => {
4284
+ blocks.push(
4285
+ ` it(${quote(`rend ${JSON.stringify(accept.value)} pour l'exemple ${index + 1} (${accept.rows.length} ligne(s))`)}, async () => {`,
4286
+ ` const example = ${constant}[${index}];`,
4287
+ ' if (example === undefined) throw new Error("example missing");',
4288
+ ` expect(${call(`example.input, example.rows${hole.from.length === 0 ? "" : ", example.shared"}`)}).toEqual(example.value);`,
4289
+ " });"
4290
+ );
4291
+ });
4292
+ } else {
4293
+ blocks.push(
4294
+ ` it("accepte le fichier exemple et rend ses lignes", async () => {`,
4295
+ ` const result = ${call(`{ text: ${JSON.stringify(hole.hole.examples.accept.input)} }`)};`,
4296
+ " expect(result.ok).toBe(true);",
4297
+ " if (!result.ok) return;",
4298
+ ` expect(result.value).toEqual(${JSON.stringify(hole.hole.examples.accept.output)});`,
4299
+ " });",
4300
+ ` it(${quote(`refuse le fichier illisible avec ${hole.errorCode} et sa ligne`)}, async () => {`,
4301
+ ` const result = ${call(`{ text: ${JSON.stringify(hole.hole.examples.reject.input)} }`)};`,
4302
+ " expect(result.ok).toBe(false);",
4303
+ " if (result.ok) return;",
4304
+ ` expect(result.code).toBe(${quote(hole.errorCode)});`,
4305
+ ` expect(result.line).toBe(${JSON.stringify(hole.hole.examples.reject.line ?? 1)});`,
4306
+ " expect(result.suggestedFix.length).toBeGreaterThan(0);",
4307
+ " });"
4308
+ );
4309
+ }
4310
+ blocks.push(" });");
4311
+ }
4312
+ return fileOf(
4313
+ tsGeneratedHeader(plan.generatorVersion),
4314
+ intentBlock(
4315
+ wrap(
4316
+ `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." : ""}`
4317
+ )
4318
+ ),
4319
+ 'import { describe, expect, it } from "vitest";',
4320
+ needsClock ? 'import { fixedClock } from "@ogygie/core/clock";' : null,
4321
+ `import { ${testedHoles.map((hole) => hole.id).sort().join(", ")} } from "./logic.js";`,
4322
+ needsOutputs.size === 0 ? null : `import { ${[...needsOutputs].sort().join(", ")} } from "./schemas.js";`,
4323
+ needsExamples.size === 0 ? null : `import { ${[...needsExamples].sort().join(", ")} } from "./factories.gen.js";`,
4324
+ needsClock ? "" : null,
4325
+ needsClock ? `const CLOCK = fixedClock(${quote(HOLE_TESTS_CLOCK_INSTANT)});` : null,
4326
+ "",
4327
+ `describe(${quote(`${plan.featureId} \u2014 trous`)}, () => {`,
4328
+ ...blocks,
4329
+ "});"
4330
+ );
4331
+ }
4332
+
4014
4333
  // src/hole-shape.ts
4015
4334
  import { createHash } from "node:crypto";
4016
4335
  import { readdirSync, readFileSync as readFileSync2 } from "node:fs";
@@ -4073,7 +4392,7 @@ function normalizedTypeOf(model) {
4073
4392
  return `${base}${model.nullable ? "?" : ""}`;
4074
4393
  }
4075
4394
  var HoleShapeSchema = z6.strictObject({
4076
- kind: z6.enum(["validate", "transform", "derive", "project", "view", "custom-view"]),
4395
+ kind: z6.enum(["validate", "transform", "derive", "project", "view", "aggregate", "custom-view"]),
4077
4396
  /** Normalized types of the fields read, by position. */
4078
4397
  reads: z6.array(z6.string()),
4079
4398
  /** Transform only: normalized types of the columns, by position. */
@@ -4107,7 +4426,7 @@ function hash16(text) {
4107
4426
  }
4108
4427
  function rowModelOf2(hole, plan) {
4109
4428
  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)
4429
+ (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
4430
  );
4112
4431
  if (carrier === void 0) return null;
4113
4432
  if (carrier.kind === "import") {
@@ -4143,6 +4462,9 @@ function shapeOfHole(hole, plan) {
4143
4462
  if (hole.kind === "view") {
4144
4463
  return { kind: "view", reads: hole.from.map(() => "table"), columns: hole.columns.map((column) => column.sqlType), codeRole: "none", returns: `view:${hole.shape}` };
4145
4464
  }
4465
+ if (hole.kind === "aggregate") {
4466
+ return { kind: "aggregate", reads: readTypes(hole.reads), columns: ["rows", ...hole.from.map(() => "table")], codeRole: "none", returns: normalizedTypeOf(hole.writesModel) };
4467
+ }
4146
4468
  return {
4147
4469
  kind: "transform",
4148
4470
  reads: ["string:text"],
@@ -4166,7 +4488,7 @@ function signatureHashOf(signature) {
4166
4488
  }
4167
4489
  function bodyTemplateOf(body, hole) {
4168
4490
  const replacements = [];
4169
- if (hole.kind === "validate" || hole.kind === "derive") {
4491
+ if (hole.kind === "validate" || hole.kind === "derive" || hole.kind === "aggregate") {
4170
4492
  hole.reads.forEach((name, index) => replacements.push([name, `{{read:${index}}}`]));
4171
4493
  } else if (hole.kind === "transform") {
4172
4494
  hole.columns.forEach((name, index) => replacements.push([name, `{{column:${index}}}`]));
@@ -4185,7 +4507,7 @@ function bodyTemplateOf(body, hole) {
4185
4507
  }
4186
4508
  function instantiateBody(template, hole) {
4187
4509
  let body = template;
4188
- if (hole.kind === "validate" || hole.kind === "derive") {
4510
+ if (hole.kind === "validate" || hole.kind === "derive" || hole.kind === "aggregate") {
4189
4511
  hole.reads.forEach((name, index) => {
4190
4512
  body = body.replaceAll(`{{read:${index}}}`, name);
4191
4513
  });
@@ -4235,143 +4557,6 @@ function libraryFileNameOf(entry) {
4235
4557
  return `${entry.signatureHash}.json`;
4236
4558
  }
4237
4559
 
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
4560
  // src/emit/mssql/sql-types.ts
4376
4561
  function sqlColumnName(plan, column) {
4377
4562
  return plan.target.naming === "pascal" ? column.columnName.charAt(0).toUpperCase() + column.columnName.slice(1) : column.columnName;
@@ -4647,6 +4832,33 @@ function mssqlStatements(plan) {
4647
4832
  parameters: []
4648
4833
  });
4649
4834
  }
4835
+ for (const table of plan.sharedTables) {
4836
+ statements.push({
4837
+ key: `select${table.typeName}Rows`,
4838
+ kind: "selectShared",
4839
+ text: `SELECT ${selectList(plan, "t", table.columns)} FROM ${qualifiedTable(plan, table.sqlName)} AS t`,
4840
+ table: table.sqlName,
4841
+ rowType: table.rowType,
4842
+ parameters: []
4843
+ });
4844
+ }
4845
+ const arrangingRows = plan.operations.every((operation) => operation.kind !== "create") && plan.operations.some((operation) => operation.aggregateHoles.length > 0);
4846
+ if (arrangingRows) {
4847
+ const arrangeOf = (sqlName, columns, rowType, typeName) => {
4848
+ const written = [...columns.filter((column) => column.key === "id"), ...columns.filter((column) => column.key !== "id")];
4849
+ return {
4850
+ key: `arrange${typeName}Row`,
4851
+ kind: "insert",
4852
+ 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(", ")})`,
4853
+ table: sqlName,
4854
+ rowType,
4855
+ parameters: written.map((column) => column.key),
4856
+ written: written.map((column) => column.key)
4857
+ };
4858
+ };
4859
+ statements.push(arrangeOf(resource.sqlName, resource.columns, resource.rowType, plan.resource.typeName));
4860
+ for (const table of plan.sharedTables) statements.push(arrangeOf(table.sqlName, table.columns, table.rowType, table.typeName));
4861
+ }
4650
4862
  if (keyField !== null && keyColumn !== null) {
4651
4863
  const keyName = sqlColumnName(plan, keyColumn);
4652
4864
  statements.push({
@@ -4812,6 +5024,10 @@ function rowInterfaceLines(name, columns) {
4812
5024
  function parametersLiteral(entries) {
4813
5025
  return `{ ${entries.map(([name, expression]) => `${name}: ${expression}`).join(", ")} }`;
4814
5026
  }
5027
+ function parameterValue(column, expression) {
5028
+ if (column.sqlType === "timestamptz") return expression;
5029
+ return expression;
5030
+ }
4815
5031
  function notFoundLines(plan, codeExpression, indent) {
4816
5032
  const key = plan.key?.field ?? "id";
4817
5033
  return [
@@ -4851,6 +5067,34 @@ function viewReadLines(plan, view, statement) {
4851
5067
  "}"
4852
5068
  ];
4853
5069
  }
5070
+ function sharedReadLines(plan, table, statement) {
5071
+ return [
5072
+ `/** The rows of the shared table ${table.sqlName} (D60) \u2014 read-only, the product owns it. */`,
5073
+ `export async function ${statement.key}(db: ${plan.databasePort?.typeName ?? "Database"}): Promise<${table.rowType}[]> {`,
5074
+ ` return db.query<${table.rowType}>(SQL.${statement.key});`,
5075
+ "}"
5076
+ ];
5077
+ }
5078
+ function arrangeLines(plan, statement, typeName, columns, withId) {
5079
+ const written = statement.written ?? [];
5080
+ const valueOf = (field) => {
5081
+ if (field === "id") return "crypto.randomUUID()";
5082
+ const column = columns.find((candidate) => candidate.key === field);
5083
+ return column === void 0 ? `row.${field}` : parameterValue(column, `row.${field}`);
5084
+ };
5085
+ return [
5086
+ `/** TESTS ONLY (D60): rows of ${statement.table} arranged at their schema \u2014 the feature never writes them, they come from elsewhere. */`,
5087
+ `export async function arrange${typeName}Rows(db: ${plan.databasePort?.typeName ?? "Database"}, rows: readonly ${withId ? `Omit<${typeName}, "id">` : typeName}[]): Promise<${statement.rowType}[]> {`,
5088
+ ` const inserted: ${statement.rowType}[] = [];`,
5089
+ " for (const row of rows) {",
5090
+ ` const [written] = await db.query<${statement.rowType}>(SQL.${statement.key}, ${parametersLiteral(written.map((field) => [field, valueOf(field)]))});`,
5091
+ " if (written === undefined) throwOgygieError(insertReturnedNothing());",
5092
+ " inserted.push(written);",
5093
+ " }",
5094
+ " return inserted;",
5095
+ "}"
5096
+ ];
5097
+ }
4854
5098
  function referenceLines(plan, table, column, countStatement, insertStatement) {
4855
5099
  const database = plan.databasePort?.typeName ?? "Database";
4856
5100
  const typeName = table.rowType.replace(/Row$/u, "");
@@ -5061,6 +5305,7 @@ function emitMssqlRepository(plan) {
5061
5305
  for (const slot of plan.slots) rows.push("", ...rowInterfaceLines(slot.table.rowType, slot.table.columns));
5062
5306
  for (const reference of plan.references) if (reference.table !== null) rows.push("", ...rowInterfaceLines(reference.table.rowType, reference.table.columns));
5063
5307
  for (const view of views) rows.push("", ...rowInterfaceLines(view.rowType, view.columns));
5308
+ for (const table of plan.sharedTables) rows.push("", ...rowInterfaceLines(table.rowType, table.columns));
5064
5309
  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
5310
  const sections = [];
5066
5311
  for (const operation of plan.operations.filter((candidate) => candidate.kind === "read")) sections.push("", ...readFunctionLines(plan, operation, statementNamed(statements, `select${operation.pascalId}`)));
@@ -5071,8 +5316,14 @@ function emitMssqlRepository(plan) {
5071
5316
  sections.push("", ...referenceLines(plan, reference.table, reference.column, statementNamed(statements, `count${typeName}`), statementNamed(statements, `insert${typeName}`)));
5072
5317
  }
5073
5318
  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`)));
5319
+ for (const table of plan.sharedTables) sections.push("", ...sharedReadLines(plan, table, statementNamed(statements, `select${table.typeName}Rows`)));
5075
5320
  const create = plan.operations.find((operation) => operation.kind === "create");
5321
+ const arranging = create === void 0 && plan.operations.some((operation) => operation.aggregateHoles.length > 0);
5322
+ if (arranging) {
5323
+ sections.push("", ...arrangeLines(plan, statementNamed(statements, `arrange${plan.resource.typeName}Row`), plan.resource.typeName, plan.table.columns, true));
5324
+ for (const table of plan.sharedTables) sections.push("", ...arrangeLines(plan, statementNamed(statements, `arrange${table.typeName}Row`), table.typeName, table.columns, false));
5325
+ }
5326
+ if (plan.key !== null) sections.push("", ...loadByKeyLines(plan, statementNamed(statements, `load${plan.resource.typeName}ByKey`)));
5076
5327
  const update = plan.operations.find((operation) => operation.kind === "update");
5077
5328
  const setOrClear = plan.operations.find((operation) => operation.kind === "setOrClear");
5078
5329
  const remove = plan.operations.find((operation) => operation.kind === "delete");
@@ -5081,7 +5332,7 @@ function emitMssqlRepository(plan) {
5081
5332
  if (setOrClear !== void 0) sections.push("", ...setOrClearFunctionLines(plan, setOrClear, statements));
5082
5333
  if (remove !== void 0) sections.push("", ...deleteFunctionLines(plan, remove, statements));
5083
5334
  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();
5335
+ 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
5336
  return fileOf(
5086
5337
  tsGeneratedHeader(plan.generatorVersion),
5087
5338
  intentBlock(
@@ -5162,6 +5413,8 @@ function emitMssqlStub(plan) {
5162
5413
  const tables = tablesOf(plan);
5163
5414
  const views = plan.holes.filter((hole) => hole.kind === "view");
5164
5415
  const tableOf = (sqlName) => {
5416
+ const shared = plan.sharedTables.find((candidate) => candidate.sqlName === sqlName);
5417
+ 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
5418
  const table = tables.find((candidate) => candidate.sqlName === sqlName);
5166
5419
  if (table === void 0) throw new Error(`generation plan: stub has no table ${sqlName}`);
5167
5420
  return table;
@@ -5181,7 +5434,8 @@ function emitMssqlStub(plan) {
5181
5434
  cases.push(` const rows = tables.${statement.table};`, ...compareLines(statement));
5182
5435
  break;
5183
5436
  }
5184
- case "selectView": {
5437
+ case "selectView":
5438
+ case "selectShared": {
5185
5439
  rowTypes.add(statement.rowType);
5186
5440
  cases.push(` return [...tables.${statement.table}];`);
5187
5441
  break;
@@ -5316,9 +5570,9 @@ function emitMssqlStub(plan) {
5316
5570
  }
5317
5571
  cases.push(" }");
5318
5572
  }
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();
5573
+ const tableFields = [...tables.map((table) => ` ${table.sqlName}: ${table.rowType}[];`), ...views.map((view) => ` ${view.name}: ${view.rowType}[];`), ...plan.sharedTables.map((table) => ` ${table.sqlName}: ${table.rowType}[];`)];
5574
+ const emptyFields = [...tables.map((table) => `${table.sqlName}: []`), ...views.map((view) => `${view.name}: []`), ...plan.sharedTables.map((table) => `${table.sqlName}: []`)];
5575
+ const importedRowTypes = [.../* @__PURE__ */ new Set([...tables.map((table) => table.rowType), ...views.map((view) => view.rowType), ...plan.sharedTables.map((table) => table.rowType)])].sort();
5322
5576
  return fileOf(
5323
5577
  tsGeneratedHeader(plan.generatorVersion),
5324
5578
  intentBlock(
@@ -5428,8 +5682,9 @@ function sampleOrThrow(model, variant, label) {
5428
5682
  }
5429
5683
  return sample.value;
5430
5684
  }
5431
- function acceptOverlay(holes, derives, variant) {
5685
+ function acceptOverlay(holes, derives, variant, aggregates = []) {
5432
5686
  const overlay = {};
5687
+ for (const hole of aggregates) Object.assign(overlay, hole.accepts[Math.min(variant, hole.accepts.length - 1)]?.input ?? {});
5433
5688
  for (const hole of holes) {
5434
5689
  const example = hole.accepts[Math.min(variant, hole.accepts.length - 1)] ?? {};
5435
5690
  Object.assign(overlay, example);
@@ -5443,7 +5698,8 @@ function acceptOverlay(holes, derives, variant) {
5443
5698
  function validInputsOf(plan, operation) {
5444
5699
  const holes = operation.kind === "import" ? [] : operation.validateHoles;
5445
5700
  const derives = operation.deriveHoles;
5446
- const variants = Math.max(1, ...holes.map((hole) => hole.accepts.length), ...derives.map((hole) => hole.accepts.length));
5701
+ const aggregates = operation.aggregateHoles;
5702
+ const variants = Math.max(1, ...holes.map((hole) => hole.accepts.length), ...derives.map((hole) => hole.accepts.length), ...aggregates.map((hole) => hole.accepts.length));
5447
5703
  if (operation.kind === "update" && operation.inputModel.kind === "object") {
5448
5704
  return Array.from({ length: variants }, (_, variant) => {
5449
5705
  const body = {};
@@ -5455,7 +5711,7 @@ function validInputsOf(plan, operation) {
5455
5711
  });
5456
5712
  }
5457
5713
  const base = sampleOrThrow(operation.inputModel, 0, operation.inputSchema);
5458
- return Array.from({ length: variants }, (_, variant) => ({ ...base, ...acceptOverlay(holes, derives, variant) }));
5714
+ return Array.from({ length: variants }, (_, variant) => ({ ...base, ...acceptOverlay(holes, derives, variant, aggregates) }));
5459
5715
  }
5460
5716
  function derivedOf(operation, variant = 0) {
5461
5717
  const derived = {};
@@ -5490,7 +5746,7 @@ function sampleOfSqlType(sqlType) {
5490
5746
  if (sqlType === "uuid") return "00000000-0000-4000-8000-000000000001";
5491
5747
  return "arranged";
5492
5748
  }
5493
- function constantNameOf(typeName) {
5749
+ function constantNameOf2(typeName) {
5494
5750
  return typeName.replace(/([a-z0-9])([A-Z])/gu, "$1_$2").toUpperCase();
5495
5751
  }
5496
5752
  function sampleValueOf(model, variant = 0) {
@@ -5511,13 +5767,13 @@ function emitFactories(plan) {
5511
5767
  sections.push(
5512
5768
  "",
5513
5769
  `/** 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)};`
5770
+ `export const INVALID_${constantNameOf2(operation.inputType)}: Record<string, unknown> = ${JSON.stringify(invalidImport.value)};`
5515
5771
  );
5516
5772
  }
5517
5773
  continue;
5518
5774
  }
5519
5775
  typeImports.add(operation.inputType);
5520
- const constant = `VALID_${constantNameOf(operation.inputType)}S`;
5776
+ const constant = `VALID_${constantNameOf2(operation.inputType)}S`;
5521
5777
  const factory = `valid${operation.inputType}`;
5522
5778
  sections.push(
5523
5779
  "",
@@ -5537,13 +5793,13 @@ function emitFactories(plan) {
5537
5793
  sections.push(
5538
5794
  "",
5539
5795
  `/** 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)};`
5796
+ `export const INVALID_${constantNameOf2(operation.inputType)}: Record<string, unknown> = ${JSON.stringify(invalid.value)};`
5541
5797
  );
5542
5798
  } else if (operation.kind === "update") {
5543
5799
  sections.push(
5544
5800
  "",
5545
5801
  `/** 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> = {};`
5802
+ `export const INVALID_${constantNameOf2(operation.inputType)}: Record<string, unknown> = {};`
5547
5803
  );
5548
5804
  }
5549
5805
  }
@@ -5575,6 +5831,31 @@ function emitFactories(plan) {
5575
5831
  } else if (create !== null) {
5576
5832
  sections.push("", "/** No derive hole: an arranged row carries nothing beyond its input. */", "export const DERIVED_OF_VALID_CREATES: readonly Record<string, unknown>[] = [{}];");
5577
5833
  }
5834
+ for (const hole of plan.holes) {
5835
+ if (hole.kind !== "aggregate") continue;
5836
+ const carrier = plan.operations.find((operation) => operation.aggregateHoles.some((candidate) => candidate.id === hole.id));
5837
+ if (carrier === void 0) continue;
5838
+ typeImports.add(hole.resourceTypeName);
5839
+ typeImports.add(hole.outputTypeName);
5840
+ if (hole.reads.length > 0) typeImports.add(hole.inputTypeName);
5841
+ for (const table of hole.from) typeImports.add(table.typeName);
5842
+ const inputType = hole.reads.length === 0 ? "Record<never, never>" : `Pick<${hole.inputTypeName}, ${hole.reads.map(quote).join(" | ")}>`;
5843
+ const sharedType = hole.from.length === 0 ? "Record<never, never>" : `{ ${hole.from.map((table) => `${table.tsName}: readonly ${table.typeName}[]`).join("; ")} }`;
5844
+ const examples = hole.accepts.map((accept) => {
5845
+ const fullInput = sampleUnder(carrier.inputModel, accept.input);
5846
+ const input = Object.fromEntries(hole.reads.map((field) => [field, fullInput[field]]));
5847
+ const rows = accept.rows.map((row) => sampleUnder(plan.resource.model, row));
5848
+ const shared = Object.fromEntries(hole.from.map((table) => [table.tsName, (accept.shared[table.sqlName] ?? []).map((row) => sampleUnder(table.model, row))]));
5849
+ return { input, rows, shared, value: accept.value };
5850
+ });
5851
+ sections.push(
5852
+ "",
5853
+ `/** 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. */`,
5854
+ `export const EXAMPLES_OF_${constantNameOf(hole.id)}: readonly { input: ${inputType}; rows: readonly ${hole.resourceTypeName}[]; shared: ${sharedType}; value: ${hole.outputTypeName}[${quote(hole.writes)}] }[] = [`,
5855
+ ...examples.map((example) => ` ${JSON.stringify(example)},`),
5856
+ "];"
5857
+ );
5858
+ }
5578
5859
  for (const reference of plan.references) {
5579
5860
  if (reference.table === null) continue;
5580
5861
  const model = reference.schemaName === null ? void 0 : plan.schemaModels[reference.schemaName];
@@ -5640,14 +5921,14 @@ function emitFactories(plan) {
5640
5921
  sections.push(
5641
5922
  "",
5642
5923
  `/** 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;`
5924
+ `export const REJECT_EXAMPLE_OF_${constantNameOf2(hole.id)} = ${JSON.stringify(hole.reject)} as const;`
5644
5925
  );
5645
5926
  }
5646
5927
  if (hole.kind === "derive" && hole.reject !== null) {
5647
5928
  sections.push(
5648
5929
  "",
5649
5930
  `/** 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;`
5931
+ `export const REJECT_EXAMPLE_OF_${constantNameOf2(hole.id)} = ${JSON.stringify(hole.reject)} as const;`
5651
5932
  );
5652
5933
  }
5653
5934
  }
@@ -5670,7 +5951,7 @@ function markdownGeneratedHeader(version) {
5670
5951
  }
5671
5952
  function carriersOf(hole, plan) {
5672
5953
  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)
5954
+ (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
5955
  ).map((operation) => `\`${operation.id}\` (${operation.method} ${operation.path})`);
5675
5956
  }
5676
5957
  function tick(name) {
@@ -5712,6 +5993,17 @@ function contractLines(hole, plan, shape) {
5712
5993
  "## Champs",
5713
5994
  ...hole.writes.map((path) => `- ${tick(path)} : \xE9crit par le trou`)
5714
5995
  ];
5996
+ case "aggregate":
5997
+ return [
5998
+ `- 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(", ")}`}`,
5999
+ `- 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)`,
6000
+ `- effets : ${effects(hole.hole.effects)}`,
6001
+ "- 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",
6002
+ "",
6003
+ "## Champs",
6004
+ ...hole.reads.map((name, index) => `- ${tick(name)} : ${shape.reads[index] ?? "unknown"}`),
6005
+ `- ${tick(hole.writes)} (\xE9crit) : ${shape.returns}`
6006
+ ];
5715
6007
  case "view":
5716
6008
  return [
5717
6009
  `- 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 +6040,10 @@ function exampleLines(hole, plan) {
5748
6040
  ];
5749
6041
  case "project":
5750
6042
  return hole.accepts.map((accept) => `- contexte ${tick(JSON.stringify(accept.context))}, entr\xE9e ${tick(JSON.stringify(accept.input))} \u2192 ${tick(JSON.stringify(accept.output))}`);
6043
+ case "aggregate":
6044
+ return hole.accepts.map(
6045
+ (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)`
6046
+ );
5751
6047
  case "view":
5752
6048
  return [
5753
6049
  `- lignes de case d'UNE ligne de ressource : ${tick(JSON.stringify(hole.hole.examples.rows))}`,
@@ -5786,7 +6082,7 @@ function emitHoleDoc(plan, hole, library) {
5786
6082
  "## Contrat",
5787
6083
  `- appel\xE9 par : ${carriersOf(hole, plan).join(", ")}`,
5788
6084
  ...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}\``,
6085
+ `- 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
6086
  "",
5791
6087
  hole.kind === "view" ? "## Colonnes (g\xE9n\xE9r\xE9es, le SQL doit les produire)" : "## Signature (g\xE9n\xE9r\xE9e)",
5792
6088
  hole.kind === "view" ? "```sql" : "```ts",
@@ -5810,8 +6106,8 @@ function emitHoleDoc(plan, hole, library) {
5810
6106
  "- 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
6107
  "- ne touche rien hors des marqueurs : le reste de la migration est r\xE9g\xE9n\xE9r\xE9"
5812
6108
  ] : [
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",
6109
+ 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)",
6110
+ 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
6111
  "- aucun `any`, aucune d\xE9pendance nouvelle, identifiants et commentaires en anglais",
5816
6112
  "- ne touche rien hors du corps : signature, marqueurs et imports sont r\xE9g\xE9n\xE9r\xE9s"
5817
6113
  ]
@@ -5821,7 +6117,7 @@ function emitHoleDoc(plan, hole, library) {
5821
6117
 
5822
6118
  // src/emit/tests.ts
5823
6119
  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) {
6120
+ function constantNameOf3(typeName) {
5825
6121
  return typeName.replace(/([a-z0-9])([A-Z])/gu, "$1_$2").toUpperCase();
5826
6122
  }
5827
6123
  function invariant2(condition, message) {
@@ -5868,7 +6164,7 @@ function harnessLines(plan, options) {
5868
6164
  const create = createOperation(plan);
5869
6165
  const imports = [
5870
6166
  `import { ${plan.tenant === null ? "afterEach, beforeEach" : "afterAll, beforeAll"}, describe, expect, it } from "vitest";`,
5871
- ...options.arranging ? ['import { fixedClock } from "@ogygie/core/clock";'] : [],
6167
+ ...options.arranging && create !== null ? ['import { fixedClock } from "@ogygie/core/clock";'] : [],
5872
6168
  `import type { ${plan.context.typeName} } from ${quote(plan.context.importPath)};`,
5873
6169
  `import { ${plan.testBench.exportName} } from ${quote(plan.testBench.importPath)};`,
5874
6170
  `import { ${[...fixtureNames].sort().join(", ")} } from ${quote(fixturesImportPath ?? "")};`
@@ -5936,6 +6232,33 @@ function harnessLines(plan, options) {
5936
6232
  " return String(a) < String(b) ? -1 : 1;",
5937
6233
  "}"
5938
6234
  ];
6235
+ const arrangingReport = options.arranging && options.report === true && create === null && plan.operations.some((operation) => operation.aggregateHoles.length > 0);
6236
+ if (arrangingReport) {
6237
+ const handleType = plan.tenant === null ? `db: ${plan.databasePort?.typeName ?? "Database"}` : `context: ${plan.context.typeName}`;
6238
+ const handleName = plan.tenant === null ? "db" : "context";
6239
+ imports.push(
6240
+ `import type { ${plan.table.rowType} } from ${plan.target.database === "mssql-raw" ? '"./repository.gen.js"' : '"./db.schema.gen.js"'};`,
6241
+ `import { ${["arrange" + plan.resource.typeName + "Rows", ...plan.sharedTables.map((table) => `arrange${table.typeName}Rows`)].sort().join(", ")} } from "./repository.gen.js";`,
6242
+ `import type { ${[plan.resource.typeName, ...plan.sharedTables.map((table) => table.typeName)].sort().join(", ")} } from "./schemas.js";`
6243
+ );
6244
+ if (plan.tenant === null && plan.databasePort !== null) imports.push(`import type { ${plan.databasePort.typeName} } from ${quote(plan.databasePort.importPath)};`);
6245
+ helpers.push(
6246
+ "",
6247
+ "/** Rows arranged at their schema (D60): the block has no create, the tests put their rows on the bench through the repository's arrangement. */",
6248
+ `async function arrangeRows(${handleType}, rows: readonly Omit<${plan.resource.typeName}, "id">[]): Promise<${plan.table.rowType}[]> {`,
6249
+ ` return arrange${plan.resource.typeName}Rows(${handleName}, rows);`,
6250
+ "}"
6251
+ );
6252
+ if (plan.sharedTables.length > 0) {
6253
+ helpers.push(
6254
+ "",
6255
+ "/** The shared rows of an aggregate example (D60), arranged in their tables before the request. */",
6256
+ `async function arrangeShared(${handleType}, shared: { ${plan.sharedTables.map((table) => `${table.tsName}?: readonly ${table.typeName}[]`).join("; ")} }): Promise<void> {`,
6257
+ ...plan.sharedTables.map((table) => ` if (shared.${table.tsName} !== undefined) await arrange${table.typeName}Rows(${handleName}, shared.${table.tsName});`),
6258
+ "}"
6259
+ );
6260
+ }
6261
+ }
5939
6262
  if (options.arranging && create !== null) {
5940
6263
  imports.push(
5941
6264
  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 +6357,7 @@ function emitContractTests(plan) {
6034
6357
  );
6035
6358
  }
6036
6359
  const invalid = operation.kind === "delete" ? { ok: false, reason: "no body" } : invalidSampleOf(operation.inputModel);
6037
- const invalidConstant = `INVALID_${constantNameOf2(operation.inputType)}`;
6360
+ const invalidConstant = `INVALID_${constantNameOf3(operation.inputType)}`;
6038
6361
  const hasInvalid = invalid.ok || operation.kind === "update";
6039
6362
  if (hasInvalid) {
6040
6363
  factoryImports.add(invalidConstant);
@@ -6148,7 +6471,14 @@ function behaviorTestLines(plan, item, factoryImports, schemaImports, repository
6148
6471
  referenced: false,
6149
6472
  key: "ARRANGED_KEY",
6150
6473
  snapshotBefore: behavior.expectations.some((expectation) => expectation.raw === "rien n'est \xE9crit"),
6151
- needsReplacement: false
6474
+ needsReplacement: false,
6475
+ sharedExample: null,
6476
+ bindArranged: !operation.report
6477
+ };
6478
+ const examplesOf = (aggregate) => {
6479
+ const constant = `EXAMPLES_OF_${constantNameOf(aggregate.id)}`;
6480
+ useFactory(constant);
6481
+ return constant;
6152
6482
  };
6153
6483
  const useFactory = (name) => {
6154
6484
  factoryImports.add(name);
@@ -6175,11 +6505,21 @@ function behaviorTestLines(plan, item, factoryImports, schemaImports, repository
6175
6505
  if (key === `with:${guard.entry.valid.case}`) continue;
6176
6506
  if (key === "with:entr\xE9e valide") continue;
6177
6507
  if (key === "with:entr\xE9e invalide") {
6178
- const constant = `INVALID_${constantNameOf2(operation.inputType)}`;
6508
+ const constant = `INVALID_${constantNameOf3(operation.inputType)}`;
6179
6509
  useFactory(constant);
6180
6510
  derivation.input = constant;
6181
6511
  continue;
6182
6512
  }
6513
+ const firstAggregate = operation.aggregateHoles[0] ?? null;
6514
+ if (operation.kind === "read" && operation.motif === null && createFactory === null && firstAggregate !== null) {
6515
+ if (key === "with:lignes") {
6516
+ const constant = examplesOf(firstAggregate);
6517
+ derivation.arrangements.push(`...(${constant}[0]?.rows ?? [])`);
6518
+ if (firstAggregate.from.length > 0) derivation.sharedExample = `${constant}[0]?.shared ?? {}`;
6519
+ continue;
6520
+ }
6521
+ if (key === "without:ligne") continue;
6522
+ }
6183
6523
  if (operation.kind === "read" && operation.motif === null && createFactory !== null) {
6184
6524
  if (key === "with:lignes") {
6185
6525
  derivation.arrangements.push(`${createFactory}(0)`, `${createFactory}(1)`);
@@ -6294,7 +6634,7 @@ function behaviorTestLines(plan, item, factoryImports, schemaImports, repository
6294
6634
  const refusedByHole = hole !== null && behavior.code !== null && ((hole.kind === "validate" || hole.kind === "transform" || hole.kind === "derive") && hole.errorCode === behavior.code);
6295
6635
  if (hole !== null && refusedByHole) {
6296
6636
  if ((hole.kind === "validate" || hole.kind === "derive" && hole.reject !== null) && inputFactory !== null) {
6297
- const constant = `REJECT_EXAMPLE_OF_${constantNameOf2(hole.id)}`;
6637
+ const constant = `REJECT_EXAMPLE_OF_${constantNameOf3(hole.id)}`;
6298
6638
  useFactory(constant);
6299
6639
  derivation.overrides.push(`...${constant}`);
6300
6640
  } else if (hole.kind === "transform") {
@@ -6314,6 +6654,16 @@ function behaviorTestLines(plan, item, factoryImports, schemaImports, repository
6314
6654
  derivation.needsReplacement = true;
6315
6655
  derivation.expectedIds = "[arranged[0]?.id, replacement.id]";
6316
6656
  }
6657
+ if (hole !== null && hole.kind === "aggregate") {
6658
+ const constant = examplesOf(hole);
6659
+ derivation.arrangements = [`...(${constant}[0]?.rows ?? [])`];
6660
+ derivation.sharedExample = hole.from.length === 0 ? null : `${constant}[0]?.shared ?? {}`;
6661
+ if (inputFactory === null) derivation.input = `(${constant}[0]?.input ?? {})`;
6662
+ else {
6663
+ useFactory(inputFactory);
6664
+ derivation.input = `{ ...${inputFactory}(), ...(${constant}[0]?.input ?? {}) }`;
6665
+ }
6666
+ }
6317
6667
  if (createFactory !== null && derivation.arrangements.length > 0) useFactory(createFactory);
6318
6668
  if (inputFactory !== null && derivation.input !== null && derivation.input.startsWith(inputFactory)) useFactory(inputFactory);
6319
6669
  if (inputFactory !== null && (derivation.overrides.length > 0 || derivation.queryOverrides.length > 0)) {
@@ -6330,8 +6680,9 @@ function behaviorTestLines(plan, item, factoryImports, schemaImports, repository
6330
6680
  }
6331
6681
  const handle = arrangingHandle(plan, "member");
6332
6682
  if (derivation.arrangements.length > 0) {
6333
- lines.push(` const arranged = await arrangeRows(${handle}, [${derivation.arrangements.join(", ")}]);`);
6683
+ lines.push(` ${derivation.bindArranged ? "const arranged = " : ""}await arrangeRows(${handle}, [${derivation.arrangements.join(", ")}]);`);
6334
6684
  }
6685
+ if (derivation.sharedExample !== null) lines.push(` await arrangeShared(${handle}, ${derivation.sharedExample});`);
6335
6686
  if (derivation.needsReplacement && createFactory !== null) {
6336
6687
  lines.push(` const replacement = await insert${plan.resource.typeName}(${handle}, ${createFactory}(0, { supersedes: arranged[0]?.id })${plan.signed === null ? "" : ", ARRANGED_SIGNATURE"}, CLOCK);`);
6337
6688
  }
@@ -6442,6 +6793,9 @@ function behaviorTestLines(plan, item, factoryImports, schemaImports, repository
6442
6793
  const target = signatureTarget();
6443
6794
  const expected = value === "renouvel\xE9e" ? `${plan.signature?.exportName ?? "signerOf"}(${derivation.session}.context)` : "ARRANGED_SIGNATURE.by";
6444
6795
  lines.push(` expect(${target}?.${plan.signed.by}).toBe(${expected});`);
6796
+ } else if (value.endsWith(" d\xE9riv\xE9") && operation.aggregateHoles.some((candidate) => value === `${candidate.writes} d\xE9riv\xE9`)) {
6797
+ const writer = operation.aggregateHoles.find((candidate) => value === `${candidate.writes} d\xE9riv\xE9`);
6798
+ if (writer !== void 0) lines.push(` expect(output.${subPath}).toEqual(${examplesOf(writer)}[0]?.value);`);
6445
6799
  } else if (value.endsWith(" d\xE9riv\xE9") && operation.deriveHoles.some((candidate) => value === `${candidate.writes} d\xE9riv\xE9`)) {
6446
6800
  const field = value.slice(0, -" d\xE9riv\xE9".length);
6447
6801
  lines.push(` expect(valuesAt(${root}, ${quote(subPath)})).toEqual([DERIVED_OF_VALID_CREATES[0]?.[${quote(field)}]]);`);
@@ -6502,7 +6856,7 @@ function behaviorTestLines(plan, item, factoryImports, schemaImports, repository
6502
6856
  return lines;
6503
6857
  }
6504
6858
  function emitBehaviorTests(plan) {
6505
- const { imports, helpers } = harnessLines(plan, { arranging: true });
6859
+ const { imports, helpers } = harnessLines(plan, { arranging: true, report: true });
6506
6860
  const factoryImports = /* @__PURE__ */ new Set();
6507
6861
  const schemaImports = /* @__PURE__ */ new Set();
6508
6862
  const repositoryImports = /* @__PURE__ */ new Set();
@@ -6542,6 +6896,9 @@ function emitBehaviorTests(plan) {
6542
6896
  }
6543
6897
 
6544
6898
  // src/emit/repository.ts
6899
+ function writtenTypeOf(plan, newType) {
6900
+ return plan.tenant === null ? newType : `Omit<${newType}, ${quote(plan.tenant.columnKey)}>`;
6901
+ }
6545
6902
  function comparatorLines(plan, operation) {
6546
6903
  const order = operation.order;
6547
6904
  if (order === null) return [" return 0;"];
@@ -6680,7 +7037,7 @@ function insertFunctionLines2(plan, operation) {
6680
7037
  const signature = signatureParts(plan);
6681
7038
  const derivedParameter = derived.length === 0 ? "" : `, derived: Pick<${plan.resource.typeName}, ${derived.map(quote).join(" | ")}>`;
6682
7039
  const valuesLines = [
6683
- ` const values: Omit<${plan.table.newType}, ${quote(tenantOf(plan).columnKey)}> = {`,
7040
+ ` const values: ${writtenTypeOf(plan, plan.table.newType)} = {`,
6684
7041
  " id,",
6685
7042
  ...inputFieldsPresent.map((field) => ` ${field}: input.${field},`),
6686
7043
  ...derived.map((field) => ` ${field}: derived.${field},`),
@@ -6790,7 +7147,7 @@ function insertFunctionLines2(plan, operation) {
6790
7147
  " 409,",
6791
7148
  " );",
6792
7149
  " }",
6793
- ` const marking: Partial<Omit<${plan.table.newType}, ${quote(tenantOf(plan).columnKey)}>> = { supersededBy: id };`,
7150
+ ` const marking: Partial<${writtenTypeOf(plan, plan.table.newType)}> = { supersededBy: id };`,
6794
7151
  ` await db.update(${table}).set(marking).where(eq(${table}.id, old.id));`,
6795
7152
  " } else {",
6796
7153
  ` const [existing] = await db`,
@@ -6882,7 +7239,7 @@ function importFunctionLines(plan, operation) {
6882
7239
  }
6883
7240
  lines.push(
6884
7241
  " for (const { row } of rows) {",
6885
- ` const values: Omit<${plan.table.newType}, ${quote(tenantOf(plan).columnKey)}> = {`,
7242
+ ` const values: ${writtenTypeOf(plan, plan.table.newType)} = {`,
6886
7243
  " id: crypto.randomUUID(),",
6887
7244
  ...valueFields.map((field) => ` ${field}: row.${field},`),
6888
7245
  ...appendOnly === null ? [] : [" supersedes: null,", " supersededBy: null,"],
@@ -6919,7 +7276,7 @@ function updateFunctionLines2(plan, operation) {
6919
7276
  `/** ${operation.id} \u2014 update by key (D28): the fields sent are written, the key never changes${plan.signed === null ? "" : ", the signature is renewed (D31)"}. */`,
6920
7277
  `export async function update${plan.resource.typeName}ByKey(ctx: ${plan.context.typeName}, key: ${plan.resource.typeName}[${quote(plan.key.field)}], input: ${operation.inputType}${signature.parameter}): Promise<${plan.table.rowType}> {`,
6921
7278
  ` const row = await load${plan.resource.typeName}ByKey(ctx, key, ${quote(operation.notFound ?? "")});`,
6922
- ` const patch: Partial<Omit<${plan.table.newType}, ${quote(tenantOf(plan).columnKey)}>> = {`,
7279
+ ` const patch: Partial<${writtenTypeOf(plan, plan.table.newType)}> = {`,
6923
7280
  ...fields.map((field) => ` ...(input.${field} === undefined ? {} : { ${field}: input.${field} }),`),
6924
7281
  ...signature.values,
6925
7282
  " };",
@@ -6956,11 +7313,11 @@ function setOrClearFunctionLines2(plan, operation) {
6956
7313
  " }",
6957
7314
  ...idempotent ? [` if (existing !== undefined && existing.${slot.value} === value) return "unchanged";`] : [],
6958
7315
  " if (existing !== undefined) {",
6959
- ` const patch: Partial<Omit<${slot.table.newType}, ${quote(tenantOf(plan).columnKey)}>> = { ${slot.value}: value${signature.values.length === 0 ? "" : `, ${plan.signed?.by ?? ""}: signature.by, ${plan.signed?.at ?? ""}: signature.at`} };`,
7316
+ ` const patch: Partial<${writtenTypeOf(plan, slot.table.newType)}> = { ${slot.value}: value${signature.values.length === 0 ? "" : `, ${plan.signed?.by ?? ""}: signature.by, ${plan.signed?.at ?? ""}: signature.at`} };`,
6960
7317
  ` await db.update(${table}).set(patch).where(${whereExisting});`,
6961
7318
  ` return "set";`,
6962
7319
  " }",
6963
- ` const values: Omit<${slot.table.newType}, ${quote(tenantOf(plan).columnKey)}> = {`,
7320
+ ` const values: ${writtenTypeOf(plan, slot.table.newType)} = {`,
6964
7321
  ...slot.table.hasId ? [" id: crypto.randomUUID(),"] : [],
6965
7322
  ` ${slot.parentKey}: parent.${plan.key.field},`,
6966
7323
  ` ${slot.key}: slotKey,`,
@@ -7027,13 +7384,46 @@ function referenceLines2(plan, reference) {
7027
7384
  "",
7028
7385
  `/** One ${reference.name} row pointing at a resource key \u2014 what a test arranges to prove \xAB ligne r\xE9f\xE9renc\xE9e \xBB. */`,
7029
7386
  `export async function insert${typeName}(ctx: ${plan.context.typeName}, values: Pick<${reference.table.newType}, ${writtenColumns2.map(quote).join(" | ")}>): Promise<${reference.table.rowType}> {`,
7030
- ` const inserted: Omit<${reference.table.newType}, ${quote(tenantOf(plan).columnKey)}> = { ...values };`,
7387
+ ` const inserted: ${writtenTypeOf(plan, reference.table.newType)} = { ...values };`,
7031
7388
  ` const [row] = await tenantDb(ctx).insert(${reference.table.tsName}).values(inserted).returning();`,
7032
7389
  " if (row === undefined) throwOgygieError(insertReturnedNothing());",
7033
7390
  " return row;",
7034
7391
  "}"
7035
7392
  ];
7036
7393
  }
7394
+ function sharedReadLines2(plan, table) {
7395
+ return [
7396
+ `/** 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. */`,
7397
+ `export async function select${table.typeName}Rows(ctx: ${plan.context.typeName}): Promise<${table.rowType}[]> {`,
7398
+ ` return tenantDb(ctx).select(${table.tsName}).all();`,
7399
+ "}"
7400
+ ];
7401
+ }
7402
+ function insertExpressionOf(field) {
7403
+ if (field.model.kind === "string" && field.model.format === "datetime") {
7404
+ return field.model.nullable ? `row.${field.name} === null ? null : new Date(row.${field.name})` : `new Date(row.${field.name})`;
7405
+ }
7406
+ return `row.${field.name}`;
7407
+ }
7408
+ function arrangeLines2(plan, target) {
7409
+ const valuesType = target.newType === null ? `typeof ${target.tsName}.$inferInsert` : target.newType;
7410
+ return [
7411
+ `/** 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. */`,
7412
+ `export async function arrange${target.typeName}Rows(ctx: ${plan.context.typeName}, rows: readonly ${target.withId ? `Omit<${target.typeName}, "id">` : target.typeName}[]): Promise<${target.rowType}[]> {`,
7413
+ ` const inserted: ${target.rowType}[] = [];`,
7414
+ " for (const row of rows) {",
7415
+ ` const values: ${writtenTypeOf(plan, valuesType)} = {`,
7416
+ ...target.withId ? [" id: crypto.randomUUID(),"] : [],
7417
+ ...target.fields.filter((field) => !(target.withId && field.name === "id")).map((field) => ` ${field.name}: ${insertExpressionOf(field)},`),
7418
+ " };",
7419
+ ` const [written] = await tenantDb(ctx).insert(${target.tsName}).values(values).returning();`,
7420
+ " if (written === undefined) throwOgygieError(insertReturnedNothing());",
7421
+ " inserted.push(written);",
7422
+ " }",
7423
+ " return inserted;",
7424
+ "}"
7425
+ ];
7426
+ }
7037
7427
  function viewReadLines2(plan, view) {
7038
7428
  return [
7039
7429
  `/** The rows of the view ${view.name} (hole ${view.id}, D32) for the tenant, through its read-only mapping. */`,
@@ -7064,34 +7454,44 @@ function emitRepository(plan) {
7064
7454
  }
7065
7455
  const queryTypes = reads.filter((operation) => operation.motif?.method === "currentByKey").map((operation) => operation.inputType);
7066
7456
  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();
7457
+ const arranging = create === void 0 && plan.operations.some((operation) => operation.aggregateHoles.length > 0);
7458
+ 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
7459
  const tableImports = [`${plan.table.tsName}, type ${plan.table.newType}, type ${plan.table.rowType}`];
7069
7460
  for (const slot of plan.slots) tableImports.push(`${slot.table.tsName}, type ${slot.table.newType}, type ${slot.table.rowType}`);
7070
7461
  for (const reference of plan.references) if (reference.table !== null) tableImports.push(`${reference.table.tsName}, type ${reference.table.newType}, type ${reference.table.rowType}`);
7071
7462
  for (const view of views) tableImports.push(`${view.tsName}, type ${view.rowType}`);
7463
+ for (const table of plan.sharedTables) tableImports.push(`${table.tsName}, type ${table.rowType}`);
7072
7464
  const sections = [];
7073
7465
  for (const operation of reads) sections.push("", ...readFunctionLines2(plan, operation));
7074
7466
  for (const slot of plan.slots) sections.push("", ...slotReadLines2(plan, slot));
7075
7467
  for (const reference of plan.references) sections.push("", ...referenceLines2(plan, reference));
7076
7468
  for (const view of views) sections.push("", ...viewReadLines2(plan, view));
7469
+ for (const table of plan.sharedTables) sections.push("", ...sharedReadLines2(plan, table));
7470
+ if (create === void 0 && plan.operations.some((operation) => operation.aggregateHoles.length > 0)) {
7471
+ 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 }));
7472
+ 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 }));
7473
+ }
7077
7474
  if (plan.key !== null) sections.push("", ...loadByKeyLines2(plan));
7078
7475
  if (create !== void 0) sections.push("", ...insertFunctionLines2(plan, create));
7079
7476
  if (update !== void 0) sections.push("", ...updateFunctionLines2(plan, update));
7080
7477
  if (setOrClear !== void 0) sections.push("", ...setOrClearFunctionLines2(plan, setOrClear));
7081
7478
  if (deleteOperation !== void 0) sections.push("", ...deleteFunctionLines2(plan, deleteOperation));
7082
7479
  if (importOperation !== void 0) sections.push("", ...importFunctionLines(plan, importOperation));
7480
+ const noTenant = plan.tenant === null;
7481
+ const handleType = noTenant ? plan.databasePort?.typeName ?? "Database" : plan.context.typeName;
7482
+ const finalSections = noTenant ? sections.map((line) => line.replace(/\btenantDb\(ctx\)/gu, "plainDb(db)").replace(/\bctx\b/gu, "db").replaceAll(`db: ${plan.context.typeName}`, `db: ${handleType}`)) : sections;
7083
7483
  return fileOf(
7084
7484
  tsGeneratedHeader(plan.generatorVersion),
7085
7485
  intentBlock(
7086
7486
  wrap(
7087
- `Persistence of the ${plan.featureId} feature \u2014 the only file of the feature allowed to touch the database, always through tenantDb: the tenant column is injected on insert and filtered on every read, never written by hand (AGENTS.md \xA7Multi-tenant).${plan.appendOnly === null ? "" : ` The append-only motif appendOnlySupersedes on (${plan.appendOnly.key.join(", ")}): current rows per key, supersede by marking the old row BEFORE inserting the new one.`}${plan.importMotif === null ? "" : " The all-or-nothing import: one transaction or nothing."}${plan.key === null ? "" : ` The keyed writes of D28 address a row by ${plan.key.field} and answer the operation's 404 otherwise.`}${plan.unique === null ? "" : ` uniqueKey: 409 ${plan.unique.error} on a taken key.`}${plan.referenced === null ? "" : ` refuseWhenReferenced: 409 ${plan.referenced.error} while a ${plan.referenced.reference.name} row points at the row.`}${plan.signed === null ? "" : ` signedWrite: ${plan.signed.by} and ${plan.signed.at} on every write.`}${views.length === 0 ? "" : ` The views ${views.map((view) => view.name).join(", ")} are read through their read-only mappings (D32).`} Derived from ${plan.specPath}.`
7487
+ `Persistence of the ${plan.featureId} feature \u2014 the only file of the feature allowed to touch the database, ${noTenant ? "always through plainDb: the product is single-tenant (target.tenancy none, D33), the whole table is read and written through the one facade, never a raw query builder" : "always through tenantDb: the tenant column is injected on insert and filtered on every read, never written by hand (AGENTS.md \xA7Multi-tenant)"}.${plan.appendOnly === null ? "" : ` The append-only motif appendOnlySupersedes on (${plan.appendOnly.key.join(", ")}): current rows per key, supersede by marking the old row BEFORE inserting the new one.`}${plan.importMotif === null ? "" : " The all-or-nothing import: one transaction or nothing."}${plan.key === null ? "" : ` The keyed writes of D28 address a row by ${plan.key.field} and answer the operation's 404 otherwise.`}${plan.unique === null ? "" : ` uniqueKey: 409 ${plan.unique.error} on a taken key.`}${plan.referenced === null ? "" : ` refuseWhenReferenced: 409 ${plan.referenced.error} while a ${plan.referenced.reference.name} row points at the row.`}${plan.signed === null ? "" : ` signedWrite: ${plan.signed.by} and ${plan.signed.at} on every write.`}${views.length === 0 ? "" : ` The views ${views.map((view) => view.name).join(", ")} are read through their read-only mappings (D32).`} Derived from ${plan.specPath}.`
7088
7488
  )
7089
7489
  ),
7090
7490
  drizzleImports.size === 0 ? null : `import { ${[...drizzleImports].sort().join(", ")} } from "drizzle-orm";`,
7091
7491
  'import type { Clock } from "@ogygie/core/clock";',
7092
7492
  'import { throwOgygieError } from "@ogygie/core/errors";',
7093
- 'import { tenantDb } from "@ogygie/core/tenant";',
7094
- `import type { ${plan.context.typeName} } from ${quote(plan.context.importPath)};`,
7493
+ noTenant ? 'import { plainDb } from "@ogygie/core/tenant";' : 'import { tenantDb } from "@ogygie/core/tenant";',
7494
+ noTenant ? `import type { ${handleType} } from ${quote(plan.databasePort?.importPath ?? "../../db.js")};` : `import type { ${plan.context.typeName} } from ${quote(plan.context.importPath)};`,
7095
7495
  "import {",
7096
7496
  ...tableImports.map((line) => ` ${line},`),
7097
7497
  '} from "./db.schema.gen.js";',
@@ -7126,7 +7526,7 @@ function emitRepository(plan) {
7126
7526
  ` suggestedFix: "Check the migration ${plan.migrationFileName} was applied to this database.",`,
7127
7527
  " };",
7128
7528
  "}",
7129
- ...sections
7529
+ ...finalSections
7130
7530
  );
7131
7531
  }
7132
7532
 
@@ -7340,7 +7740,51 @@ function emitService(plan) {
7340
7740
  const signatureLine = plan.signed === null ? [] : [` const signature: WriteSignature = { by: ${plan.signature?.exportName ?? "signerOf"}(deps.ctx), at: deps.clock.now() };`];
7341
7741
  const signatureArgument = plan.signed === null ? "" : ", signature";
7342
7742
  if (plan.signed !== null) repositoryImports.add("type WriteSignature");
7743
+ const projectionLines = (operation) => {
7744
+ if (operation.projectHoles.length === 0) return [" return computed;"];
7745
+ const lines = [];
7746
+ if (plan.tenant === null) lines.push(" const projection = deps.ctx;");
7747
+ else lines.push(" const { db: _db, ...projection } = deps.ctx;", " void _db;");
7748
+ let current = "computed";
7749
+ for (const hole of operation.projectHoles) {
7750
+ lines.push(` const ${hole.id}Output = ${hole.id}(projection, ${current});`);
7751
+ current = `${hole.id}Output`;
7752
+ }
7753
+ lines.push(` return ${current};`);
7754
+ return lines;
7755
+ };
7756
+ const reportBodyLines = (operation) => {
7757
+ const selectName = `select${operation.pascalId}`;
7758
+ repositoryImports.add(selectName);
7759
+ const lines = [` const rows = (await ${selectName}(${handle})).map(${dto});`];
7760
+ const sharedUsed = /* @__PURE__ */ new Map();
7761
+ for (const hole of operation.aggregateHoles) for (const table of hole.from) sharedUsed.set(table.sqlName, table);
7762
+ for (const table of sharedUsed.values()) {
7763
+ repositoryImports.add(`select${table.typeName}Rows`);
7764
+ dbImports.add(`type ${table.rowType}`);
7765
+ typeImports.add(table.typeName);
7766
+ lines.push(` const ${table.tsName}Rows = (await select${table.typeName}Rows(${handle})).map(to${table.typeName}Dto);`);
7767
+ }
7768
+ for (const hole of operation.aggregateHoles) {
7769
+ const picked = hole.reads.map((field) => `${field}: input.${field}`).join(", ");
7770
+ const shared = hole.from.length === 0 ? "" : `, { ${hole.from.map((table) => `${table.tsName}: ${table.tsName}Rows`).join(", ")} }`;
7771
+ const call = `${hole.id}({ ${picked} }, rows${shared}${effectArguments(plan, hole)})`;
7772
+ lines.push(` const ${hole.id}Value = ${isAsyncHole(hole) ? `await ${call}` : call};`);
7773
+ }
7774
+ const rootFields = [];
7775
+ if (operation.outputModel.kind === "object") {
7776
+ for (const [name, model] of Object.entries(operation.outputModel.fields)) {
7777
+ const hole = operation.aggregateHoles.find((candidate) => candidate.writes === name);
7778
+ if (hole !== void 0) rootFields.push(` ${name}: ${hole.id}Value,`);
7779
+ else if (operation.echoFields.includes(name)) rootFields.push(` ${name}: input.${name},`);
7780
+ else rootFields.push(` ${name}: ${neutralValueOf(model)},`);
7781
+ }
7782
+ }
7783
+ lines.push(` const computed: ${operation.outputType} = {`, ...rootFields, " };", ...projectionLines(operation));
7784
+ return lines;
7785
+ };
7343
7786
  const readBodyLines = (operation) => {
7787
+ if (operation.report) return reportBodyLines(operation);
7344
7788
  const element = operation.element;
7345
7789
  if (element === null) {
7346
7790
  const selectName2 = `select${operation.pascalId}`;
@@ -7400,20 +7844,9 @@ function emitService(plan) {
7400
7844
  ...elementFields,
7401
7845
  " })),",
7402
7846
  ...rootFields,
7403
- " };"
7847
+ " };",
7848
+ ...projectionLines(operation)
7404
7849
  );
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
7850
  return lines;
7418
7851
  };
7419
7852
  const respondLines = (operation) => {
@@ -7429,7 +7862,7 @@ function emitService(plan) {
7429
7862
  "",
7430
7863
  `/** ${operation.method} ${operation.path} \u2014 ${operation.id}. */`,
7431
7864
  `export async function ${operation.serviceName}(deps: ${deps}, input: ${operation.inputType}): Promise<${operation.outputType}> {`,
7432
- ...operation.echoFields.length === 0 && operation.motif?.method !== "currentByKey" ? [" void input;"] : [],
7865
+ ...operation.echoFields.length === 0 && operation.motif?.method !== "currentByKey" && !operation.aggregateHoles.some((hole) => hole.reads.length > 0) ? [" void input;"] : [],
7433
7866
  ...readBodyLines(operation),
7434
7867
  "}"
7435
7868
  );
@@ -7588,6 +8021,16 @@ function emitService(plan) {
7588
8021
  " };",
7589
8022
  "}"
7590
8023
  ];
8024
+ const sharedUsedByHoles = plan.sharedTables.filter((table) => plan.holes.some((hole) => hole.kind === "aggregate" && hole.from.some((candidate) => candidate.sqlName === table.sqlName)));
8025
+ const sharedDtoLines = sharedUsedByHoles.flatMap((table) => [
8026
+ "",
8027
+ `/** One row of the shared table ${table.sqlName} at its schema (D60): what the aggregate holes receive. */`,
8028
+ `function to${table.typeName}Dto(row: ${table.rowType}): ${table.typeName} {`,
8029
+ " return {",
8030
+ ...table.fields.map((field) => ` ${field.name}: ${field.dtoExpression},`),
8031
+ " };",
8032
+ "}"
8033
+ ]);
7591
8034
  const slotHelpers = plan.slots.flatMap((slot) => {
7592
8035
  const dtoFields = slot.fields.map((field) => `${field.name}: ${field.dtoExpression}`).join(", ");
7593
8036
  const keyValues = slot.keyValues === null ? null : `KEY_VALUES_OF_${slot.name.toUpperCase()}`;
@@ -7636,6 +8079,7 @@ ${[...typeImports].sort().map((name) => ` type ${name},`).join("\n")}
7636
8079
  "}",
7637
8080
  "",
7638
8081
  ...dtoLines,
8082
+ ...sharedDtoLines,
7639
8083
  ...slotHelpers,
7640
8084
  "",
7641
8085
  "/** A hole's refusal becomes the operation's 400 OgygieError. */",
@@ -7758,7 +8202,8 @@ function manifestEntryOf(plan) {
7758
8202
  })),
7759
8203
  // The views of the view holes are read through Drizzle like tables: what the repository reads is on the map (D32).
7760
8204
  // 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] : [])]
8205
+ // 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.
8206
+ 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
8207
  };
7763
8208
  }
7764
8209
  function canonicalJson(value) {
@@ -7990,6 +8435,8 @@ function holeLine(hole, plan) {
7990
8435
  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
8436
  case "derive":
7992
8437
  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}`;
8438
+ case "aggregate":
8439
+ 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
8440
  case "project":
7994
8441
  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
8442
  case "view":
@@ -8037,6 +8484,7 @@ function emitSpecContracts(plan) {
8037
8484
  ...tableSection(plan.table, plan, indexes.length === 0 ? [] : [`index ${indexes.join(" ; ")}`]),
8038
8485
  ...detailTables.flatMap((table) => ["", ...tableSection(table, plan, [])]),
8039
8486
  ...views.flatMap((view) => ["", ...viewSection(view)]),
8487
+ ...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
8488
  "",
8041
8489
  "## Trous (D23)",
8042
8490
  "",
@@ -11504,10 +11952,14 @@ export {
11504
11952
  tableNameOf,
11505
11953
  tableRefOf,
11506
11954
  parentKeyFieldOf,
11955
+ SharedTableSchema,
11956
+ sharedTableNameOf,
11957
+ typedSharedTablesOf,
11507
11958
  OperationSchema,
11508
11959
  isHttpOperation,
11509
11960
  isWriteOperation,
11510
11961
  isKeyedOperation,
11962
+ AggregateHoleSchema,
11511
11963
  HoleSchema,
11512
11964
  acceptExamplesOf,
11513
11965
  HOLE_KIND_SERVES,
@@ -11588,6 +12040,7 @@ export {
11588
12040
  tablesOf,
11589
12041
  ownedTables,
11590
12042
  viewBodiesOf,
12043
+ deepOverlay,
11591
12044
  normalizedTypeOf,
11592
12045
  HoleShapeSchema,
11593
12046
  HoleProvenanceSchema,
@@ -11603,7 +12056,6 @@ export {
11603
12056
  HOLES_LIBRARY_DIR,
11604
12057
  loadHolesLibrary,
11605
12058
  libraryFileNameOf,
11606
- deepOverlay,
11607
12059
  GENERATED_FEATURE_FILES,
11608
12060
  generateFeature,
11609
12061
  migrationFileNameFor,