@metaobjectsdev/codegen-ts 0.23.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/column-mapper.d.ts +32 -0
- package/dist/column-mapper.d.ts.map +1 -1
- package/dist/column-mapper.js +91 -8
- package/dist/column-mapper.js.map +1 -1
- package/dist/enum-meta.d.ts +20 -0
- package/dist/enum-meta.d.ts.map +1 -1
- package/dist/enum-meta.js +32 -1
- package/dist/enum-meta.js.map +1 -1
- package/dist/generator.d.ts +9 -0
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js.map +1 -1
- package/dist/generators/api-field-shape.js +1 -1
- package/dist/generators/api-field-shape.js.map +1 -1
- package/dist/generators/api-model.d.ts.map +1 -1
- package/dist/generators/api-model.js +71 -42
- package/dist/generators/api-model.js.map +1 -1
- package/dist/generators/docs-data-builder.d.ts.map +1 -1
- package/dist/generators/docs-data-builder.js +36 -1
- package/dist/generators/docs-data-builder.js.map +1 -1
- package/dist/generators/docs-data.d.ts +14 -0
- package/dist/generators/docs-data.d.ts.map +1 -1
- package/dist/generators/docs-file.d.ts.map +1 -1
- package/dist/generators/docs-file.js +13 -4
- package/dist/generators/docs-file.js.map +1 -1
- package/dist/generators/extractor-file.d.ts.map +1 -1
- package/dist/generators/extractor-file.js +7 -11
- package/dist/generators/extractor-file.js.map +1 -1
- package/dist/generators/index.d.ts +4 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +5 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/output-parser-file.d.ts.map +1 -1
- package/dist/generators/output-parser-file.js +12 -7
- package/dist/generators/output-parser-file.js.map +1 -1
- package/dist/generators/output-prompt-file.d.ts.map +1 -1
- package/dist/generators/output-prompt-file.js +14 -24
- package/dist/generators/output-prompt-file.js.map +1 -1
- package/dist/generators/requirement-tests.d.ts +44 -0
- package/dist/generators/requirement-tests.d.ts.map +1 -0
- package/dist/generators/requirement-tests.js +127 -0
- package/dist/generators/requirement-tests.js.map +1 -0
- package/dist/generators/requirements-file.d.ts +9 -0
- package/dist/generators/requirements-file.d.ts.map +1 -0
- package/dist/generators/requirements-file.js +53 -0
- package/dist/generators/requirements-file.js.map +1 -0
- package/dist/generators/requirements-markdown.d.ts +10 -0
- package/dist/generators/requirements-markdown.d.ts.map +1 -0
- package/dist/generators/requirements-markdown.js +71 -0
- package/dist/generators/requirements-markdown.js.map +1 -0
- package/dist/generators/requirements-toon.d.ts +3 -0
- package/dist/generators/requirements-toon.d.ts.map +1 -0
- package/dist/generators/requirements-toon.js +47 -0
- package/dist/generators/requirements-toon.js.map +1 -0
- package/dist/generators/requirements-view.d.ts +32 -0
- package/dist/generators/requirements-view.d.ts.map +1 -0
- package/dist/generators/requirements-view.js +64 -0
- package/dist/generators/requirements-view.js.map +1 -0
- package/dist/generators/trace-helper-file.d.ts.map +1 -1
- package/dist/generators/trace-helper-file.js +19 -11
- package/dist/generators/trace-helper-file.js.map +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -1
- package/dist/metaobjects-config.d.ts +12 -20
- package/dist/metaobjects-config.d.ts.map +1 -1
- package/dist/metaobjects-config.js +5 -1
- package/dist/metaobjects-config.js.map +1 -1
- package/dist/orphan-sweep.d.ts +38 -0
- package/dist/orphan-sweep.d.ts.map +1 -0
- package/dist/orphan-sweep.js +129 -0
- package/dist/orphan-sweep.js.map +1 -0
- package/dist/overwrite-policy.d.ts +97 -3
- package/dist/overwrite-policy.d.ts.map +1 -1
- package/dist/overwrite-policy.js +293 -81
- package/dist/overwrite-policy.js.map +1 -1
- package/dist/projection/build-projection-views.d.ts +8 -0
- package/dist/projection/build-projection-views.d.ts.map +1 -1
- package/dist/projection/build-projection-views.js +2 -0
- package/dist/projection/build-projection-views.js.map +1 -1
- package/dist/projection/extract-view-spec.d.ts.map +1 -1
- package/dist/projection/extract-view-spec.js +70 -6
- package/dist/projection/extract-view-spec.js.map +1 -1
- package/dist/reconcile-orphans.d.ts +80 -0
- package/dist/reconcile-orphans.d.ts.map +1 -0
- package/dist/reconcile-orphans.js +61 -0
- package/dist/reconcile-orphans.js.map +1 -0
- package/dist/render-engine/embedded-templates.generated.js +1 -1
- package/dist/render-engine/embedded-templates.generated.js.map +1 -1
- package/dist/requirement-walk.d.ts +57 -0
- package/dist/requirement-walk.d.ts.map +1 -0
- package/dist/requirement-walk.js +94 -0
- package/dist/requirement-walk.js.map +1 -0
- package/dist/runner.d.ts +35 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +217 -21
- package/dist/runner.js.map +1 -1
- package/dist/templates/drizzle-schema.d.ts.map +1 -1
- package/dist/templates/drizzle-schema.js +67 -4
- package/dist/templates/drizzle-schema.js.map +1 -1
- package/dist/templates/extractor.d.ts +4 -4
- package/dist/templates/extractor.d.ts.map +1 -1
- package/dist/templates/extractor.js +18 -22
- package/dist/templates/extractor.js.map +1 -1
- package/dist/templates/filter-allowlist.d.ts.map +1 -1
- package/dist/templates/filter-allowlist.js +4 -2
- package/dist/templates/filter-allowlist.js.map +1 -1
- package/dist/templates/filter-type.d.ts.map +1 -1
- package/dist/templates/filter-type.js +6 -2
- package/dist/templates/filter-type.js.map +1 -1
- package/dist/templates/find-inbound.d.ts +44 -0
- package/dist/templates/find-inbound.d.ts.map +1 -0
- package/dist/templates/find-inbound.js +69 -0
- package/dist/templates/find-inbound.js.map +1 -0
- package/dist/templates/output-format-spec-emitter.d.ts.map +1 -1
- package/dist/templates/output-format-spec-emitter.js +7 -4
- package/dist/templates/output-format-spec-emitter.js.map +1 -1
- package/dist/templates/output-parser.d.ts +8 -3
- package/dist/templates/output-parser.d.ts.map +1 -1
- package/dist/templates/output-parser.js +71 -31
- package/dist/templates/output-parser.js.map +1 -1
- package/dist/templates/output-prompt.d.ts +6 -5
- package/dist/templates/output-prompt.d.ts.map +1 -1
- package/dist/templates/output-prompt.js +26 -37
- package/dist/templates/output-prompt.js.map +1 -1
- package/dist/templates/queries.js +1 -1
- package/dist/templates/queries.js.map +1 -1
- package/dist/templates/requirement-test.d.ts +12 -0
- package/dist/templates/requirement-test.d.ts.map +1 -0
- package/dist/templates/requirement-test.js +120 -0
- package/dist/templates/requirement-test.js.map +1 -0
- package/dist/templates/zod-validators.d.ts.map +1 -1
- package/dist/templates/zod-validators.js +32 -15
- package/dist/templates/zod-validators.js.map +1 -1
- package/package.json +7 -6
- package/src/column-mapper.ts +124 -8
- package/src/enum-meta.ts +37 -1
- package/src/generator.ts +9 -0
- package/src/generators/api-field-shape.ts +1 -1
- package/src/generators/api-model.ts +69 -44
- package/src/generators/docs-data-builder.ts +37 -0
- package/src/generators/docs-data.ts +15 -0
- package/src/generators/docs-file.ts +13 -4
- package/src/generators/extractor-file.ts +7 -11
- package/src/generators/index.ts +11 -0
- package/src/generators/output-parser-file.ts +12 -7
- package/src/generators/output-prompt-file.ts +14 -27
- package/src/generators/requirement-tests.ts +203 -0
- package/src/generators/requirements-file.ts +71 -0
- package/src/generators/requirements-markdown.ts +72 -0
- package/src/generators/requirements-toon.ts +64 -0
- package/src/generators/requirements-view.ts +93 -0
- package/src/generators/trace-helper-file.ts +20 -10
- package/src/index.ts +47 -1
- package/src/metaobjects-config.ts +17 -22
- package/src/orphan-sweep.ts +178 -0
- package/src/overwrite-policy.ts +362 -89
- package/src/projection/build-projection-views.ts +10 -0
- package/src/projection/extract-view-spec.ts +88 -4
- package/src/reconcile-orphans.ts +136 -0
- package/src/reference/barrel.ts +4 -0
- package/src/reference/entity.ts +4 -0
- package/src/reference/queries.ts +4 -0
- package/src/reference/routes.ts +4 -0
- package/src/render-engine/embedded-templates.generated.ts +1 -1
- package/src/requirement-walk.ts +124 -0
- package/src/runner.ts +266 -27
- package/src/templates/drizzle-schema.ts +70 -6
- package/src/templates/extractor.ts +19 -24
- package/src/templates/filter-allowlist.ts +4 -2
- package/src/templates/filter-type.ts +6 -2
- package/src/templates/find-inbound.ts +96 -0
- package/src/templates/output-format-spec-emitter.ts +7 -4
- package/src/templates/output-parser.ts +76 -34
- package/src/templates/output-prompt.ts +29 -42
- package/src/templates/queries.ts +1 -1
- package/src/templates/requirement-test.ts +140 -0
- package/src/templates/zod-validators.ts +32 -15
- package/templates/docs/entity-page.md.mustache +8 -0
package/src/column-mapper.ts
CHANGED
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
AGG_COLLECT,
|
|
47
47
|
} from "@metaobjectsdev/metadata";
|
|
48
48
|
import { columnNameFromField } from "./naming.js";
|
|
49
|
-
import { enumValues } from "./enum-meta.js";
|
|
49
|
+
import { enumValues, intValueMapOf, intValueForMember } from "./enum-meta.js";
|
|
50
50
|
import { DEFAULT_COLUMN_NAMING_STRATEGY, stripPackage } from "@metaobjectsdev/metadata";
|
|
51
51
|
import type { Dialect, ColumnNamingStrategy } from "./metaobjects-config.js";
|
|
52
52
|
|
|
@@ -212,9 +212,42 @@ function canonicalizeSqlExpr(value: string): string {
|
|
|
212
212
|
return value; // unrecognized — pass through (function calls etc.)
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
/**
|
|
216
|
+
* An int-backed `field.enum` column: a generated Drizzle `customType` whose
|
|
217
|
+
* `toDriver`/`fromDriver` translate member symbol <-> stored integer, so the
|
|
218
|
+
* codec lives in the COLUMN definition rather than in the query layer.
|
|
219
|
+
*
|
|
220
|
+
* This is the TS analogue of what every other port already does at its own
|
|
221
|
+
* `MetaField` codec seam (EF Core `HasConversion`, OMDB `JdbcFieldCodec`, Exposed
|
|
222
|
+
* `customEnumeration`, Python `ObjectManager` coercion) — which is why it was
|
|
223
|
+
* chosen over a Zod write-transform plus a bespoke read-decode: TS's generated
|
|
224
|
+
* queries hand back raw Drizzle rows and have no decode seam at all, so a
|
|
225
|
+
* query-layer codec would have meant inventing one and wrapping every generated
|
|
226
|
+
* read. Binding through the column type also makes filter values encode for free.
|
|
227
|
+
*/
|
|
228
|
+
export interface EnumIntCustomType {
|
|
229
|
+
/** Local const name for the customType column helper, e.g. `orderStatusEnumCol`. */
|
|
230
|
+
fnConstName: string;
|
|
231
|
+
/** Local const name for the symbol->int map, e.g. `ORDER_STATUS_TO_INT`. */
|
|
232
|
+
toIntConstName: string;
|
|
233
|
+
/** Local const name for the int->symbol map, e.g. `ORDER_STATUS_FROM_INT`. */
|
|
234
|
+
fromIntConstName: string;
|
|
235
|
+
/** Physical column type for `dataType()` — always integer for an int-backed enum. */
|
|
236
|
+
dataType: string;
|
|
237
|
+
/** Member symbols, in `@values` order (the TS union and the map key order). */
|
|
238
|
+
members: string[];
|
|
239
|
+
/** Member symbol -> stored integer. */
|
|
240
|
+
intByMember: Record<string, number>;
|
|
241
|
+
}
|
|
242
|
+
|
|
215
243
|
export interface ColumnSpec {
|
|
216
244
|
/** Drizzle function name, e.g., "text", "integer", "varchar". */
|
|
217
245
|
fnName: string;
|
|
246
|
+
/**
|
|
247
|
+
* When set, `fnName` names a LOCAL generated const (this spec's customType
|
|
248
|
+
* helper) rather than a Drizzle export — the renderer must NOT `imp()` it.
|
|
249
|
+
*/
|
|
250
|
+
enumIntCustomType?: EnumIntCustomType;
|
|
218
251
|
/** DB column name (snake_case from field name, or @column override). */
|
|
219
252
|
dbName: string;
|
|
220
253
|
/** Positional args after dbName (currently always empty; reserved). */
|
|
@@ -342,6 +375,48 @@ function objectRefBaseName(field: MetaField): string | undefined {
|
|
|
342
375
|
return undefined;
|
|
343
376
|
}
|
|
344
377
|
|
|
378
|
+
/** SCREAMING_SNAKE_CASE for a generated map const name. */
|
|
379
|
+
function screamingSnake(s: string): string {
|
|
380
|
+
return s
|
|
381
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
382
|
+
.replace(/[^A-Za-z0-9]+/g, "_")
|
|
383
|
+
.toUpperCase();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Build the customType descriptor for an int-backed `field.enum`, or undefined
|
|
388
|
+
* when `@values` is missing (the field then degrades to a plain integer column
|
|
389
|
+
* rather than emitting a codec over an unknown member set).
|
|
390
|
+
*
|
|
391
|
+
* Names are derived from the FIELD name, so the consts are per-entity-file and
|
|
392
|
+
* self-contained. A shared enum consumed by N entities therefore emits N small
|
|
393
|
+
* identical helpers rather than requiring a cross-module import — the same
|
|
394
|
+
* self-contained tradeoff the per-entity enum union already makes.
|
|
395
|
+
*/
|
|
396
|
+
function buildEnumIntCustomType(
|
|
397
|
+
field: MetaField,
|
|
398
|
+
intByMember: Record<string, number>,
|
|
399
|
+
): EnumIntCustomType | undefined {
|
|
400
|
+
const members = enumValues(field);
|
|
401
|
+
if (members === undefined || members.length === 0) return undefined;
|
|
402
|
+
// Every member must map — the loader pins key-set-equals-@values (Check 5b), so a
|
|
403
|
+
// miss is unreachable; throwing beats emitting a codec with a hole in it.
|
|
404
|
+
for (const m of members) {
|
|
405
|
+
intValueForMember(intByMember, m, `customType codec for field '${field.name}'`);
|
|
406
|
+
}
|
|
407
|
+
const base = field.name.replace(/[^A-Za-z0-9]/g, "");
|
|
408
|
+
const camel = base.charAt(0).toLowerCase() + base.slice(1);
|
|
409
|
+
const screaming = screamingSnake(base);
|
|
410
|
+
return {
|
|
411
|
+
fnConstName: `${camel}IntEnum`,
|
|
412
|
+
toIntConstName: `${screaming}_TO_INT`,
|
|
413
|
+
fromIntConstName: `${screaming}_FROM_INT`,
|
|
414
|
+
dataType: "integer",
|
|
415
|
+
members,
|
|
416
|
+
intByMember,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
345
420
|
export function mapColumnType(
|
|
346
421
|
field: MetaField,
|
|
347
422
|
dialect: Dialect,
|
|
@@ -355,6 +430,8 @@ export function mapColumnType(
|
|
|
355
430
|
|
|
356
431
|
let fnName: string;
|
|
357
432
|
let fnOptions: Record<string, unknown> | undefined;
|
|
433
|
+
// Set only for an int-backed field.enum — see EnumIntCustomType.
|
|
434
|
+
let enumIntCustomType: EnumIntCustomType | undefined;
|
|
358
435
|
|
|
359
436
|
let leadingComment: string | undefined;
|
|
360
437
|
if (dialect === "sqlite") {
|
|
@@ -405,8 +482,20 @@ export function mapColumnType(
|
|
|
405
482
|
// "string" by the time it reaches here for this dialect.
|
|
406
483
|
fnName = "text";
|
|
407
484
|
break;
|
|
408
|
-
case FIELD_SUBTYPE_STRING:
|
|
409
485
|
case FIELD_SUBTYPE_ENUM:
|
|
486
|
+
// An INT-BACKED enum stores the mapped integer on SQLite too — SQLite has
|
|
487
|
+
// one integer storage class, so this matches migrate-ts's integer{32}.
|
|
488
|
+
{
|
|
489
|
+
const im = intValueMapOf(field);
|
|
490
|
+
if (im !== undefined) {
|
|
491
|
+
enumIntCustomType = buildEnumIntCustomType(field, im);
|
|
492
|
+
fnName = enumIntCustomType?.fnConstName ?? "integer";
|
|
493
|
+
} else {
|
|
494
|
+
fnName = "text";
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
break;
|
|
498
|
+
case FIELD_SUBTYPE_STRING:
|
|
410
499
|
case FIELD_SUBTYPE_UUID:
|
|
411
500
|
case FIELD_SUBTYPE_URI:
|
|
412
501
|
case FIELD_SUBTYPE_INET:
|
|
@@ -524,6 +613,22 @@ export function mapColumnType(
|
|
|
524
613
|
fnName = "jsonb";
|
|
525
614
|
break;
|
|
526
615
|
case FIELD_SUBTYPE_ENUM:
|
|
616
|
+
// An INT-BACKED enum (@intValueMap, design D5) stores the mapped integer,
|
|
617
|
+
// so the Drizzle column is integer — matching migrate-ts's expected-schema.
|
|
618
|
+
// The TS-facing type stays the member-string union; the symbol<->int
|
|
619
|
+
// translation happens at the write/read boundary. Scalar only: D7 makes
|
|
620
|
+
// @intValueMap + isArray ERR_ENUM_INT_VALUE_MAP_ARRAY at load, so an array
|
|
621
|
+
// enum reaching here is always string-backed.
|
|
622
|
+
{
|
|
623
|
+
const im = intValueMapOf(field);
|
|
624
|
+
if (im !== undefined) {
|
|
625
|
+
enumIntCustomType = buildEnumIntCustomType(field, im);
|
|
626
|
+
fnName = enumIntCustomType?.fnConstName ?? "integer";
|
|
627
|
+
} else {
|
|
628
|
+
fnName = "text";
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
break;
|
|
527
632
|
default:
|
|
528
633
|
fnName = "text";
|
|
529
634
|
break;
|
|
@@ -669,6 +774,7 @@ export function mapColumnType(
|
|
|
669
774
|
};
|
|
670
775
|
if (fnOptions !== undefined) result.fnOptions = fnOptions;
|
|
671
776
|
if (defaultExpr !== undefined) result.defaultExpr = defaultExpr;
|
|
777
|
+
if (enumIntCustomType !== undefined) result.enumIntCustomType = enumIntCustomType;
|
|
672
778
|
if (dollarTypeRef !== undefined) result.dollarTypeRef = dollarTypeRef;
|
|
673
779
|
if (leadingComment !== undefined) result.leadingComment = leadingComment;
|
|
674
780
|
|
|
@@ -676,12 +782,22 @@ export function mapColumnType(
|
|
|
676
782
|
if (subType === FIELD_SUBTYPE_ENUM && !isArray) {
|
|
677
783
|
const values = enumValues(field);
|
|
678
784
|
if (values !== undefined && values.length > 0) {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
785
|
+
const intMap = intValueMapOf(field);
|
|
786
|
+
let list: string;
|
|
787
|
+
if (intMap !== undefined) {
|
|
788
|
+
// Int-backed: the column holds integers, so the CHECK lists them unquoted.
|
|
789
|
+
// Keyed BY MEMBER through the map (not Object.values) so the constraint can
|
|
790
|
+
// never disagree with @values, which stays the SSOT. Must match
|
|
791
|
+
// migrate-ts's buildChecks exactly or `meta verify` reports permanent drift.
|
|
792
|
+
list = values
|
|
793
|
+
.map((v) => String(intValueForMember(intMap, v, `CHECK for column '${dbName}'`)))
|
|
794
|
+
.join(", ");
|
|
795
|
+
} else {
|
|
796
|
+
// Single-quote escaping is belt-and-suspenders: the loader's
|
|
797
|
+
// ENUM_MEMBER_PATTERN already rejects quote-bearing members (members are
|
|
798
|
+
// validated to be identifier-safe), so this never fires in practice.
|
|
799
|
+
list = values.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ");
|
|
800
|
+
}
|
|
685
801
|
result.checkConstraint = `${dbName} IN (${list})`;
|
|
686
802
|
}
|
|
687
803
|
}
|
package/src/enum-meta.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// extraction and the z.enum([...]) expression are derived in exactly one place.
|
|
4
4
|
|
|
5
5
|
import type { MetaField } from "@metaobjectsdev/metadata";
|
|
6
|
-
import { FIELD_ATTR_VALUES } from "@metaobjectsdev/metadata";
|
|
6
|
+
import { FIELD_ATTR_VALUES, FIELD_ATTR_INT_VALUE_MAP } from "@metaobjectsdev/metadata";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Effective enum member values (`@values`) for a field, as strings.
|
|
@@ -20,6 +20,42 @@ export function enumValues(field: MetaField): string[] | undefined {
|
|
|
20
20
|
return values.map((v) => String(v));
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* The effective `@intValueMap` (member symbol → integer) for an int-backed enum,
|
|
25
|
+
* or undefined when the enum is string-backed. Its PRESENCE is the whole trigger
|
|
26
|
+
* for integer persistence (design D5) — there is no separate flag or config.
|
|
27
|
+
*
|
|
28
|
+
* ADR-0039: RESOLVING (`attr`, not `ownAttr`), and this is load-bearing rather
|
|
29
|
+
* than incidental. Post-#246 an own `@intValueMap` declared against a shared
|
|
30
|
+
* (root-level abstract) enum is `ERR_ENUM_EXTENDS_VALUES_CONFLICT`, so the map
|
|
31
|
+
* lives on the SHARED DECLARATION and every consuming field INHERITS it. An
|
|
32
|
+
* own-only read would therefore see undefined on exactly the shape adopters are
|
|
33
|
+
* steered toward, and silently emit a string codec into an integer column.
|
|
34
|
+
*/
|
|
35
|
+
export function intValueMapOf(field: MetaField): Record<string, number> | undefined {
|
|
36
|
+
const raw = field.attr(FIELD_ATTR_INT_VALUE_MAP);
|
|
37
|
+
if (raw === undefined || raw === null || typeof raw !== "object") return undefined;
|
|
38
|
+
return raw as Record<string, number>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The integer a member symbol persists as, for an int-backed enum. Throws when the
|
|
43
|
+
* member has no mapping — the loader pins key-set-equals-`@values` (Check 5b) in
|
|
44
|
+
* every port, so a miss is unreachable and must not be papered over: emitting the
|
|
45
|
+
* symbol instead would fail only at INSERT time, against a live database.
|
|
46
|
+
*/
|
|
47
|
+
export function intValueForMember(
|
|
48
|
+
intMap: Record<string, number>,
|
|
49
|
+
member: string,
|
|
50
|
+
context: string,
|
|
51
|
+
): number {
|
|
52
|
+
const n = intMap[member];
|
|
53
|
+
if (typeof n !== "number") {
|
|
54
|
+
throw new Error(`@intValueMap has no integer for member '${member}' (${context}).`);
|
|
55
|
+
}
|
|
56
|
+
return n;
|
|
57
|
+
}
|
|
58
|
+
|
|
23
59
|
/** Build the Zod expression for a set of enum members, e.g. `z.enum(["A", "B"])`. */
|
|
24
60
|
export function zodEnumExpr(values: string[]): string {
|
|
25
61
|
return `z.enum([${values.map((v) => JSON.stringify(v)).join(", ")}])`;
|
package/src/generator.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { MetaObject, MetaRoot } from "@metaobjectsdev/metadata";
|
|
2
2
|
import type { RenderContext } from "./render-context.js";
|
|
3
3
|
import type { ResolvedGenConfig } from "./metaobjects-config.js";
|
|
4
|
+
import type { OrphanPolicy } from "./reconcile-orphans.js";
|
|
4
5
|
import { effectivePackage } from "./docs-paths.js";
|
|
5
6
|
|
|
6
7
|
export interface EmittedFile {
|
|
@@ -48,6 +49,14 @@ export interface Generator {
|
|
|
48
49
|
/** Marks the generator that produces entity modules — the runner uses its
|
|
49
50
|
* target as the entity-module target for cross-target import resolution. */
|
|
50
51
|
emitsEntityModule?: boolean;
|
|
52
|
+
/** FR-038 §8 — OPT IN to orphan reconciliation by declaring the output
|
|
53
|
+
* namespace this generator exclusively owns. When set, the runner removes
|
|
54
|
+
* files inside that namespace which a previous run generated and this run did
|
|
55
|
+
* not, REFUSING any that have been edited by hand. Absent (the default for
|
|
56
|
+
* every existing generator) means the runner never deletes anything, which is
|
|
57
|
+
* why this is additive: output stops being generated and the stale file simply
|
|
58
|
+
* stays, exactly as before. */
|
|
59
|
+
orphanPolicy?: OrphanPolicy;
|
|
51
60
|
/** Marks the OPT-IN Hono routes generator (routesFileHono). The runner
|
|
52
61
|
* aggregates this across the active suite into `ctx.config.includeHonoRoutes`,
|
|
53
62
|
* so a generator that documents the API surface (api-docs) can AUTO-DETECT
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// emitter uses) and the optional/nullable rule from `isFieldRequired` (the
|
|
10
10
|
// SAME rule the docs Storage/Constraints nullable column uses).
|
|
11
11
|
// • create-payload → `insertSchemaFields` (the EXACT field set + optionality
|
|
12
|
-
// the zod InsertSchema emitter walks: auto-gen PK omitted, @readOnly omitted,
|
|
12
|
+
// the zod InsertSchema emitter walks: auto-gen PK omitted, @mutability:"readOnly" omitted,
|
|
13
13
|
// TPH discriminator pinned, @autoSet optional, else `fieldWillBeOptional`).
|
|
14
14
|
// • update-payload → `updateSchemaFields` (the UpdateSchema walk: TPH
|
|
15
15
|
// discriminator + @autoSet-onCreate omitted, everything else optional).
|
|
@@ -107,6 +107,7 @@ import {
|
|
|
107
107
|
variableNameFromEntity,
|
|
108
108
|
} from "../naming.js";
|
|
109
109
|
import { getPkInfo } from "../templates/queries.js";
|
|
110
|
+
import { responseShape } from "../templates/find-inbound.js";
|
|
110
111
|
import { isTphSubtype } from "../templates/zod-validators.js";
|
|
111
112
|
import { isTphDiscriminatorBase } from "../templates/tph-discriminator.js";
|
|
112
113
|
import { isCallableEntity } from "../templates/callable-file.js";
|
|
@@ -823,50 +824,21 @@ function templateOutputs(root: MetaRoot): MetaData[] {
|
|
|
823
824
|
function buildTemplateUnit(tmpl: MetaData, root: MetaRoot, _layout: OutputLayout): ApiUnitDoc {
|
|
824
825
|
const name = tmpl.name;
|
|
825
826
|
const symbols: ApiSymbol[] = [];
|
|
826
|
-
//
|
|
827
|
-
//
|
|
828
|
-
const extractorMod = templateModulePath(`${name}.extractor`);
|
|
827
|
+
// The render-helper generator emits a FLAT `<Name>.render.ts` (no package
|
|
828
|
+
// folding), so importPath ignores layout.
|
|
829
829
|
const renderMod = templateModulePath(`${name}.render`);
|
|
830
830
|
|
|
831
|
-
// ADR-0039: resolving — a template may inherit @payloadRef/@
|
|
831
|
+
// ADR-0039: resolving — a template may inherit @payloadRef/@kind via extends.
|
|
832
832
|
const payloadRef = tmpl.attr(TEMPLATE_ATTR_PAYLOAD_REF);
|
|
833
833
|
const payload = typeof payloadRef === "string" ? payloadRef : undefined;
|
|
834
|
-
const format = ((tmpl.attr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text").toLowerCase();
|
|
835
834
|
const kind = ((tmpl.attr(TEMPLATE_ATTR_KIND) as string | undefined) ?? TEMPLATE_KIND_DEFAULT).toLowerCase();
|
|
836
835
|
|
|
837
|
-
//
|
|
838
|
-
//
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
// document its field shape (same VO field walk the payload interface emitter
|
|
844
|
-
// uses) so an agent sees what `extract<Name>` yields, not just the type name.
|
|
845
|
-
const payloadShape = payloadFieldShapes(root, payload);
|
|
846
|
-
const extractSym: ApiSymbol = {
|
|
847
|
-
name: extract,
|
|
848
|
-
kind: "extractor",
|
|
849
|
-
importPath: extractorMod,
|
|
850
|
-
signature: `${extract}(root: MetaRoot, text: string): ${payload}`,
|
|
851
|
-
params: [`root: MetaRoot`, `text: string`],
|
|
852
|
-
returns: payload,
|
|
853
|
-
throws: `Error when a @required field is lost (the strict opt-in gate).`,
|
|
854
|
-
usage: `Parse dirty LLM ${format} text into a strict, fully-typed ${payload} graph.`,
|
|
855
|
-
};
|
|
856
|
-
if (payloadShape !== undefined) extractSym.fields = payloadShape;
|
|
857
|
-
symbols.push(
|
|
858
|
-
extractSym,
|
|
859
|
-
{
|
|
860
|
-
name: extractLenient,
|
|
861
|
-
kind: "extractor",
|
|
862
|
-
importPath: extractorMod,
|
|
863
|
-
signature: `${extractLenient}(root: MetaRoot, text: string): ExtractionResult<${name}Extracted>`,
|
|
864
|
-
params: [`root: MetaRoot`, `text: string`],
|
|
865
|
-
returns: `ExtractionResult<${name}Extracted>`,
|
|
866
|
-
usage: `Never-throwing extract; inspect report for lost/defaulted fields.`,
|
|
867
|
-
},
|
|
868
|
-
);
|
|
869
|
-
}
|
|
836
|
+
// ADR-0052: a template.output documents its RENDER and nothing else. The
|
|
837
|
+
// extractor symbols used to be built here, gated on @format ∈ {json,xml} and
|
|
838
|
+
// typed on @payloadRef — so the reference documented functions this subtype no
|
|
839
|
+
// longer emits, named the REQUEST shape as the parse result, and said nothing
|
|
840
|
+
// about the prompts that actually do emit them. They now live in
|
|
841
|
+
// buildPromptUnit, keyed on @responseRef.
|
|
870
842
|
|
|
871
843
|
// --- render: render<Name>; document → string, email → EmailDocument
|
|
872
844
|
// (matches render-helper.ts's @kind branch). Render is emitted for any
|
|
@@ -875,7 +847,7 @@ function buildTemplateUnit(tmpl: MetaData, root: MetaRoot, _layout: OutputLayout
|
|
|
875
847
|
const render = `render${name}`;
|
|
876
848
|
const isEmail = kind === TEMPLATE_KIND_EMAIL;
|
|
877
849
|
const returns = isEmail ? "EmailDocument" : "string";
|
|
878
|
-
|
|
850
|
+
const renderSym: ApiSymbol = {
|
|
879
851
|
name: render,
|
|
880
852
|
kind: "render",
|
|
881
853
|
importPath: renderMod,
|
|
@@ -885,7 +857,14 @@ function buildTemplateUnit(tmpl: MetaData, root: MetaRoot, _layout: OutputLayout
|
|
|
885
857
|
usage: isEmail
|
|
886
858
|
? `Render the ${name} email (subject + bodies) from a typed ${payload} payload.`
|
|
887
859
|
: `Render the ${name} document from a typed ${payload} payload.`,
|
|
888
|
-
}
|
|
860
|
+
};
|
|
861
|
+
// The payload shape used to reach this unit only via the extractor symbol.
|
|
862
|
+
// ADR-0052 moved that away, which left the render example with no field shape
|
|
863
|
+
// to build from — it degraded to `render<Name>({}, provider)`. Attach it here,
|
|
864
|
+
// where it always belonged: this is the payload the render handle takes.
|
|
865
|
+
const renderShape = payloadFieldShapes(root, payload);
|
|
866
|
+
if (renderShape !== undefined) renderSym.fields = renderShape;
|
|
867
|
+
symbols.push(renderSym);
|
|
889
868
|
}
|
|
890
869
|
|
|
891
870
|
const unit: ApiUnitDoc = {
|
|
@@ -950,12 +929,57 @@ function buildPromptUnit(tmpl: MetaData, root: MetaRoot): ApiUnitDoc {
|
|
|
950
929
|
symbols.push(sym);
|
|
951
930
|
}
|
|
952
931
|
|
|
953
|
-
|
|
932
|
+
// --- ADR-0052 inbound half: a prompt declaring @responseRef also owns the
|
|
933
|
+
// parser-on-receipt and the tolerant extract. Gated on @responseRef
|
|
934
|
+
// PRESENCE, matching extractor-file.ts / output-parser-file.ts — never on
|
|
935
|
+
// a format value, and never on @payloadRef, which types the REQUEST. ---
|
|
936
|
+
const shape = responseShape(root, tmpl);
|
|
937
|
+
if (shape) {
|
|
938
|
+
const { vo, ref: responseRef, format } = shape;
|
|
939
|
+
// The extractor generator emits a FLAT `<Name>.extractor.ts`.
|
|
940
|
+
const extractorMod = templateModulePath(`${name}.extractor`);
|
|
941
|
+
const extract = `extract${name}`;
|
|
942
|
+
const extractLenient = `extractLenient${name}`;
|
|
943
|
+
// The strict return IS the @responseRef value-object's interface — document
|
|
944
|
+
// its field shape so an agent sees what `extract<Name>` yields, not just a
|
|
945
|
+
// type name. `vo` is the resolved node; `responseRef` is the authored ref.
|
|
946
|
+
void vo;
|
|
947
|
+
const responseFieldShape = payloadFieldShapes(root, responseRef);
|
|
948
|
+
const extractSym: ApiSymbol = {
|
|
949
|
+
name: extract,
|
|
950
|
+
kind: "extractor",
|
|
951
|
+
importPath: extractorMod,
|
|
952
|
+
signature: `${extract}(root: MetaRoot, text: string): ${responseRef}`,
|
|
953
|
+
params: [`root: MetaRoot`, `text: string`],
|
|
954
|
+
returns: responseRef,
|
|
955
|
+
throws: `Error when a @required field is lost (the strict opt-in gate).`,
|
|
956
|
+
usage: `Parse the model's ${format} reply to ${name} into a strict, fully-typed ${responseRef} graph.`,
|
|
957
|
+
};
|
|
958
|
+
if (responseFieldShape !== undefined) extractSym.fields = responseFieldShape;
|
|
959
|
+
symbols.push(extractSym, {
|
|
960
|
+
name: extractLenient,
|
|
961
|
+
kind: "extractor",
|
|
962
|
+
importPath: extractorMod,
|
|
963
|
+
signature: `${extractLenient}(root: MetaRoot, text: string): ExtractionResult<${name}Extracted>`,
|
|
964
|
+
params: [`root: MetaRoot`, `text: string`],
|
|
965
|
+
returns: `ExtractionResult<${name}Extracted>`,
|
|
966
|
+
usage: `Never-throwing extract of the ${name} reply; inspect report for lost/defaulted fields.`,
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const unit: ApiUnitDoc = {
|
|
954
971
|
node: name,
|
|
955
972
|
package: effectivePackage(tmpl),
|
|
956
973
|
nodeKind: "template",
|
|
957
974
|
symbols,
|
|
958
975
|
};
|
|
976
|
+
// ADR-0052: the extractor symbols moved here, so their worked example must move
|
|
977
|
+
// with them. Without this the page documents `extract<Name>(root, …)` but the
|
|
978
|
+
// setup preamble never introduces `root`, because the preamble derives its
|
|
979
|
+
// handles from the rendered EXAMPLE text.
|
|
980
|
+
const example = templateExample(name, symbols);
|
|
981
|
+
if (example !== undefined) unit.example = example;
|
|
982
|
+
return unit;
|
|
959
983
|
}
|
|
960
984
|
|
|
961
985
|
// ---------------------------------------------------------------------------
|
|
@@ -1069,9 +1093,10 @@ function templateExample(name: string, symbols: ApiSymbol[]): UnitExample | unde
|
|
|
1069
1093
|
if (renderSym !== undefined) {
|
|
1070
1094
|
picks.push({ name: renderSym.name, importPath: renderSym.importPath });
|
|
1071
1095
|
// Render's payload object literal comes from the @payloadRef VO shape the
|
|
1072
|
-
// render symbol
|
|
1073
|
-
//
|
|
1074
|
-
|
|
1096
|
+
// render symbol consumes. Prefer the render symbol's OWN fields — it borrowed
|
|
1097
|
+
// the extractor's before ADR-0052, which silently degraded the example to
|
|
1098
|
+
// `render<Name>({}, provider)` the moment the extractor moved to the prompt.
|
|
1099
|
+
const payloadFields = renderSym.fields ?? extract?.fields;
|
|
1075
1100
|
const payloadLit = objectLiteralFromFields(payloadFields);
|
|
1076
1101
|
body.push(`const output = ${renderSym.name}(${payloadLit}, provider);`);
|
|
1077
1102
|
}
|
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
type MetaRoot,
|
|
12
12
|
TYPE_TEMPLATE,
|
|
13
13
|
TEMPLATE_ATTR_PAYLOAD_REF,
|
|
14
|
+
REQUIREMENT_ATTR_STATEMENT,
|
|
14
15
|
OBJECT_SUBTYPE_VALUE,
|
|
16
|
+
OBJECT_SUBTYPE_ENTITY,
|
|
15
17
|
IDENTITY_SUBTYPE_PRIMARY,
|
|
16
18
|
IDENTITY_SUBTYPE_SECONDARY,
|
|
17
19
|
IDENTITY_SUBTYPE_REFERENCE,
|
|
@@ -52,6 +54,9 @@ import { enumValues } from "../enum-meta.js";
|
|
|
52
54
|
import { hasWritableRdbSource } from "../source-detect.js";
|
|
53
55
|
import { GENERATED_HEADER } from "../constants.js";
|
|
54
56
|
import { renderEntityNeighborhoodErBlock } from "../templates/mermaid-er.js";
|
|
57
|
+
// Shape C reuses the SAME walk the stub generator and the requirements index use, so
|
|
58
|
+
// all three agree about what the ledger contains by construction.
|
|
59
|
+
import { walkRequirements } from "../requirement-walk.js";
|
|
55
60
|
import type {
|
|
56
61
|
EntityDocData,
|
|
57
62
|
StorageFieldDoc,
|
|
@@ -684,6 +689,34 @@ export function buildEntityDocData(
|
|
|
684
689
|
}
|
|
685
690
|
const usedBy = usedByMatches.length > 0 ? usedByMatches : undefined;
|
|
686
691
|
|
|
692
|
+
// ---- ClaimedBy (shape C) — the requirements whose `@implementedBy` resolves HERE.
|
|
693
|
+
//
|
|
694
|
+
// Matched on the RESOLVED NODE, never on the reference string: `@implementedBy` may
|
|
695
|
+
// be authored fully-qualified or bare, and a bare ref binds package-locally under
|
|
696
|
+
// ADR-0042. `walkRequirements` already did that resolution (it is what the stub
|
|
697
|
+
// generator uses), so comparing nodes inherits the correct binding rather than
|
|
698
|
+
// re-deriving it here and drifting from it.
|
|
699
|
+
//
|
|
700
|
+
// ENTITY-GRAIN. Object coverage is entity-grain, so a claimed value/projection gets
|
|
701
|
+
// nothing — surfacing one would imply a coverage rule the ledger does not have.
|
|
702
|
+
const claimedByMatches: UsedByDoc[] = [];
|
|
703
|
+
if (entity.subType === OBJECT_SUBTYPE_ENTITY) {
|
|
704
|
+
for (const walked of walkRequirements(root)) {
|
|
705
|
+
if (!walked.targets.some((t) => t.node === entity)) continue;
|
|
706
|
+
const v = walked.view;
|
|
707
|
+
const level = v.level === undefined ? "" : ` · **L${v.level}**`;
|
|
708
|
+
const status = v.status === undefined ? "" : ` · status: \`${v.status}\``;
|
|
709
|
+
const statement = walked.node.attr(REQUIREMENT_ATTR_STATEMENT);
|
|
710
|
+
const said = typeof statement === "string" && statement.length > 0 ? ` — ${statement}` : "";
|
|
711
|
+
claimedByMatches.push({
|
|
712
|
+
bullet: `\`requirement.${v.subType} ${v.path}\`${level}${status}${said}`,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
// ABSENT, not empty: the Mustache gate must not render for an unclaimed entity, so
|
|
717
|
+
// its page stays byte-identical to its pre-feature output.
|
|
718
|
+
const claimedBy = claimedByMatches.length > 0 ? claimedByMatches : undefined;
|
|
719
|
+
|
|
687
720
|
// Preamble header — built up exactly as the legacy emitter did.
|
|
688
721
|
const preambleLines: string[] = [];
|
|
689
722
|
const typeStr = `${entity.type}.${entity.subType}`;
|
|
@@ -761,6 +794,10 @@ export function buildEntityDocData(
|
|
|
761
794
|
data.usedBy = usedBy;
|
|
762
795
|
data.hasUsedBy = true;
|
|
763
796
|
}
|
|
797
|
+
if (claimedBy !== undefined) {
|
|
798
|
+
data.claimedBy = claimedBy;
|
|
799
|
+
data.hasClaimedBy = true;
|
|
800
|
+
}
|
|
764
801
|
// Cross-link to the api surfaces — present ONLY when the caller computed the
|
|
765
802
|
// hrefs (api surfaces emitted alongside model); model-only runs stay identical.
|
|
766
803
|
// `last` flags the final ref so the template renders an inline ` · ` separator.
|
|
@@ -256,6 +256,21 @@ export interface EntityDocData {
|
|
|
256
256
|
/** Present-and-non-empty flag for the usedBy section. */
|
|
257
257
|
hasUsedBy?: boolean;
|
|
258
258
|
|
|
259
|
+
/** "Required by" (shape C) — the `requirement.*` nodes whose `@implementedBy`
|
|
260
|
+
* resolves to THIS entity. Same list+flag pattern as `usedBy`.
|
|
261
|
+
*
|
|
262
|
+
* ABSENT — not empty, not false — when nothing claims the entity, so the
|
|
263
|
+
* Mustache section does not render and an unclaimed entity's page stays
|
|
264
|
+
* BYTE-IDENTICAL to its pre-feature output. That is the no-churn contract:
|
|
265
|
+
* a project with no ledger, or an entity nothing claims, must see no diff.
|
|
266
|
+
*
|
|
267
|
+
* ENTITY-GRAIN ONLY. Object coverage is entity-grain (`spec/capability-ledger.md`),
|
|
268
|
+
* so a claimed `object.value` / `object.projection` gets nothing here — surfacing
|
|
269
|
+
* one would imply a coverage rule the ledger does not actually have. */
|
|
270
|
+
claimedBy?: UsedByDoc[];
|
|
271
|
+
/** Present-and-non-empty flag for the claimedBy section. */
|
|
272
|
+
hasClaimedBy?: boolean;
|
|
273
|
+
|
|
259
274
|
/** Present flag for the storage section. */
|
|
260
275
|
hasStorage?: boolean;
|
|
261
276
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// breaks, the refactor is the bug, not the fixture.
|
|
20
20
|
|
|
21
21
|
import type { MetaObject, MetaRoot } from "@metaobjectsdev/metadata";
|
|
22
|
-
import { TYPE_TEMPLATE
|
|
22
|
+
import { TYPE_TEMPLATE } from "@metaobjectsdev/metadata";
|
|
23
23
|
import { render } from "@metaobjectsdev/render";
|
|
24
24
|
import type { Provider } from "@metaobjectsdev/render";
|
|
25
25
|
import type { Generator, GeneratorFactory, EmittedFile } from "../generator.js";
|
|
@@ -132,12 +132,21 @@ export const docsFile = function docsFile(opts?: DocsFileOpts): Generator {
|
|
|
132
132
|
return { path, content: renderDocPage(TEMPLATE_REF, payload, provider, path) };
|
|
133
133
|
});
|
|
134
134
|
|
|
135
|
-
// ALSO emit one NEUTRAL render-contract page per `template
|
|
136
|
-
// a sibling artifact, distinct from the entity page. Raw node name → file
|
|
135
|
+
// ALSO emit one NEUTRAL render-contract page per top-level `template.*` node
|
|
136
|
+
// — a sibling artifact, distinct from the entity page. Raw node name → file
|
|
137
137
|
// (`<name>.md`), agreeing with the entity Used-by back-link target.
|
|
138
|
+
//
|
|
139
|
+
// EVERY template subtype, not just `template.output`: the api-docs surface
|
|
140
|
+
// has always emitted `api/<lang>/<pkg>/<Prompt>.md` for a top-level
|
|
141
|
+
// `template.prompt` (it documents the prompt-render handle), and that page
|
|
142
|
+
// carries a "Model / metadata" back-link to `<pkg>/<Prompt>.md`. While this
|
|
143
|
+
// loop filtered to outputs, that link pointed at a page nothing wrote — a
|
|
144
|
+
// broken link in every shipped doc tree containing a prompt. ADR-0052 makes
|
|
145
|
+
// it worse rather than better: a responding prompt now carries the whole
|
|
146
|
+
// inbound tier, so it is the LAST node a doc tree should omit.
|
|
138
147
|
// ADR-0039: resolving — root has no super (children()==ownChildren()).
|
|
139
148
|
for (const child of ctx.loadedRoot.children()) {
|
|
140
|
-
if (child.type !== TYPE_TEMPLATE
|
|
149
|
+
if (child.type !== TYPE_TEMPLATE) continue;
|
|
141
150
|
const node = docPageNode(child);
|
|
142
151
|
templateNodes.push(node);
|
|
143
152
|
const path = docPageOutputPath(layout, node);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// node whose @format is json/xml. Wraps renderExtractor() from templates/extractor.ts.
|
|
5
5
|
//
|
|
6
6
|
// The emitted extractor sits over the output-parser's nested-capable extract and turns dirty LLM
|
|
7
|
-
// text into the strict typed payload graph. It imports from the sibling <Name>.
|
|
7
|
+
// text into the strict typed payload graph. It imports from the sibling <Name>.response.ts (the
|
|
8
8
|
// output-parser) and from each payload value-object's own entity module (<VO>.ts, emitted by
|
|
9
9
|
// entityFile), so run it alongside outputParser() + entityFile().
|
|
10
10
|
//
|
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
// Custom output directory:
|
|
15
15
|
// generators: [..., extractor({ outDir: "src/generated/outputs" })]
|
|
16
16
|
|
|
17
|
-
import { TYPE_TEMPLATE, TEMPLATE_SUBTYPE_OUTPUT, TEMPLATE_ATTR_FORMAT } from "@metaobjectsdev/metadata";
|
|
18
17
|
import {
|
|
19
18
|
type EmittedFile,
|
|
20
19
|
type Generator,
|
|
21
20
|
type GeneratorFactory,
|
|
22
21
|
oncePerRun,
|
|
23
22
|
} from "../generator.js";
|
|
23
|
+
import { inboundTemplates, responseShape } from "../templates/find-inbound.js";
|
|
24
24
|
import { renderExtractor } from "../templates/extractor.js";
|
|
25
25
|
|
|
26
26
|
export interface ExtractorOpts {
|
|
@@ -35,16 +35,12 @@ export const extractor = function extractor(opts?: ExtractorOpts): Generator {
|
|
|
35
35
|
const generator: Generator = {
|
|
36
36
|
name: "extractor",
|
|
37
37
|
generate: oncePerRun((_entities, ctx) => {
|
|
38
|
-
// ADR-0039: resolving — root has no super (children()==ownChildren()).
|
|
39
|
-
const outputs = ctx.loadedRoot
|
|
40
|
-
.children()
|
|
41
|
-
.filter((c) => c.type === TYPE_TEMPLATE && c.subType === TEMPLATE_SUBTYPE_OUTPUT);
|
|
42
38
|
const files: EmittedFile[] = [];
|
|
43
|
-
for (const t of
|
|
44
|
-
// The extract tier
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
if (
|
|
39
|
+
for (const t of inboundTemplates(ctx.loadedRoot)) {
|
|
40
|
+
// The extract tier sits over the parser's extract API. Since ADR-0052 every
|
|
41
|
+
// responding prompt emits one (@responseFormat is a closed json|xml set), so
|
|
42
|
+
// the only remaining skip is an unresolvable @responseRef.
|
|
43
|
+
if (!responseShape(ctx.loadedRoot, t)) continue;
|
|
48
44
|
files.push({
|
|
49
45
|
path: `${dirPrefix}${t.name}.extractor.ts`,
|
|
50
46
|
// ADR-0044/#228: thread ctx.renderContext (when present — runGen always supplies it;
|
package/src/generators/index.ts
CHANGED
|
@@ -44,3 +44,14 @@ export type {
|
|
|
44
44
|
export { buildEntityDocData } from "./docs-data-builder.js";
|
|
45
45
|
export type { TemplateDocData, TemplateOutputPart } from "./template-doc-data.js";
|
|
46
46
|
export { buildTemplateDocData } from "./template-doc-builder.js";
|
|
47
|
+
|
|
48
|
+
// FR-038 — requirement-derived test stubs. The factory is a convenience; the
|
|
49
|
+
// primitives beside it are the real escape hatch, so an app needing different
|
|
50
|
+
// behaviour composes its own generator instead of filing an issue here.
|
|
51
|
+
export { requirementTests } from "./requirement-tests.js";
|
|
52
|
+
export { requirementsFile } from "./requirements-file.js";
|
|
53
|
+
export type { RequirementRow } from "./requirements-view.js";
|
|
54
|
+
export type {
|
|
55
|
+
RequirementTestsOpts,
|
|
56
|
+
RequirementTestRenderer,
|
|
57
|
+
} from "./requirement-tests.js";
|