@metaobjectsdev/codegen-ts 0.23.2 → 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/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/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/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/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/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/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/extractor.ts +19 -24
- 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
|
@@ -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
|
+
}
|
|
@@ -22,7 +22,8 @@ import {
|
|
|
22
22
|
IDENTITY_ATTR_FIELDS, IDENTITY_ATTR_GENERATION,
|
|
23
23
|
FIELD_ATTR_STRING_FORMAT, FIELD_ATTR_LENIENT, STRING_FORMAT_EMAIL, STRING_FORMAT_HOSTNAME,
|
|
24
24
|
FIELD_ATTR_REQUIRED, FIELD_ATTR_MAX_LENGTH, FIELD_ATTR_DEFAULT,
|
|
25
|
-
FIELD_ATTR_AUTO_SET, FIELD_ATTR_OBJECT_REF, FIELD_ATTR_VALUE_TYPE,
|
|
25
|
+
FIELD_ATTR_AUTO_SET, FIELD_ATTR_OBJECT_REF, FIELD_ATTR_VALUE_TYPE,
|
|
26
|
+
isReadOnlyMutability, isWriteOnceMutability,
|
|
26
27
|
FIELD_ATTR_DB_COLUMN_TYPE, DB_COLUMN_TYPE_JSONB,
|
|
27
28
|
AUTO_SET_ON_CREATE, AUTO_SET_ON_UPDATE,
|
|
28
29
|
VALIDATOR_ATTR_MAX, VALIDATOR_ATTR_MIN, VALIDATOR_ATTR_PATTERN,
|
|
@@ -215,12 +216,13 @@ export function renderInsertSchemaOnly(obj: MetaObject, ctx?: RenderContext): Co
|
|
|
215
216
|
const insertFieldLines: Code[] = [];
|
|
216
217
|
for (const child of obj.fields()) {
|
|
217
218
|
if (autoGenPkFields.has(child.name)) continue;
|
|
218
|
-
// FR-
|
|
219
|
-
// owner; the application has no path to write
|
|
220
|
-
// create-shape schema entirely.
|
|
219
|
+
// FR-037 R1: an @mutability:"readOnly" field is populated by DB / replication /
|
|
220
|
+
// external owner; the application has no path to write it. Exclude from the
|
|
221
|
+
// create-shape schema entirely. ("writeOnce" is NOT excluded here — that is
|
|
222
|
+
// the whole point of the mode: settable on create, frozen after.)
|
|
221
223
|
// #213 — a derived (origin-bearing) field is read-only (FR-024 §7 / ADR-0028):
|
|
222
|
-
// excluded from the Insert/Update/preserving schemas, exactly like
|
|
223
|
-
if (child
|
|
224
|
+
// excluded from the Insert/Update/preserving schemas, exactly like readOnly.
|
|
225
|
+
if (isReadOnlyMutability(child) || child.isDerived()) continue;
|
|
224
226
|
|
|
225
227
|
// FR-017 Tier 1: TPH subtype pins its discriminator field to z.literal(...).
|
|
226
228
|
if (tphPin !== undefined && child.name === tphPin.fieldName) {
|
|
@@ -300,8 +302,9 @@ export function insertSchemaFields(obj: MetaObject): SchemaFieldShape[] {
|
|
|
300
302
|
for (const child of obj.fields()) {
|
|
301
303
|
if (autoGenPkFields.has(child.name)) continue;
|
|
302
304
|
// #213 — a derived (origin-bearing) field is read-only (FR-024 §7 / ADR-0028):
|
|
303
|
-
// excluded from the Insert/Update/preserving schemas, exactly like
|
|
304
|
-
|
|
305
|
+
// excluded from the Insert/Update/preserving schemas, exactly like readOnly.
|
|
306
|
+
// "writeOnce" stays IN the create shape — it is settable exactly once.
|
|
307
|
+
if (isReadOnlyMutability(child) || child.isDerived()) continue;
|
|
305
308
|
if (tphPin !== undefined && child.name === tphPin.fieldName) {
|
|
306
309
|
out.push({ name: child.name, optional: false, pinnedLiteral: tphPin.value });
|
|
307
310
|
continue;
|
|
@@ -335,8 +338,16 @@ export function updateSchemaFields(obj: MetaObject): SchemaFieldShape[] {
|
|
|
335
338
|
for (const child of obj.fields()) {
|
|
336
339
|
if (autoGenPkFields.has(child.name)) continue;
|
|
337
340
|
// #213 — a derived (origin-bearing) field is read-only (FR-024 §7 / ADR-0028):
|
|
338
|
-
// excluded from the Insert/Update/preserving schemas, exactly like
|
|
339
|
-
if (child
|
|
341
|
+
// excluded from the Insert/Update/preserving schemas, exactly like readOnly.
|
|
342
|
+
if (isReadOnlyMutability(child) || child.isDerived()) continue;
|
|
343
|
+
// FR-037 R1 — "writeOnce" leaves the UPDATE shape, and only the update shape.
|
|
344
|
+
// Omitted, not rejected: a key absent from the settable set is STRIPPED on this
|
|
345
|
+
// path today (D1), and the generated edit form submits EVERY registered field
|
|
346
|
+
// (0.19.2 switched the resolver to UpdateSchema on edit and does not
|
|
347
|
+
// diff-and-omit), so 400-on-present would fail every save on every generated
|
|
348
|
+
// edit form for an entity carrying one. Clearing counts as a write, so the
|
|
349
|
+
// FR-035 present-null arm never reaches it either.
|
|
350
|
+
if (isWriteOnceMutability(child)) continue;
|
|
340
351
|
// TPH subtype discriminator: omitted from the update schema entirely.
|
|
341
352
|
if (tphPin !== undefined && child.name === tphPin.fieldName) continue;
|
|
342
353
|
const autoSet = child.attr(FIELD_ATTR_AUTO_SET);
|
|
@@ -370,13 +381,13 @@ export function renderZodValidators(obj: MetaObject, ctx?: RenderContext): Code
|
|
|
370
381
|
const emitPreserving = hasAutoSetFields(obj);
|
|
371
382
|
for (const child of obj.fields()) {
|
|
372
383
|
if (autoGenPkFields.has(child.name)) continue;
|
|
373
|
-
// FR-
|
|
374
|
-
// The DB / trigger / replication owns the write path; the app
|
|
375
|
-
// pass these values in POST/PATCH bodies (routesFile enforces the same
|
|
384
|
+
// FR-037 R1: an @mutability:"readOnly" field appears in neither InsertSchema
|
|
385
|
+
// nor UpdateSchema. The DB / trigger / replication owns the write path; the app
|
|
386
|
+
// must not pass these values in POST/PATCH bodies (routesFile enforces the same
|
|
376
387
|
// contract at the boundary with a 400 response).
|
|
377
388
|
// #213 — a derived (origin-bearing) field is read-only (FR-024 §7 / ADR-0028):
|
|
378
|
-
// excluded from the Insert/Update/preserving schemas, exactly like
|
|
379
|
-
if (child
|
|
389
|
+
// excluded from the Insert/Update/preserving schemas, exactly like readOnly.
|
|
390
|
+
if (isReadOnlyMutability(child) || child.isDerived()) continue;
|
|
380
391
|
|
|
381
392
|
// FR-017 Tier 1: TPH subtype pins its discriminator field to z.literal(...).
|
|
382
393
|
// The discriminator is implicit on subtype rows (controlled by URL / insert
|
|
@@ -413,6 +424,12 @@ export function renderZodValidators(obj: MetaObject, ctx?: RenderContext): Code
|
|
|
413
424
|
preservingFieldLines.push(fieldLine);
|
|
414
425
|
}
|
|
415
426
|
|
|
427
|
+
// FR-037 R1 — "writeOnce": present in Insert + preserving (both are CREATE
|
|
428
|
+
// shapes), absent from Update. Same mechanism that drops @autoSet onCreate
|
|
429
|
+
// below; a value presented on PATCH is stripped, not 400'd (see
|
|
430
|
+
// updateSchemaFields for the grounding).
|
|
431
|
+
if (isWriteOnceMutability(child)) continue;
|
|
432
|
+
|
|
416
433
|
// Update schema: @autoSet onCreate → omit entirely; onUpdate → transform
|
|
417
434
|
if (autoSet === AUTO_SET_ON_CREATE) {
|
|
418
435
|
// Omit: creation timestamps cannot be changed after creation
|