@metaobjectsdev/render 0.8.1-rc.1 → 0.9.0-rc.1
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/email-document.d.ts +7 -0
- package/dist/email-document.d.ts.map +1 -0
- package/dist/email-document.js +2 -0
- package/dist/email-document.js.map +1 -0
- package/dist/extract/coerce.d.ts +15 -0
- package/dist/extract/coerce.d.ts.map +1 -0
- package/dist/{recover → extract}/coerce.js +87 -13
- package/dist/extract/coerce.js.map +1 -0
- package/dist/{recover/recover-map.d.ts → extract/extract-map.d.ts} +1 -1
- package/dist/{recover/recover-map.d.ts.map → extract/extract-map.d.ts.map} +1 -1
- package/dist/{recover/recover-map.js → extract/extract-map.js} +3 -3
- package/dist/{recover/recover-map.js.map → extract/extract-map.js.map} +1 -1
- package/dist/extract/extract.d.ts +4 -0
- package/dist/extract/extract.d.ts.map +1 -0
- package/dist/extract/extract.js +157 -0
- package/dist/extract/extract.js.map +1 -0
- package/dist/{recover → extract}/json-forgiving-reader.d.ts.map +1 -1
- package/dist/{recover → extract}/json-forgiving-reader.js +1 -1
- package/dist/{recover → extract}/json-forgiving-reader.js.map +1 -1
- package/dist/{recover → extract}/locate.d.ts.map +1 -1
- package/dist/{recover → extract}/locate.js.map +1 -1
- package/dist/extract/normalize.d.ts +4 -0
- package/dist/extract/normalize.d.ts.map +1 -0
- package/dist/extract/normalize.js +22 -0
- package/dist/extract/normalize.js.map +1 -0
- package/dist/extract/strip.d.ts.map +1 -0
- package/dist/{recover → extract}/strip.js.map +1 -1
- package/dist/extract/types.d.ts +160 -0
- package/dist/extract/types.d.ts.map +1 -0
- package/dist/extract/types.js +221 -0
- package/dist/extract/types.js.map +1 -0
- package/dist/{recover → extract}/xml-forgiving-reader.d.ts.map +1 -1
- package/dist/{recover → extract}/xml-forgiving-reader.js +1 -1
- package/dist/{recover → extract}/xml-forgiving-reader.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/prompt/output-format-renderer.d.ts.map +1 -1
- package/dist/prompt/output-format-renderer.js +113 -59
- package/dist/prompt/output-format-renderer.js.map +1 -1
- package/dist/prompt/output-format-spec.d.ts +1 -1
- package/dist/prompt/prompt-field.d.ts +1 -1
- package/package.json +1 -1
- package/src/email-document.ts +6 -0
- package/src/extract/KNOWN_GAPS.md +59 -0
- package/src/extract/coerce.ts +224 -0
- package/src/{recover/recover-map.ts → extract/extract-map.ts} +2 -2
- package/src/extract/extract.ts +187 -0
- package/src/{recover → extract}/json-forgiving-reader.ts +1 -1
- package/src/extract/normalize.ts +23 -0
- package/src/extract/types.ts +346 -0
- package/src/{recover → extract}/xml-forgiving-reader.ts +1 -1
- package/src/index.ts +17 -11
- package/src/prompt/output-format-renderer.ts +140 -61
- package/src/prompt/output-format-spec.ts +1 -1
- package/src/prompt/prompt-field.ts +1 -1
- package/dist/recover/coerce.d.ts +0 -5
- package/dist/recover/coerce.d.ts.map +0 -1
- package/dist/recover/coerce.js.map +0 -1
- package/dist/recover/recover.d.ts +0 -4
- package/dist/recover/recover.d.ts.map +0 -1
- package/dist/recover/recover.js +0 -115
- package/dist/recover/recover.js.map +0 -1
- package/dist/recover/strip.d.ts.map +0 -1
- package/dist/recover/types.d.ts +0 -117
- package/dist/recover/types.d.ts.map +0 -1
- package/dist/recover/types.js +0 -124
- package/dist/recover/types.js.map +0 -1
- package/src/recover/KNOWN_GAPS.md +0 -35
- package/src/recover/coerce.ts +0 -141
- package/src/recover/recover.ts +0 -146
- package/src/recover/types.ts +0 -217
- /package/dist/{recover → extract}/json-forgiving-reader.d.ts +0 -0
- /package/dist/{recover → extract}/locate.d.ts +0 -0
- /package/dist/{recover → extract}/locate.js +0 -0
- /package/dist/{recover → extract}/strip.d.ts +0 -0
- /package/dist/{recover → extract}/strip.js +0 -0
- /package/dist/{recover → extract}/xml-forgiving-reader.d.ts +0 -0
- /package/src/{recover → extract}/locate.ts +0 -0
- /package/src/{recover → extract}/strip.ts +0 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// reference (com.metaobjects.render.prompt.OutputFormatRenderer). Do not change
|
|
10
10
|
// the verbatim prose, skeleton shapes, or numeric-vs-quoted decision.
|
|
11
11
|
import { ESCAPERS } from "../escapers.js";
|
|
12
|
-
import { FieldKind, Format } from "../
|
|
12
|
+
import { FieldKind, Format } from "../extract/types.js";
|
|
13
13
|
import { PromptStyle } from "./prompt-style.js";
|
|
14
14
|
const NUMERIC_KINDS = new Set([
|
|
15
15
|
FieldKind.INT,
|
|
@@ -17,6 +17,8 @@ const NUMERIC_KINDS = new Set([
|
|
|
17
17
|
FieldKind.DOUBLE,
|
|
18
18
|
FieldKind.BOOLEAN,
|
|
19
19
|
]);
|
|
20
|
+
const INDENT = " ";
|
|
21
|
+
const MAX_NEST_DEPTH = 8;
|
|
20
22
|
// The render engine OWNS format-keyed escaping; Format ("JSON"/"XML") maps to the
|
|
21
23
|
// lowercase ESCAPERS keys.
|
|
22
24
|
const escapeXml = (s) => ESCAPERS.xml(s);
|
|
@@ -39,20 +41,8 @@ export function renderOutputFormat(spec, overrides) {
|
|
|
39
41
|
// ---- INLINE ----------------------------------------------------------------
|
|
40
42
|
function renderInline(spec, overrides) {
|
|
41
43
|
return spec.format === Format.XML
|
|
42
|
-
?
|
|
43
|
-
:
|
|
44
|
-
}
|
|
45
|
-
function renderXmlInline(spec, overrides) {
|
|
46
|
-
const lines = spec.fields.map((field) => {
|
|
47
|
-
const escaped = escapeXml(inlineContent(field, overrides));
|
|
48
|
-
return ` <${field.name}>${escaped}</${field.name}>\n`;
|
|
49
|
-
});
|
|
50
|
-
return `<${spec.rootName}>\n${lines.join("")}</${spec.rootName}>`;
|
|
51
|
-
}
|
|
52
|
-
function renderJsonInline(spec, overrides) {
|
|
53
|
-
const lines = spec.fields.map((field) => ` "${field.name}": "${escapeJson(inlineContent(field, overrides))}"`);
|
|
54
|
-
// Empty object is `{\n}` (Java/C# parity), not `{\n\n}` from join("") on no lines.
|
|
55
|
-
return spec.fields.length === 0 ? "{\n}" : `{\n${lines.join(",\n")}\n}`;
|
|
44
|
+
? renderXmlSkeleton(spec, overrides, "inline")
|
|
45
|
+
: renderJsonSkeleton(spec, overrides, "inline");
|
|
56
46
|
}
|
|
57
47
|
function inlineContent(field, overrides) {
|
|
58
48
|
if (field.kind === FieldKind.ENUM && field.enumValues != null && field.enumValues.length > 0) {
|
|
@@ -74,59 +64,123 @@ function resolveInstruction(field, overrides) {
|
|
|
74
64
|
// ---- GUIDE -----------------------------------------------------------------
|
|
75
65
|
function renderGuide(spec, overrides) {
|
|
76
66
|
let sb = "Fill in each field as described below:\n";
|
|
67
|
+
sb += guideFields(spec, overrides, "", new Set([spec]), 0);
|
|
68
|
+
sb += "\nRespond exactly like this:\n";
|
|
69
|
+
sb += renderExampleOnly(spec, overrides);
|
|
70
|
+
return sb;
|
|
71
|
+
}
|
|
72
|
+
function guideFields(spec, overrides, prefix, path, depth) {
|
|
73
|
+
let sb = "";
|
|
77
74
|
for (const field of spec.fields) {
|
|
78
|
-
const
|
|
79
|
-
sb +=
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
const displayName = prefix + field.name;
|
|
76
|
+
sb += guideEntry(field, overrides, displayName);
|
|
77
|
+
if (canExpand(field, path, depth)) {
|
|
78
|
+
const nested = field.nested;
|
|
79
|
+
const childPrefix = field.array ? `${displayName}[].` : `${displayName}.`;
|
|
80
|
+
path.add(nested);
|
|
81
|
+
sb += guideFields(nested, overrides, childPrefix, path, depth + 1);
|
|
82
|
+
path.delete(nested);
|
|
83
83
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
84
|
+
}
|
|
85
|
+
return sb;
|
|
86
|
+
}
|
|
87
|
+
function guideEntry(field, overrides, displayName) {
|
|
88
|
+
const req = field.required ? "required" : "optional";
|
|
89
|
+
let sb = `- ${displayName} (${req})`;
|
|
90
|
+
const instruction = resolveInstruction(field, overrides);
|
|
91
|
+
if (instruction != null)
|
|
92
|
+
sb += `: ${instruction}`;
|
|
93
|
+
sb += "\n";
|
|
94
|
+
if (field.kind === FieldKind.ENUM && field.enumValues != null && field.enumValues.length > 0) {
|
|
95
|
+
sb += ` one of ${field.enumValues.join(", ")}\n`;
|
|
96
|
+
const enumDoc = field.enumDoc;
|
|
97
|
+
if (enumDoc != null) {
|
|
98
|
+
for (const val of field.enumValues) {
|
|
99
|
+
const doc = enumDoc[val];
|
|
100
|
+
if (doc != null)
|
|
101
|
+
sb += ` ${val} = ${doc}\n`;
|
|
95
102
|
}
|
|
96
103
|
}
|
|
97
|
-
const eg = exampleValueIfDeclared(field, overrides);
|
|
98
|
-
if (eg != null) {
|
|
99
|
-
sb += ` e.g. ${eg}\n`;
|
|
100
|
-
}
|
|
101
104
|
}
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
const eg = exampleValueIfDeclared(field, overrides);
|
|
106
|
+
if (eg != null)
|
|
107
|
+
sb += ` e.g. ${eg}\n`;
|
|
104
108
|
return sb;
|
|
105
109
|
}
|
|
106
110
|
// ---- EXAMPLE-ONLY (also the skeleton appended by GUIDE) ---------------------
|
|
107
111
|
function renderExampleOnly(spec, overrides) {
|
|
108
112
|
return spec.format === Format.XML
|
|
109
|
-
? renderXmlSkeleton(spec, overrides)
|
|
110
|
-
: renderJsonSkeleton(spec, overrides);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
113
|
+
? renderXmlSkeleton(spec, overrides, "example")
|
|
114
|
+
: renderJsonSkeleton(spec, overrides, "example");
|
|
115
|
+
}
|
|
116
|
+
// ---- JSON skeleton (recursive) ---------------------------------------------
|
|
117
|
+
function renderJsonSkeleton(spec, overrides, mode) {
|
|
118
|
+
return jsonObject(spec, overrides, "", mode, new Set([spec]), 0);
|
|
119
|
+
}
|
|
120
|
+
function jsonObject(spec, overrides, braceIndent, mode, path, depth) {
|
|
121
|
+
if (spec.fields.length === 0)
|
|
122
|
+
return `{\n${braceIndent}}`;
|
|
123
|
+
const fieldIndent = braceIndent + INDENT;
|
|
124
|
+
const lines = spec.fields.map((field) => `${fieldIndent}"${field.name}": ${jsonValue(field, overrides, fieldIndent, mode, path, depth)}`);
|
|
125
|
+
return `{\n${lines.join(",\n")}\n${braceIndent}}`;
|
|
126
|
+
}
|
|
127
|
+
function jsonValue(field, overrides, indent, mode, path, depth) {
|
|
128
|
+
if (field.array)
|
|
129
|
+
return jsonArray(field, overrides, indent, mode, path, depth);
|
|
130
|
+
if (field.kind === FieldKind.OBJECT)
|
|
131
|
+
return jsonObjectField(field, overrides, indent, mode, path, depth);
|
|
132
|
+
return jsonLeaf(field, overrides, mode);
|
|
133
|
+
}
|
|
134
|
+
function jsonLeaf(field, overrides, mode) {
|
|
135
|
+
if (mode === "inline")
|
|
136
|
+
return `"${escapeJson(inlineContent(field, overrides))}"`;
|
|
137
|
+
const value = exampleValue(field, overrides);
|
|
138
|
+
return isNumericOrBoolean(field.kind, value) ? value : `"${escapeJson(value)}"`;
|
|
139
|
+
}
|
|
140
|
+
function canExpand(field, path, depth) {
|
|
141
|
+
return field.kind === FieldKind.OBJECT && field.nested != null
|
|
142
|
+
&& depth < MAX_NEST_DEPTH && !path.has(field.nested);
|
|
143
|
+
}
|
|
144
|
+
function jsonObjectField(field, overrides, indent, mode, path, depth) {
|
|
145
|
+
if (!canExpand(field, path, depth))
|
|
146
|
+
return jsonLeaf(field, overrides, mode);
|
|
147
|
+
const nested = field.nested;
|
|
148
|
+
path.add(nested);
|
|
149
|
+
const out = jsonObject(nested, overrides, indent, mode, path, depth + 1);
|
|
150
|
+
path.delete(nested);
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
function jsonArray(field, overrides, indent, mode, path, depth) {
|
|
154
|
+
const elemIndent = indent + INDENT;
|
|
155
|
+
let elem;
|
|
156
|
+
if (canExpand(field, path, depth)) {
|
|
157
|
+
const nested = field.nested;
|
|
158
|
+
path.add(nested);
|
|
159
|
+
elem = jsonObject(nested, overrides, elemIndent, mode, path, depth + 1);
|
|
160
|
+
path.delete(nested);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
elem = jsonLeaf(field, overrides, mode);
|
|
164
|
+
}
|
|
165
|
+
return `[\n${elemIndent}${elem}\n${indent}]`;
|
|
166
|
+
}
|
|
167
|
+
// ---- XML skeleton (recursive) ----------------------------------------------
|
|
168
|
+
function renderXmlSkeleton(spec, overrides, mode) {
|
|
169
|
+
return `<${spec.rootName}>\n${xmlBody(spec, overrides, INDENT, mode, new Set([spec]), 0)}</${spec.rootName}>`;
|
|
170
|
+
}
|
|
171
|
+
function xmlBody(spec, overrides, indent, mode, path, depth) {
|
|
172
|
+
return spec.fields.map((field) => xmlField(field, overrides, indent, mode, path, depth)).join("");
|
|
173
|
+
}
|
|
174
|
+
function xmlField(field, overrides, indent, mode, path, depth) {
|
|
175
|
+
if (canExpand(field, path, depth)) {
|
|
176
|
+
const nested = field.nested;
|
|
177
|
+
path.add(nested);
|
|
178
|
+
const body = xmlBody(nested, overrides, indent + INDENT, mode, path, depth + 1);
|
|
179
|
+
path.delete(nested);
|
|
180
|
+
return `${indent}<${field.name}>\n${body}${indent}</${field.name}>\n`;
|
|
181
|
+
}
|
|
182
|
+
const content = mode === "inline" ? inlineContent(field, overrides) : exampleValue(field, overrides);
|
|
183
|
+
return `${indent}<${field.name}>${escapeXml(content)}</${field.name}>\n`;
|
|
130
184
|
}
|
|
131
185
|
function exampleValueIfDeclared(field, overrides) {
|
|
132
186
|
const ov = overrides.examples?.[field.name];
|
|
@@ -155,7 +209,7 @@ function isNumericOrBoolean(kind, value) {
|
|
|
155
209
|
// Finite-only: NaN/Infinity fall through to a quoted string so the emitted JSON
|
|
156
210
|
// stays valid. Number("") is 0, so guard the empty/blank case explicitly. Reject
|
|
157
211
|
// JS-only radix literals (0x../0b../0o..) that Number() accepts but Java/C# don't —
|
|
158
|
-
// same guard as the
|
|
212
|
+
// same guard as the extract engine's parseFiniteNumber (keeps the JSON valid + parity).
|
|
159
213
|
const t = value.trim();
|
|
160
214
|
if (t === "" || /^[+-]?0[xXbBoO]/.test(t))
|
|
161
215
|
return false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output-format-renderer.js","sourceRoot":"","sources":["../../src/prompt/output-format-renderer.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,EAAE;AACF,gFAAgF;AAChF,+EAA+E;AAC/E,iFAAiF;AACjF,8DAA8D;AAC9D,EAAE;AACF,kFAAkF;AAClF,gFAAgF;AAChF,sEAAsE;AAEtE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAIxD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,MAAM,aAAa,GAA2B,IAAI,GAAG,CAAY;IAC/D,SAAS,CAAC,GAAG;IACb,SAAS,CAAC,IAAI;IACd,SAAS,CAAC,MAAM;IAChB,SAAS,CAAC,OAAO;CAClB,CAAC,CAAC;AAEH,kFAAkF;AAClF,2BAA2B;AAC3B,MAAM,SAAS,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AACzD,MAAM,UAAU,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAE3D;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAsB,EAAE,SAA0B;IACnF,MAAM,cAAc,GAAG,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC;IACrD,QAAQ,cAAc,EAAE,CAAC;QACvB,KAAK,WAAW,CAAC,YAAY;YAC3B,OAAO,iBAAiB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QAC5C,KAAK,WAAW,CAAC,MAAM;YACrB,OAAO,YAAY,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QACvC;YACE,OAAO,WAAW,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACxC,CAAC;AACH,CAAC;AAED,+EAA+E;AAE/E,SAAS,YAAY,CAAC,IAAsB,EAAE,SAA0B;IACtE,OAAO,IAAI,CAAC,MAAM,KAAK,MAAM,CAAC,GAAG;QAC/B,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"output-format-renderer.js","sourceRoot":"","sources":["../../src/prompt/output-format-renderer.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metaobjectsdev/render",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0-rc.1",
|
|
4
4
|
"description": "Logic-less, deterministic text render engine (Mustache) for MetaObjects templates — provider-resolved partials, format-driven escaping, zero core dependency.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# FR-010 TypeScript extract engine — known gaps & intentional cross-port divergences
|
|
2
|
+
|
|
3
|
+
Scope: the tolerant `extract` pipeline (`src/extract/`). The Java engine
|
|
4
|
+
(`server/java/render/.../extract/`) is the cross-port reference; `fixtures/extract-conformance/`
|
|
5
|
+
is the oracle. All 10 corpus cases pass.
|
|
6
|
+
|
|
7
|
+
## Additive capability (TS + C#, beyond Java/Kotlin)
|
|
8
|
+
|
|
9
|
+
- **Nested-object extract is implemented.** A `FieldSpec` with a non-null `nested` schema
|
|
10
|
+
(built via the `object(...)` factory) is descended into and its sub-fields classified. The
|
|
11
|
+
Java/Kotlin ports defer this (their codegen emits a scalar-STRING placeholder). The C# port
|
|
12
|
+
also carries the OBJECT branch, so TS and C# agree. This is **dormant** under both the
|
|
13
|
+
conformance corpus (no nested fixture; the runner's schema parser never sets `nested`) and the
|
|
14
|
+
FR-010 codegen (Phase 3 emits the scalar placeholder for cross-port parity), so it changes no
|
|
15
|
+
shared-corpus result. If a future shared fixture adds a nested case, Java/Kotlin catch up.
|
|
16
|
+
|
|
17
|
+
## Intentional, documented divergence (NOT a bug)
|
|
18
|
+
|
|
19
|
+
The cross-port contract pins *classification + canonical value* (numbers within ±1e-9), not
|
|
20
|
+
byte-identical native parsing.
|
|
21
|
+
|
|
22
|
+
- **Java-style numeric suffixes / hex-float literals.** Java's `Double.parseDouble` accepts
|
|
23
|
+
`"42d"` / `"42f"` and hex-float forms (→ EXTRACTED); TS uses `Number(...)` + `Number.isFinite`,
|
|
24
|
+
which rejects them → **MALFORMED** (same accepted divergence the C# port records). The
|
|
25
|
+
load-bearing behavior — finite-only acceptance, `NaN`/`±Infinity` → MALFORMED — is identical.
|
|
26
|
+
|
|
27
|
+
- **JS-only radix-prefixed literals are GUARDED for parity.** `Number("0x10")` is `16` in JS, but
|
|
28
|
+
Java/C# reject `0x..`/`0b..`/`0o..` → MALFORMED. `parseFiniteNumber` rejects these prefixes
|
|
29
|
+
explicitly so TS matches Java/C# (→ MALFORMED) rather than over-accepting. (Not a divergence —
|
|
30
|
+
noted here because the guard exists precisely to prevent one.)
|
|
31
|
+
|
|
32
|
+
## Bounded deferral (parity with all ports)
|
|
33
|
+
|
|
34
|
+
- Array-of-enum is not specialized (a scalar array extracts via `asStringList`).
|
|
35
|
+
- `asInt`/`asLong` both return `number | null` (JS has one number type) and truncate toward zero.
|
|
36
|
+
|
|
37
|
+
## FR-011 extract hardening — current state
|
|
38
|
+
|
|
39
|
+
- **Enum coercion pipeline.** Enum extraction runs a fixed ladder: exact → normalize
|
|
40
|
+
(`@normalize` mode `none | collapse | strip`, default `strip`, per-field with an
|
|
41
|
+
`object.value`-level default) → `@enumAlias` → `@coerceDefault` → MALFORMED. `@default` fills
|
|
42
|
+
an absent enum (→ `DEFAULTED`, which satisfies `@required`); the `DEFAULTED` classification is
|
|
43
|
+
now emitted by the engine.
|
|
44
|
+
- **Nested/embedded-object extraction is supported** uniformly at the engine level (dotted child
|
|
45
|
+
paths, element-wise arrays) — this closes the FR-010 nested deferral noted above for the
|
|
46
|
+
*engine*. NOTE: the codegen schema-emitters still emit a scalar-STRING placeholder for nested
|
|
47
|
+
object fields (a deliberate, cross-port-consistent codegen deferral), so nested extraction is
|
|
48
|
+
reachable via a hand-built / engine-level schema but is not yet auto-emitted by codegen.
|
|
49
|
+
- **Fuzzy matching is deliberately DEFERRED.** A reserved no-op slot exists in the pipeline
|
|
50
|
+
(between `@enumAlias` and `@coerceDefault`). If added later it must be guarded integer
|
|
51
|
+
Levenshtein — never float / Jaro-Winkler — to preserve cross-port determinism.
|
|
52
|
+
- **`@normalize` `unicode` mode is intentionally NOT offered.** Normalization is ASCII-only (enum
|
|
53
|
+
members are ASCII identifiers), so it is byte-identical cross-port. A full Unicode / NFKC_Casefold
|
|
54
|
+
mode was rejected: cross-port byte-identity can't be guaranteed.
|
|
55
|
+
- **Known cross-port caveat (out of corpus).** The pre-normalization `trim` / `strip` step uses
|
|
56
|
+
each language's native trim, which differs on *non-ASCII* leading/trailing whitespace under
|
|
57
|
+
`collapse` mode — TS strips Unicode whitespace, Java trims only ≤U+0020. Unreachable via the
|
|
58
|
+
ASCII-only conformance corpus and irrelevant under `strip` / `none` modes; enum members and
|
|
59
|
+
typical LLM whitespace are ASCII. Documented for completeness.
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Stage 7: canonicalize a raw scalar string per its FieldSpec. Returns the MALFORMED
|
|
2
|
+
// sentinel when present-but-uncoercible. Mirrors Java Coerce.
|
|
3
|
+
//
|
|
4
|
+
// Tier-2 divergence (documented, parity with the C# port's KNOWN_GAPS): JS has one
|
|
5
|
+
// number type. INT/LONG both truncate toward zero via Math.trunc and return `number`.
|
|
6
|
+
// Coercion uses Number(...) + Number.isFinite, NOT Java's Double.parseDouble — so JS
|
|
7
|
+
// does NOT accept Java's numeric suffixes ("42d"/"42f") or hex-float literals. The
|
|
8
|
+
// load-bearing contract (finite-only acceptance; NaN/±Infinity → MALFORMED; numeric
|
|
9
|
+
// classification) is identical across ports.
|
|
10
|
+
|
|
11
|
+
import { FieldKind, Tolerance } from "./types.js";
|
|
12
|
+
import type { FieldSpec, ExtractOptions, ExtractionReport } from "./types.js";
|
|
13
|
+
import { normalizeEnum } from "./normalize.js";
|
|
14
|
+
import type { NormalizeMode } from "./normalize.js";
|
|
15
|
+
|
|
16
|
+
/** Sentinel: the value was present but could not be coerced to the declared kind/vocabulary. */
|
|
17
|
+
export const MALFORMED: unique symbol = Symbol("extract.coerce.MALFORMED");
|
|
18
|
+
|
|
19
|
+
export function coerceValue(
|
|
20
|
+
raw: string | null,
|
|
21
|
+
spec: FieldSpec,
|
|
22
|
+
opts: ExtractOptions,
|
|
23
|
+
fieldPath: string,
|
|
24
|
+
report: ExtractionReport,
|
|
25
|
+
): unknown | typeof MALFORMED {
|
|
26
|
+
if (raw == null) return MALFORMED;
|
|
27
|
+
|
|
28
|
+
if (opts.onField != null) {
|
|
29
|
+
const hooked = opts.onField(fieldPath, raw, spec);
|
|
30
|
+
if (hooked != null) {
|
|
31
|
+
report.addCoercion({ fieldPath, from: raw, to: stringify(hooked), kind: "onField" });
|
|
32
|
+
return hooked;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Per-field runtime normalizer (bounded 20% surface). Keyed by field path, then simple name.
|
|
37
|
+
const norm = opts.normalizers[fieldPath] ?? opts.normalizers[spec.name];
|
|
38
|
+
if (norm != null) {
|
|
39
|
+
const normalized = norm(raw);
|
|
40
|
+
if (normalized != null) {
|
|
41
|
+
report.addCoercion({ fieldPath, from: raw, to: stringify(normalized), kind: "normalizer" });
|
|
42
|
+
return normalized;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const ci = opts.tolerance !== Tolerance.STRICT;
|
|
47
|
+
switch (spec.kind) {
|
|
48
|
+
case FieldKind.ENUM:
|
|
49
|
+
return coerceEnum(raw, spec, opts, fieldPath, report, ci);
|
|
50
|
+
case FieldKind.INT:
|
|
51
|
+
case FieldKind.LONG:
|
|
52
|
+
return coerceInt(raw, spec, fieldPath, report);
|
|
53
|
+
case FieldKind.DOUBLE:
|
|
54
|
+
return coerceDouble(raw, spec, fieldPath, report);
|
|
55
|
+
case FieldKind.BOOLEAN:
|
|
56
|
+
return coerceBool(raw, ci);
|
|
57
|
+
default:
|
|
58
|
+
return raw;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Phase B (generalized `@default`): PURE coercion of a metadata-sourced string to the field's
|
|
64
|
+
* scalar kind, with NO side effects (no normalizer/onField hooks, no clamp logging) — the value
|
|
65
|
+
* originates from metadata, not the model response. Returns the coerced value or the MALFORMED
|
|
66
|
+
* sentinel. INT/LONG accept an integer or a truncatable finite number; DOUBLE accepts any finite
|
|
67
|
+
* number; BOOLEAN accepts `true|false|yes|no|1|0`; STRING (and any other kind) passes through
|
|
68
|
+
* verbatim. Mirrors Java `Coerce.scalar` (parse semantics of {@link coerceValue} without its
|
|
69
|
+
* range-clamp / report machinery).
|
|
70
|
+
*/
|
|
71
|
+
export function scalarCoerce(raw: string | null, spec: FieldSpec): unknown | typeof MALFORMED {
|
|
72
|
+
if (raw == null) return MALFORMED;
|
|
73
|
+
switch (spec.kind) {
|
|
74
|
+
case FieldKind.INT:
|
|
75
|
+
case FieldKind.LONG: {
|
|
76
|
+
const n = parseFiniteNumber(raw);
|
|
77
|
+
return n === null ? MALFORMED : Math.trunc(n);
|
|
78
|
+
}
|
|
79
|
+
case FieldKind.DOUBLE: {
|
|
80
|
+
const n = parseFiniteNumber(raw);
|
|
81
|
+
return n === null ? MALFORMED : n;
|
|
82
|
+
}
|
|
83
|
+
case FieldKind.BOOLEAN:
|
|
84
|
+
return coerceBool(raw, true);
|
|
85
|
+
default:
|
|
86
|
+
return raw; // STRING / ENUM / OBJECT — verbatim
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* FR-011 enum coercion pipeline: exact → normalize → @enumAlias → (reserved fuzzy) →
|
|
92
|
+
* @coerceDefault → MALFORMED. Resolution mode is `spec.normalize` (default "strip"); under
|
|
93
|
+
* STRICT tolerance (ci === false) normalization is forced to "none" (exact-only), preserving
|
|
94
|
+
* the case-sensitive STRICT contract. The FR-010 case-insensitive default is now mode "strip".
|
|
95
|
+
*/
|
|
96
|
+
function coerceEnum(
|
|
97
|
+
raw: string,
|
|
98
|
+
spec: FieldSpec,
|
|
99
|
+
opts: ExtractOptions,
|
|
100
|
+
path: string,
|
|
101
|
+
report: ExtractionReport,
|
|
102
|
+
ci: boolean,
|
|
103
|
+
): unknown | typeof MALFORMED {
|
|
104
|
+
const mode: NormalizeMode = ci ? spec.normalize : "none";
|
|
105
|
+
|
|
106
|
+
// 1. exact match.
|
|
107
|
+
if (spec.enumValues != null) {
|
|
108
|
+
for (const v of spec.enumValues) if (v === raw) return v;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 2. normalized match (skipped when mode === "none").
|
|
112
|
+
if (mode !== "none" && spec.enumValues != null) {
|
|
113
|
+
const normRaw = normalizeEnum(raw, mode);
|
|
114
|
+
for (const v of spec.enumValues) {
|
|
115
|
+
if (normalizeEnum(v, mode) === normRaw) {
|
|
116
|
+
report.addCoercion({ fieldPath: path, from: raw, to: v, kind: "normalize" });
|
|
117
|
+
return v;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 3. @enumAlias — runtime aliases win over schema; alias keys normalized by the mode.
|
|
123
|
+
const aliasTarget = lookupAlias(raw, spec, opts, mode);
|
|
124
|
+
if (aliasTarget != null) {
|
|
125
|
+
const schemaTarget = lookupAliasIn(raw, spec.enumAlias ?? {}, mode);
|
|
126
|
+
const kind =
|
|
127
|
+
aliasTarget.fromRuntime && schemaTarget != null && schemaTarget !== aliasTarget.target
|
|
128
|
+
? "runtime-alias-override"
|
|
129
|
+
: "alias";
|
|
130
|
+
report.addCoercion({ fieldPath: path, from: raw, to: aliasTarget.target, kind });
|
|
131
|
+
return aliasTarget.target;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 4. reserved fuzzy slot — NOT implemented (see FR-011 spec "Out of scope").
|
|
135
|
+
|
|
136
|
+
// 5. @coerceDefault — present-but-uncoercible fallback to a valid member → DEFAULTED.
|
|
137
|
+
if (spec.coerceDefault != null && spec.enumValues != null && spec.enumValues.includes(spec.coerceDefault)) {
|
|
138
|
+
report.addCoercion({ fieldPath: path, from: raw, to: spec.coerceDefault, kind: "coerceDefault" });
|
|
139
|
+
return spec.coerceDefault;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 6. MALFORMED.
|
|
143
|
+
return MALFORMED;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Resolve `raw` against the merged alias maps (runtime wins), comparing keys under `mode`.
|
|
148
|
+
* Returns the target member + whether the winning hit came from the runtime map.
|
|
149
|
+
*/
|
|
150
|
+
function lookupAlias(
|
|
151
|
+
raw: string,
|
|
152
|
+
spec: FieldSpec,
|
|
153
|
+
opts: ExtractOptions,
|
|
154
|
+
mode: NormalizeMode,
|
|
155
|
+
): { target: string; fromRuntime: boolean } | null {
|
|
156
|
+
const runtime = lookupAliasIn(raw, opts.aliases, mode);
|
|
157
|
+
if (runtime != null) return { target: runtime, fromRuntime: true };
|
|
158
|
+
const schema = lookupAliasIn(raw, spec.enumAlias ?? {}, mode);
|
|
159
|
+
if (schema != null) return { target: schema, fromRuntime: false };
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Find `raw` in an alias map, matching keys exactly first then under `mode` normalization. */
|
|
164
|
+
function lookupAliasIn(raw: string, aliases: Readonly<Record<string, string>>, mode: NormalizeMode): string | null {
|
|
165
|
+
if (Object.prototype.hasOwnProperty.call(aliases, raw)) return aliases[raw]!;
|
|
166
|
+
if (mode === "none") return null;
|
|
167
|
+
const normRaw = normalizeEnum(raw, mode);
|
|
168
|
+
for (const key of Object.keys(aliases)) {
|
|
169
|
+
if (normalizeEnum(key, mode) === normRaw) return aliases[key]!;
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function coerceInt(raw: string, spec: FieldSpec, path: string, report: ExtractionReport): unknown | typeof MALFORMED {
|
|
175
|
+
const n = parseFiniteNumber(raw);
|
|
176
|
+
if (n === null) return MALFORMED;
|
|
177
|
+
return clamp(Math.trunc(n), spec, path, report);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function coerceDouble(raw: string, spec: FieldSpec, path: string, report: ExtractionReport): unknown | typeof MALFORMED {
|
|
181
|
+
const n = parseFiniteNumber(raw);
|
|
182
|
+
if (n === null) return MALFORMED;
|
|
183
|
+
return clamp(n, spec, path, report);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Parse a trimmed numeric string; null if empty, non-numeric, or non-finite (NaN/±Infinity). */
|
|
187
|
+
function parseFiniteNumber(raw: string): number | null {
|
|
188
|
+
const t = raw.trim();
|
|
189
|
+
if (t.length === 0) return null;
|
|
190
|
+
// Reject JS-only radix-prefixed literals (0x.., 0b.., 0o..) that Number() would
|
|
191
|
+
// accept but Java/C# numeric parsing rejects → MALFORMED. Keeps cross-port parity.
|
|
192
|
+
if (/^[+-]?0[xXbBoO]/.test(t)) return null;
|
|
193
|
+
const n = Number(t); // Number("") === 0, hence the empty guard above
|
|
194
|
+
return Number.isFinite(n) ? n : null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function clamp(n: number, spec: FieldSpec, path: string, report: ExtractionReport): number {
|
|
198
|
+
let c = n;
|
|
199
|
+
if (spec.min != null && c < spec.min) c = spec.min;
|
|
200
|
+
if (spec.max != null && c > spec.max) c = spec.max;
|
|
201
|
+
if (c !== n) report.addCoercion({ fieldPath: path, from: stringify(n), to: stringify(c), kind: "clamp" });
|
|
202
|
+
return c;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function coerceBool(raw: string, ci: boolean): boolean | typeof MALFORMED {
|
|
206
|
+
const t = ci ? raw.trim().toLowerCase() : raw.trim();
|
|
207
|
+
switch (t) {
|
|
208
|
+
case "true":
|
|
209
|
+
case "yes":
|
|
210
|
+
case "1":
|
|
211
|
+
return true;
|
|
212
|
+
case "false":
|
|
213
|
+
case "no":
|
|
214
|
+
case "0":
|
|
215
|
+
return false;
|
|
216
|
+
default:
|
|
217
|
+
return MALFORMED;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Canonical string form (locale-independent), mirroring Java String.valueOf for the corpus. */
|
|
222
|
+
function stringify(v: unknown): string {
|
|
223
|
+
return String(v);
|
|
224
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Null-safe coercions from a
|
|
2
|
-
//
|
|
1
|
+
// Null-safe coercions from a ExtractionOutcome data map onto typed values. Generated
|
|
2
|
+
// extract(...) calls these. Mirrors Java ExtractMap.
|
|
3
3
|
//
|
|
4
4
|
// Tier-2 divergence: JS has one number type, so asInt/asLong both return `number | null`
|
|
5
5
|
// and truncate toward zero via Math.trunc (Java intValue()/longValue() also truncate).
|