@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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// server/typescript/packages/codegen-ts/src/templates/find-inbound.ts
|
|
2
|
+
//
|
|
3
|
+
// The ADR-0052 direction rule, in ONE place.
|
|
4
|
+
//
|
|
5
|
+
// A template subtype's axis is DIRECTION: `template.output` renders outbound (a
|
|
6
|
+
// document or an email) and generates no parser; the inbound half — the response
|
|
7
|
+
// shape, the FR-010 response-format fragment, and the parser-on-receipt — belongs
|
|
8
|
+
// to a `template.prompt` that declares `@responseRef`.
|
|
9
|
+
//
|
|
10
|
+
// Every inbound generator calls through here rather than re-deriving "which
|
|
11
|
+
// templates have a response". Three call sites each deciding for themselves is
|
|
12
|
+
// exactly how the pre-ADR-0052 tier drifted: the parser had no `@kind` filter at
|
|
13
|
+
// all, so an email template generated a parser for text the system had just
|
|
14
|
+
// rendered, while the extractor and the fragment emitter each applied a different
|
|
15
|
+
// json/xml gate.
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
type MetaData,
|
|
19
|
+
TYPE_TEMPLATE,
|
|
20
|
+
TEMPLATE_SUBTYPE_PROMPT,
|
|
21
|
+
TEMPLATE_ATTR_RESPONSE_REF,
|
|
22
|
+
TEMPLATE_ATTR_RESPONSE_FORMAT,
|
|
23
|
+
RESPONSE_FORMAT_DEFAULT,
|
|
24
|
+
RESPONSE_FORMAT_XML,
|
|
25
|
+
type ResponseFormat,
|
|
26
|
+
resolveObjectRef,
|
|
27
|
+
} from "@metaobjectsdev/metadata";
|
|
28
|
+
|
|
29
|
+
/** What an inbound generator needs about one responding prompt. */
|
|
30
|
+
export interface InboundShape {
|
|
31
|
+
/** The resolved response value-object — the shape a reply is parsed INTO. */
|
|
32
|
+
readonly vo: MetaData;
|
|
33
|
+
/** The `@responseRef` string as authored (bare or fully-qualified). */
|
|
34
|
+
readonly ref: string;
|
|
35
|
+
/**
|
|
36
|
+
* The syntax of the REPLY (ADR-0053) — never the template's `@format`, which is
|
|
37
|
+
* the syntax of the rendered prompt BODY. The two genuinely differ.
|
|
38
|
+
*/
|
|
39
|
+
readonly format: ResponseFormat;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Every `template.prompt` that declares a response shape, in declaration order.
|
|
44
|
+
*
|
|
45
|
+
* The gate is `@responseRef` PRESENCE, not a format value: declaring a response
|
|
46
|
+
* shape is the request for a parser. Gating on `@format` was what let a `text`
|
|
47
|
+
* template get a strict parser but no tolerant extract, and — because `@format`
|
|
48
|
+
* defaults to `text` — would silently emit nothing at all after the re-homing.
|
|
49
|
+
*
|
|
50
|
+
* ADR-0039: resolving accessors throughout. A root has no super, but a template
|
|
51
|
+
* may inherit `@responseRef` from an abstract base via `extends`, and three
|
|
52
|
+
* shipped fixtures rely on exactly that.
|
|
53
|
+
*/
|
|
54
|
+
export function inboundTemplates(root: MetaData): MetaData[] {
|
|
55
|
+
return root
|
|
56
|
+
.children()
|
|
57
|
+
.filter(
|
|
58
|
+
(c) =>
|
|
59
|
+
c.type === TYPE_TEMPLATE &&
|
|
60
|
+
c.subType === TEMPLATE_SUBTYPE_PROMPT &&
|
|
61
|
+
typeof c.attr(TEMPLATE_ATTR_RESPONSE_REF) === "string",
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve a prompt's response value-object and reply syntax.
|
|
67
|
+
*
|
|
68
|
+
* Returns `undefined` when the template declares no `@responseRef` or the ref does
|
|
69
|
+
* not resolve — callers skip rather than throw, matching the pre-ADR-0052 contract
|
|
70
|
+
* for an unresolvable payload ref.
|
|
71
|
+
*/
|
|
72
|
+
export function responseShape(root: MetaData, tmpl: MetaData): InboundShape | undefined {
|
|
73
|
+
// ADR-0039: resolving — @responseRef may be inherited via extends.
|
|
74
|
+
const ref = tmpl.attr(TEMPLATE_ATTR_RESPONSE_REF);
|
|
75
|
+
if (typeof ref !== "string") return undefined;
|
|
76
|
+
// ADR-0042: a bare @responseRef resolves package-locally, then root-level.
|
|
77
|
+
const vo = resolveObjectRef(root, ref, tmpl.package ?? tmpl.fileDefaultPackage ?? "").node;
|
|
78
|
+
if (!vo) return undefined;
|
|
79
|
+
return { vo, ref, format: responseFormatOf(tmpl) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The declared reply syntax, defaulted per ADR-0053.
|
|
84
|
+
*
|
|
85
|
+
* The default is `json` because that reproduces the trace helper's pre-ADR-0053
|
|
86
|
+
* fallback exactly (anything that was not `"xml"` was treated as JSON), which is
|
|
87
|
+
* what makes the attribute's introduction behaviour-preserving rather than a new
|
|
88
|
+
* policy.
|
|
89
|
+
*/
|
|
90
|
+
export function responseFormatOf(tmpl: MetaData): ResponseFormat {
|
|
91
|
+
// ADR-0039: resolving — @responseFormat may be inherited via extends.
|
|
92
|
+
const raw = tmpl.attr(TEMPLATE_ATTR_RESPONSE_FORMAT);
|
|
93
|
+
return typeof raw === "string" && raw.toLowerCase() === RESPONSE_FORMAT_XML
|
|
94
|
+
? RESPONSE_FORMAT_XML
|
|
95
|
+
: RESPONSE_FORMAT_DEFAULT;
|
|
96
|
+
}
|
|
@@ -14,11 +14,12 @@ import {
|
|
|
14
14
|
FIELD_ATTR_EXAMPLE,
|
|
15
15
|
FIELD_ATTR_INSTRUCTION,
|
|
16
16
|
FIELD_ATTR_ENUM_DOC,
|
|
17
|
-
TEMPLATE_ATTR_FORMAT,
|
|
18
17
|
TEMPLATE_ATTR_PROMPT_STYLE,
|
|
19
18
|
PROMPT_STYLE_INLINE,
|
|
20
19
|
PROMPT_STYLE_EXAMPLE_ONLY,
|
|
20
|
+
RESPONSE_FORMAT_XML,
|
|
21
21
|
} from "@metaobjectsdev/metadata";
|
|
22
|
+
import { responseFormatOf } from "./find-inbound.js";
|
|
22
23
|
import {
|
|
23
24
|
fields,
|
|
24
25
|
isRequired,
|
|
@@ -42,9 +43,11 @@ export function specLiteral(vo: MetaData, template: MetaData, rootName: string):
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
function resolveFormat(template: MetaData): string {
|
|
45
|
-
// ADR-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
// ADR-0053: the fragment describes the REPLY, so its syntax is @responseFormat —
|
|
47
|
+
// NOT @format, which is the syntax of the rendered prompt BODY. Reading @format
|
|
48
|
+
// here typed the instruction "produce your answer like this" off the format of
|
|
49
|
+
// the question.
|
|
50
|
+
return responseFormatOf(template) === RESPONSE_FORMAT_XML ? "Format.XML" : "Format.JSON";
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
function resolvePromptStyle(template: MetaData): string {
|
|
@@ -13,13 +13,15 @@ import {
|
|
|
13
13
|
type MetaData,
|
|
14
14
|
TYPE_FIELD,
|
|
15
15
|
TYPE_TEMPLATE,
|
|
16
|
-
|
|
16
|
+
TEMPLATE_SUBTYPE_PROMPT,
|
|
17
17
|
FIELD_SUBTYPE_OBJECT,
|
|
18
18
|
FIELD_ATTR_OBJECT_REF,
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
TEMPLATE_ATTR_RESPONSE_REF,
|
|
20
|
+
RESPONSE_FORMAT_XML,
|
|
21
21
|
resolveObjectRef,
|
|
22
22
|
} from "@metaobjectsdev/metadata";
|
|
23
|
+
import { responseShape } from "./find-inbound.js";
|
|
24
|
+
import { isRequired } from "./fr010-field-mapping.js";
|
|
23
25
|
import {
|
|
24
26
|
nestedMirrorInterfaces,
|
|
25
27
|
nestedMappers,
|
|
@@ -53,7 +55,19 @@ function findTemplate(root: MetaData, name: string): MetaData | undefined {
|
|
|
53
55
|
return root.children().find((c) => c.type === TYPE_TEMPLATE && c.name === name);
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
/** Render the Zod expression for a single field; recurses on @objectRef.
|
|
58
|
+
/** Render the Zod expression for a single field; recurses on @objectRef.
|
|
59
|
+
*
|
|
60
|
+
* Optionality comes from `@required`, via the SAME `isRequired` predicate the
|
|
61
|
+
* tolerant tier uses — so the two tiers in this file cannot disagree about what
|
|
62
|
+
* the contract is. They used to: this schema emitted every field as mandatory
|
|
63
|
+
* and never read `@required`, so `parse<Name>` threw on a reply that correctly
|
|
64
|
+
* omitted a declared-optional field, while the tolerant extract accepted it.
|
|
65
|
+
*
|
|
66
|
+
* Every other port reuses the payload VO, which #309 made `@required`-correct.
|
|
67
|
+
* TypeScript re-derives its schema inline here, which is how it drifted — the
|
|
68
|
+
* same "payload tier disagrees with the rest of the toolchain" shape ADR-0052
|
|
69
|
+
* came out of.
|
|
70
|
+
*/
|
|
57
71
|
function fieldZod(field: MetaData, root: MetaData, seen: ReadonlySet<string>, depth: number): string {
|
|
58
72
|
// isArray is a native (reserved) property on MetaData, not an attr.
|
|
59
73
|
const isArray = field.resolvedIsArray();
|
|
@@ -73,7 +87,10 @@ function fieldZod(field: MetaData, root: MetaData, seen: ReadonlySet<string>, de
|
|
|
73
87
|
} else {
|
|
74
88
|
base = SCALAR_ZOD[field.subType] ?? "z.unknown()";
|
|
75
89
|
}
|
|
76
|
-
|
|
90
|
+
const shaped = isArray ? `z.array(${base})` : base;
|
|
91
|
+
// `.optional()` wraps the ARRAY, not its element: an absent list and a list of
|
|
92
|
+
// absent things are different claims.
|
|
93
|
+
return isRequired(field) ? shaped : `${shaped}.optional()`;
|
|
77
94
|
}
|
|
78
95
|
|
|
79
96
|
/** Render a `z.object({ ... })` for an object.value node.
|
|
@@ -90,28 +107,34 @@ function renderObjectSchema(vo: MetaData, root: MetaData, seen: ReadonlySet<stri
|
|
|
90
107
|
}
|
|
91
108
|
|
|
92
109
|
/**
|
|
93
|
-
* Render the full
|
|
94
|
-
* Throws if the template isn't found, isn't a template.
|
|
95
|
-
* @
|
|
110
|
+
* Render the full response-parser file for one responding `template.prompt`.
|
|
111
|
+
* Throws if the template isn't found, isn't a template.prompt, or its
|
|
112
|
+
* @responseRef is missing / doesn't resolve to an object.value.
|
|
113
|
+
*
|
|
114
|
+
* ADR-0052: the shape parsed INTO is `@responseRef`, not `@payloadRef` —
|
|
115
|
+
* `@payloadRef` types the REQUEST rendered outbound, which is the distinction the
|
|
116
|
+
* trace helper has always drawn ("@responseRef types the result; @payloadRef types
|
|
117
|
+
* the request") and the inbound tier used to ignore.
|
|
96
118
|
*/
|
|
97
119
|
export function renderOutputParser(root: MetaData, templateName: string, ctx?: RenderContext): string {
|
|
98
120
|
const tmpl = findTemplate(root, templateName);
|
|
99
121
|
if (!tmpl) {
|
|
100
122
|
throw new Error(`template "${templateName}" not found in metadata root`);
|
|
101
123
|
}
|
|
102
|
-
if (tmpl.subType !==
|
|
103
|
-
throw new Error(`template "${templateName}" is not a template.
|
|
104
|
-
}
|
|
105
|
-
// ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
|
|
106
|
-
const payloadRef = tmpl.attr(TEMPLATE_ATTR_PAYLOAD_REF);
|
|
107
|
-
if (typeof payloadRef !== "string") {
|
|
108
|
-
throw new Error(`template "${templateName}" missing @payloadRef`);
|
|
124
|
+
if (tmpl.subType !== TEMPLATE_SUBTYPE_PROMPT) {
|
|
125
|
+
throw new Error(`template "${templateName}" is not a template.prompt (got subtype "${tmpl.subType}")`);
|
|
109
126
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
127
|
+
const shape = responseShape(root, tmpl);
|
|
128
|
+
if (!shape) {
|
|
129
|
+
// ADR-0039: resolving — @responseRef may be inherited via extends.
|
|
130
|
+
const declared = tmpl.attr(TEMPLATE_ATTR_RESPONSE_REF);
|
|
131
|
+
throw new Error(
|
|
132
|
+
typeof declared === "string"
|
|
133
|
+
? `template "${templateName}" @responseRef "${declared}" not found in metadata root`
|
|
134
|
+
: `template "${templateName}" missing @responseRef`,
|
|
135
|
+
);
|
|
114
136
|
}
|
|
137
|
+
const { vo, ref: payloadRef } = shape;
|
|
115
138
|
|
|
116
139
|
const schema = renderObjectSchema(vo, root, new Set([payloadRef]), 0);
|
|
117
140
|
const schemaName = `${templateName}Schema`;
|
|
@@ -120,13 +143,31 @@ export function renderOutputParser(root: MetaData, templateName: string, ctx?: R
|
|
|
120
143
|
const parseName = `parse${templateName}`;
|
|
121
144
|
const safeParseName = `safeParse${templateName}`;
|
|
122
145
|
|
|
123
|
-
// FR-010: emit the tolerant extract() API alongside the strict Zod parser
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
146
|
+
// FR-010: emit the tolerant extract() API alongside the strict Zod parser.
|
|
147
|
+
//
|
|
148
|
+
// ADR-0052/0053: a declared @responseRef ALWAYS gets the tolerant path, and the
|
|
149
|
+
// syntax comes from @responseFormat (json|xml, default json) — never from
|
|
150
|
+
// @format, which is the syntax of the rendered prompt BODY. The old json/xml
|
|
151
|
+
// gate on @format is what made a text-bodied prompt with a JSON reply emit a
|
|
152
|
+
// strict parser and no extract at all.
|
|
153
|
+
const format = shape.format;
|
|
154
|
+
|
|
155
|
+
// The strict Zod tier is JSON-ONLY, by construction.
|
|
156
|
+
//
|
|
157
|
+
// Its body is `Schema.parse(JSON.parse(text))`, and there is no XML equivalent to
|
|
158
|
+
// generate. Not because no XML reader exists — `render/src/extract/
|
|
159
|
+
// xml-forgiving-reader.ts` ships one — but because that reader is FORGIVING by
|
|
160
|
+
// design, and strict all-or-nothing semantics layered over a repairing parser is
|
|
161
|
+
// incoherent: it would throw or accept based on how much repair happened, which
|
|
162
|
+
// is not a contract anyone can reason about. The JS runtime's `JSON.parse` is an
|
|
163
|
+
// exact parser, which is what makes the strict tier meaningful for JSON and only
|
|
164
|
+
// for JSON. Before this, an XML template got `JSON.parse` anyway — a generated
|
|
165
|
+
// function that could never work.
|
|
166
|
+
//
|
|
167
|
+
// So an XML reply gets the tolerant extract and nothing else. Its typed shape is
|
|
168
|
+
// `<Name>Extracted` — a nullable mirror, which is the honest type for a
|
|
169
|
+
// best-effort parse of model output.
|
|
170
|
+
const emitStrict = format !== RESPONSE_FORMAT_XML;
|
|
130
171
|
|
|
131
172
|
const strictBody = `const ${schemaName} = ${schema};
|
|
132
173
|
|
|
@@ -164,15 +205,14 @@ export function ${safeParseName}(
|
|
|
164
205
|
}
|
|
165
206
|
`;
|
|
166
207
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
// ---- FR-010 tolerant extract block (json/xml only) ----
|
|
208
|
+
// ---- FR-010 tolerant extract block ----
|
|
209
|
+
// Unconditional since ADR-0052: a declared @responseRef IS the request for the
|
|
210
|
+
// tolerant path, and @responseFormat is a closed json|xml set, so there is no
|
|
211
|
+
// longer a third case to gate on.
|
|
172
212
|
const extractedName = `${templateName}Extracted`;
|
|
173
213
|
const extractLenientWithName = `extractLenient${templateName}WithLoader`;
|
|
174
214
|
const payloadFqnConst = `${templateName.toUpperCase()}_PAYLOAD_NAME`;
|
|
175
|
-
const formatEnum = format
|
|
215
|
+
const formatEnum = format === RESPONSE_FORMAT_XML ? "Format.XML" : "Format.JSON";
|
|
176
216
|
|
|
177
217
|
// The nullable mirror is the return shape of the delegating extract. Use the nested-aware
|
|
178
218
|
// emitter so the payload mirror's nested-object / array-of-object components are typed (not
|
|
@@ -262,13 +302,15 @@ export function ${extractLenientWithName}(
|
|
|
262
302
|
: `import type { MetaRoot } from "@metaobjectsdev/metadata";\n`;
|
|
263
303
|
const runtimeImport = `import { extractObject } from "@metaobjectsdev/runtime-ts";\n`;
|
|
264
304
|
|
|
305
|
+
// `zod` is imported only when the strict tier is emitted — an XML reply's file
|
|
306
|
+
// would otherwise carry an unused import (tsc noUnusedLocals-unsafe).
|
|
265
307
|
return (
|
|
266
|
-
`import { z } from "zod";\n` +
|
|
308
|
+
(emitStrict ? `import { z } from "zod";\n` : "") +
|
|
267
309
|
`import {\n ${renderImports.join(",\n ")},\n} from "@metaobjectsdev/render";\n` +
|
|
268
310
|
metadataImport +
|
|
269
311
|
runtimeImport +
|
|
270
312
|
`\n` +
|
|
271
|
-
`${strictBody}\n` +
|
|
313
|
+
(emitStrict ? `${strictBody}\n` : "") +
|
|
272
314
|
`${delegating}`
|
|
273
315
|
);
|
|
274
316
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// server/typescript/packages/codegen-ts/src/templates/output-prompt.ts
|
|
2
2
|
//
|
|
3
|
-
// Per-template renderer for the FR-010 artifact-1
|
|
4
|
-
// ("produce your answer like this"). For each
|
|
5
|
-
//
|
|
6
|
-
// exporting `render<
|
|
7
|
-
// renderOutputFormat(). The baked OutputFormatSpec's rootName is the
|
|
8
|
-
// so the
|
|
3
|
+
// Per-template renderer for the FR-010 artifact-1 response-format fragment
|
|
4
|
+
// ("produce your answer like this"). For each `template.prompt` whose @responseRef
|
|
5
|
+
// resolves to a value-object, emits a `<PromptName>.responseFormat.ts` file
|
|
6
|
+
// exporting `render<PromptName>Format(overrides?)` backed by the render engine's
|
|
7
|
+
// renderOutputFormat(). The baked OutputFormatSpec's rootName is the RESPONSE VO's
|
|
8
|
+
// name, so the fragment and the extract() codegen agree on the root name.
|
|
9
9
|
//
|
|
10
10
|
// Mirrors the C# OutputPromptGenerator + OutputFormatSpecEmitter (split into a
|
|
11
11
|
// generator factory + this pure renderer, matching the TS output-parser shape).
|
|
@@ -13,59 +13,46 @@
|
|
|
13
13
|
import {
|
|
14
14
|
type MetaData,
|
|
15
15
|
TYPE_TEMPLATE,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
TEMPLATE_ATTR_FORMAT,
|
|
19
|
-
resolveObjectRef,
|
|
16
|
+
TEMPLATE_SUBTYPE_PROMPT,
|
|
17
|
+
TEMPLATE_ATTR_RESPONSE_REF,
|
|
20
18
|
} from "@metaobjectsdev/metadata";
|
|
21
19
|
import { specLiteral } from "./output-format-spec-emitter.js";
|
|
22
|
-
|
|
23
|
-
// ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
|
|
24
|
-
// ADR-0042: resolveObjectRef gives package-local-before-root-level precedence for a bare ref, FQN-exact otherwise.
|
|
25
|
-
function findObject(root: MetaData, name: string, referrerPkg = ""): MetaData | undefined {
|
|
26
|
-
return resolveObjectRef(root, name, referrerPkg).node;
|
|
27
|
-
}
|
|
20
|
+
import { responseShape } from "./find-inbound.js";
|
|
28
21
|
|
|
29
22
|
// ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
|
|
30
23
|
function findTemplate(root: MetaData, name: string): MetaData | undefined {
|
|
31
24
|
return root.children().find((c) => c.type === TYPE_TEMPLATE && c.name === name);
|
|
32
25
|
}
|
|
33
26
|
|
|
34
|
-
/** True iff the template.output's @format is json or xml (the renderable structured formats). */
|
|
35
|
-
export function templateSupportsPrompt(tmpl: MetaData): boolean {
|
|
36
|
-
// ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
|
|
37
|
-
const f = ((tmpl.attr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text").toLowerCase();
|
|
38
|
-
return f === "json" || f === "xml";
|
|
39
|
-
}
|
|
40
|
-
|
|
41
27
|
/**
|
|
42
|
-
* Render the full
|
|
43
|
-
* Throws if the template isn't found, isn't a template.
|
|
44
|
-
* or its @
|
|
28
|
+
* Render the full response-format fragment file for one responding
|
|
29
|
+
* `template.prompt`. Throws if the template isn't found, isn't a template.prompt,
|
|
30
|
+
* or its @responseRef is missing / doesn't resolve to an object.value.
|
|
31
|
+
*
|
|
32
|
+
* ADR-0052: the fragment describes the shape named by `@responseRef` — the reply —
|
|
33
|
+
* not `@payloadRef`, which is the request this prompt renders outbound.
|
|
45
34
|
*/
|
|
46
35
|
export function renderOutputPrompt(root: MetaData, templateName: string): string {
|
|
47
36
|
const tmpl = findTemplate(root, templateName);
|
|
48
37
|
if (!tmpl) {
|
|
49
38
|
throw new Error(`template "${templateName}" not found in metadata root`);
|
|
50
39
|
}
|
|
51
|
-
if (tmpl.subType !==
|
|
52
|
-
throw new Error(`template "${templateName}" is not a template.
|
|
53
|
-
}
|
|
54
|
-
if (!templateSupportsPrompt(tmpl)) {
|
|
55
|
-
throw new Error(`template "${templateName}" @format is not json/xml — no prompt fragment`);
|
|
56
|
-
}
|
|
57
|
-
// ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
|
|
58
|
-
const payloadRef = tmpl.attr(TEMPLATE_ATTR_PAYLOAD_REF);
|
|
59
|
-
if (typeof payloadRef !== "string") {
|
|
60
|
-
throw new Error(`template "${templateName}" missing @payloadRef`);
|
|
40
|
+
if (tmpl.subType !== TEMPLATE_SUBTYPE_PROMPT) {
|
|
41
|
+
throw new Error(`template "${templateName}" is not a template.prompt (got subtype "${tmpl.subType}")`);
|
|
61
42
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
43
|
+
const shape = responseShape(root, tmpl);
|
|
44
|
+
if (!shape) {
|
|
45
|
+
// ADR-0039: resolving — @responseRef may be inherited via extends.
|
|
46
|
+
const declared = tmpl.attr(TEMPLATE_ATTR_RESPONSE_REF);
|
|
47
|
+
throw new Error(
|
|
48
|
+
typeof declared === "string"
|
|
49
|
+
? `template "${templateName}" @responseRef "${declared}" not found in metadata root`
|
|
50
|
+
: `template "${templateName}" missing @responseRef`,
|
|
51
|
+
);
|
|
66
52
|
}
|
|
53
|
+
const { vo, ref: payloadRef } = shape;
|
|
67
54
|
|
|
68
|
-
// rootName ==
|
|
55
|
+
// rootName == response VO name so the fragment and extract() agree.
|
|
69
56
|
const spec = specLiteral(vo, tmpl, payloadRef);
|
|
70
57
|
const specName = `${templateName}FormatSpec`;
|
|
71
58
|
const fnName = `render${templateName}Format`;
|
|
@@ -82,7 +69,7 @@ export function renderOutputPrompt(root: MetaData, templateName: string): string
|
|
|
82
69
|
const ${specName}: OutputFormatSpec = ${spec};
|
|
83
70
|
|
|
84
71
|
/**
|
|
85
|
-
* The
|
|
72
|
+
* The response-format instruction fragment for the ${templateName} template.prompt
|
|
86
73
|
* ("produce your answer like this"). Comment-free — guidance lives in prose /
|
|
87
74
|
* inline placeholders / a filled skeleton. Pass \`overrides\` to override the style
|
|
88
75
|
* or per-field example/instruction at render time.
|
package/src/templates/queries.ts
CHANGED
|
@@ -130,7 +130,7 @@ export function renderUpdateFn(entity: MetaObject, ctx: RenderContext): Code {
|
|
|
130
130
|
const fnName = updateFnName(entityName);
|
|
131
131
|
const findByIdFn = findByIdFnName(entityName);
|
|
132
132
|
// PATCH contract (FR-035): validate the caller's assignments against the
|
|
133
|
-
// UPDATE schema (all-optional,
|
|
133
|
+
// UPDATE schema (all-optional; PK, @mutability readOnly AND writeOnce excluded; no insert-time transforms
|
|
134
134
|
// like @autoSet-onCreate → now() or the InsertSchema's discriminator handling)
|
|
135
135
|
// — NOT `InsertSchema.partial()`. The typed `<Entity>Patch` param makes a
|
|
136
136
|
// renamed/dropped field a compile error at every call site (PATCH-1..4);
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// FR-038 §4 — the DEFAULT requirement-test renderer.
|
|
2
|
+
//
|
|
3
|
+
// "Default" is the operative word: an application owns its testing style, so this
|
|
4
|
+
// is what it gets when it registers no renderer of its own. The library supplies
|
|
5
|
+
// DATA (statement, counterexample, status, claimed refs); a renderer supplies SYNTAX.
|
|
6
|
+
// That separation is what keeps bun / vitest / jest / pytest / JUnit / xUnit from
|
|
7
|
+
// each being an upstream change to this package.
|
|
8
|
+
//
|
|
9
|
+
// The load-bearing rule: an empty generated stub must NOT pass. A `live` entry
|
|
10
|
+
// claims the capability works, so an empty green test asserts the opposite of the
|
|
11
|
+
// claim — the original defect recreated in a new place.
|
|
12
|
+
|
|
13
|
+
import { GENERATED_HEADER } from "../constants.js";
|
|
14
|
+
import type { RequirementView, ResolvedClaim } from "../requirement-walk.js";
|
|
15
|
+
|
|
16
|
+
export interface RequirementTestArgs {
|
|
17
|
+
readonly view: RequirementView;
|
|
18
|
+
readonly concern: string;
|
|
19
|
+
readonly statement: string;
|
|
20
|
+
readonly counterexample: string;
|
|
21
|
+
readonly targets: readonly ResolvedClaim[];
|
|
22
|
+
readonly disposition?: string | undefined;
|
|
23
|
+
readonly trackedBy?: readonly string[] | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Statuses whose stub is SKIPPED rather than failing.
|
|
28
|
+
*
|
|
29
|
+
* The rule is "does this entry claim the capability works right now?" — only `live`
|
|
30
|
+
* and `partial` do. `planned` is intended-not-built; `abandoned` and `superseded`
|
|
31
|
+
* describe a capability deliberately retired, whose `@implementedBy` is SUPPOSED to
|
|
32
|
+
* dangle. Emitting a failing stub for any of the three reddens an application's suite
|
|
33
|
+
* forever for something nobody intends to build, which is the noise an app silences
|
|
34
|
+
* wholesale — taking the `live` stubs with it.
|
|
35
|
+
*
|
|
36
|
+
* (FR-038 §4 proposes retiring `abandoned`/`superseded` from the vocabulary entirely.
|
|
37
|
+
* Until that breaking cut lands they are legal `@status` values, so the renderer has
|
|
38
|
+
* to handle them.)
|
|
39
|
+
*/
|
|
40
|
+
const SKIPPED_STATUSES: ReadonlySet<string> = new Set([
|
|
41
|
+
"planned",
|
|
42
|
+
"abandoned",
|
|
43
|
+
"superseded",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Escape an author-supplied value for a double-quoted TS string literal.
|
|
48
|
+
*
|
|
49
|
+
* Unescaped, a quote closes the literal and a newline breaks it — emitting a stub that
|
|
50
|
+
* does not parse, which `meta gen` still reports as written because nothing here
|
|
51
|
+
* compiles it.
|
|
52
|
+
*
|
|
53
|
+
* Applied to EVERY value that reaches a literal, not only the two obviously-prose ones.
|
|
54
|
+
* `@statement` and `@counterexample` are the fields that look dangerous, but the requirement
|
|
55
|
+
* path and the concern land in the same two literals, and escaping only what looks like
|
|
56
|
+
* prose leaves the identical hole open one line down.
|
|
57
|
+
*/
|
|
58
|
+
function forStringLiteral(s: string): string {
|
|
59
|
+
return s
|
|
60
|
+
.replace(/\\/g, "\\\\")
|
|
61
|
+
.replace(/"/g, '\\"')
|
|
62
|
+
.replace(/\r?\n/g, "\\n")
|
|
63
|
+
.replace(/\r/g, "\\r");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Make an author-supplied value safe inside a JSDoc block, preserving what was written.
|
|
68
|
+
*
|
|
69
|
+
* A comment-terminator would close the block early and spill the remainder into code; a
|
|
70
|
+
* newline needs its own continuation marker or the block's shape breaks. The terminator
|
|
71
|
+
* is separated by a space rather than deleted, so the sentence still reads — and
|
|
72
|
+
* deliberately NOT by a zero-width space, which would keep the text pixel-identical at
|
|
73
|
+
* the cost of putting an invisible character into generated source that nobody can see
|
|
74
|
+
* when debugging it.
|
|
75
|
+
*
|
|
76
|
+
* Every value interpolated into the block goes through this, including the ones whose
|
|
77
|
+
* shape the loader constrains. `@trackedBy` is the one that makes the rule non-optional:
|
|
78
|
+
* it is registered free-form ON PURPOSE — `verify` never resolves it, because which
|
|
79
|
+
* sprint owns a gap belongs in the tracker — so its contract invites arbitrary text.
|
|
80
|
+
*/
|
|
81
|
+
function forDocComment(s: string): string {
|
|
82
|
+
return s.replace(/\*\//g, "* /").replace(/\r?\n/g, "\n * ");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function claimLines(targets: readonly ResolvedClaim[]): string {
|
|
86
|
+
if (targets.length === 0) {
|
|
87
|
+
return " * (none — this requirement names no model nodes)";
|
|
88
|
+
}
|
|
89
|
+
return targets
|
|
90
|
+
.map((t) => ` * - ${forDocComment(t.ref)} (${forDocComment(t.concern)})`)
|
|
91
|
+
.join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function gapLine(a: RequirementTestArgs): string {
|
|
95
|
+
const tracked = a.trackedBy ?? [];
|
|
96
|
+
if (a.disposition === undefined && tracked.length === 0) return "";
|
|
97
|
+
const decided = forDocComment(a.disposition ?? "undecided");
|
|
98
|
+
const refs = tracked.length > 0 ? ` — ${tracked.map(forDocComment).join(", ")}` : "";
|
|
99
|
+
return `\n *\n * Known gap: ${decided}${refs}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function renderRequirementTest(a: RequirementTestArgs): string {
|
|
103
|
+
const skipped = a.view.status !== undefined && SKIPPED_STATUSES.has(a.view.status);
|
|
104
|
+
const statement = forDocComment(a.statement);
|
|
105
|
+
const counterexample = forDocComment(a.counterexample);
|
|
106
|
+
const runner = skipped ? "test.skip" : "test";
|
|
107
|
+
// The test NAME is the link between the ledger entry and the assertion, so it is
|
|
108
|
+
// built from the same two values every time — but it is still a string literal, and
|
|
109
|
+
// an unescaped quote in either closes it.
|
|
110
|
+
const testName = `${forStringLiteral(a.view.path)} [${forStringLiteral(a.concern)}]`;
|
|
111
|
+
|
|
112
|
+
// A `live` or `partial` stub asserts FAILURE until someone writes the real
|
|
113
|
+
// assertion over it. `expect.unreachable` names the requirement in the failure
|
|
114
|
+
// message, so a red run says which claim is unproven rather than just "failed".
|
|
115
|
+
const body = skipped
|
|
116
|
+
? ` // Intended, not built. Write the assertion when this becomes live.`
|
|
117
|
+
: ` expect.unreachable(\n` +
|
|
118
|
+
` "unimplemented requirement stub: ${testName} — " +\n` +
|
|
119
|
+
` "replace this with an assertion that fails when: ${forStringLiteral(a.counterexample)}",\n` +
|
|
120
|
+
` );`;
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
`// ${GENERATED_HEADER}.\n` +
|
|
124
|
+
`// The test IDENTITY is generated from the requirement; the BODY below is yours\n` +
|
|
125
|
+
`// and survives regeneration. Do not rename the test — the name is the link.\n` +
|
|
126
|
+
`import { test, expect } from "bun:test";\n` +
|
|
127
|
+
`\n` +
|
|
128
|
+
`/**\n` +
|
|
129
|
+
` * ${statement}\n` +
|
|
130
|
+
` *\n` +
|
|
131
|
+
` * Counterexample: ${counterexample}${gapLine(a)}\n` +
|
|
132
|
+
` *\n` +
|
|
133
|
+
` * Claims:\n` +
|
|
134
|
+
`${claimLines(a.targets)}\n` +
|
|
135
|
+
` */\n` +
|
|
136
|
+
`${runner}("${testName}", () => {\n` +
|
|
137
|
+
`${body}\n` +
|
|
138
|
+
`});\n`
|
|
139
|
+
);
|
|
140
|
+
}
|