@metaobjectsdev/render 0.8.1 → 0.9.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.
Files changed (81) hide show
  1. package/dist/email-document.d.ts +7 -0
  2. package/dist/email-document.d.ts.map +1 -0
  3. package/dist/email-document.js +2 -0
  4. package/dist/email-document.js.map +1 -0
  5. package/dist/extract/coerce.d.ts +15 -0
  6. package/dist/extract/coerce.d.ts.map +1 -0
  7. package/dist/{recover → extract}/coerce.js +87 -13
  8. package/dist/extract/coerce.js.map +1 -0
  9. package/dist/{recover/recover-map.d.ts → extract/extract-map.d.ts} +1 -1
  10. package/dist/{recover/recover-map.d.ts.map → extract/extract-map.d.ts.map} +1 -1
  11. package/dist/{recover/recover-map.js → extract/extract-map.js} +3 -3
  12. package/dist/{recover/recover-map.js.map → extract/extract-map.js.map} +1 -1
  13. package/dist/extract/extract.d.ts +4 -0
  14. package/dist/extract/extract.d.ts.map +1 -0
  15. package/dist/extract/extract.js +157 -0
  16. package/dist/extract/extract.js.map +1 -0
  17. package/dist/{recover → extract}/json-forgiving-reader.d.ts.map +1 -1
  18. package/dist/{recover → extract}/json-forgiving-reader.js +1 -1
  19. package/dist/{recover → extract}/json-forgiving-reader.js.map +1 -1
  20. package/dist/{recover → extract}/locate.d.ts.map +1 -1
  21. package/dist/{recover → extract}/locate.js.map +1 -1
  22. package/dist/extract/normalize.d.ts +4 -0
  23. package/dist/extract/normalize.d.ts.map +1 -0
  24. package/dist/extract/normalize.js +22 -0
  25. package/dist/extract/normalize.js.map +1 -0
  26. package/dist/extract/strip.d.ts.map +1 -0
  27. package/dist/{recover → extract}/strip.js.map +1 -1
  28. package/dist/extract/types.d.ts +160 -0
  29. package/dist/extract/types.d.ts.map +1 -0
  30. package/dist/extract/types.js +221 -0
  31. package/dist/extract/types.js.map +1 -0
  32. package/dist/{recover → extract}/xml-forgiving-reader.d.ts.map +1 -1
  33. package/dist/{recover → extract}/xml-forgiving-reader.js +1 -1
  34. package/dist/{recover → extract}/xml-forgiving-reader.js.map +1 -1
  35. package/dist/index.d.ts +4 -3
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +4 -4
  38. package/dist/index.js.map +1 -1
  39. package/dist/prompt/output-format-renderer.d.ts.map +1 -1
  40. package/dist/prompt/output-format-renderer.js +113 -59
  41. package/dist/prompt/output-format-renderer.js.map +1 -1
  42. package/dist/prompt/output-format-spec.d.ts +1 -1
  43. package/dist/prompt/prompt-field.d.ts +1 -1
  44. package/package.json +1 -1
  45. package/src/email-document.ts +6 -0
  46. package/src/extract/KNOWN_GAPS.md +59 -0
  47. package/src/extract/coerce.ts +224 -0
  48. package/src/{recover/recover-map.ts → extract/extract-map.ts} +2 -2
  49. package/src/extract/extract.ts +187 -0
  50. package/src/{recover → extract}/json-forgiving-reader.ts +1 -1
  51. package/src/extract/normalize.ts +23 -0
  52. package/src/extract/types.ts +346 -0
  53. package/src/{recover → extract}/xml-forgiving-reader.ts +1 -1
  54. package/src/index.ts +17 -11
  55. package/src/prompt/output-format-renderer.ts +140 -61
  56. package/src/prompt/output-format-spec.ts +1 -1
  57. package/src/prompt/prompt-field.ts +1 -1
  58. package/dist/recover/coerce.d.ts +0 -5
  59. package/dist/recover/coerce.d.ts.map +0 -1
  60. package/dist/recover/coerce.js.map +0 -1
  61. package/dist/recover/recover.d.ts +0 -4
  62. package/dist/recover/recover.d.ts.map +0 -1
  63. package/dist/recover/recover.js +0 -115
  64. package/dist/recover/recover.js.map +0 -1
  65. package/dist/recover/strip.d.ts.map +0 -1
  66. package/dist/recover/types.d.ts +0 -117
  67. package/dist/recover/types.d.ts.map +0 -1
  68. package/dist/recover/types.js +0 -124
  69. package/dist/recover/types.js.map +0 -1
  70. package/src/recover/KNOWN_GAPS.md +0 -35
  71. package/src/recover/coerce.ts +0 -141
  72. package/src/recover/recover.ts +0 -146
  73. package/src/recover/types.ts +0 -217
  74. /package/dist/{recover → extract}/json-forgiving-reader.d.ts +0 -0
  75. /package/dist/{recover → extract}/locate.d.ts +0 -0
  76. /package/dist/{recover → extract}/locate.js +0 -0
  77. /package/dist/{recover → extract}/strip.d.ts +0 -0
  78. /package/dist/{recover → extract}/strip.js +0 -0
  79. /package/dist/{recover → extract}/xml-forgiving-reader.d.ts +0 -0
  80. /package/src/{recover → extract}/locate.ts +0 -0
  81. /package/src/{recover → extract}/strip.ts +0 -0
@@ -1,124 +0,0 @@
1
- // FR-010 recover engine — types & model (Tier-2 idiomatic TS port).
2
- //
3
- // Cross-port REFERENCE is the Java engine
4
- // (server/java/render/.../recover/). This file ports the Java records/enums to
5
- // idiomatic TS: enums become string-union `as const` objects (values match the
6
- // corpus / Java enum names exactly), records become readonly interfaces +
7
- // factory functions, and the mutable RecoveryReport is a class.
8
- /** Output format the dirty text claims to be. Corpus schema.json uses "JSON"/"XML". */
9
- export const Format = {
10
- JSON: "JSON",
11
- XML: "XML",
12
- };
13
- /** The coercion target kinds the engine understands. OBJECT = nested RecoverSchema. */
14
- export const FieldKind = {
15
- STRING: "STRING",
16
- INT: "INT",
17
- LONG: "LONG",
18
- DOUBLE: "DOUBLE",
19
- BOOLEAN: "BOOLEAN",
20
- ENUM: "ENUM",
21
- OBJECT: "OBJECT",
22
- };
23
- /**
24
- * FROZEN cross-port per-field recovery classification. Do not reorder or add
25
- * without an ADR. These string values are SERIALIZED in the conformance corpus.
26
- */
27
- export const FieldRecovery = {
28
- RECOVERED: "RECOVERED",
29
- // DEFAULTED is reserved (a future @default-backed value); the engine does not emit it yet.
30
- DEFAULTED: "DEFAULTED",
31
- LOST_OPTIONAL: "LOST_OPTIONAL",
32
- LOST_REQUIRED: "LOST_REQUIRED",
33
- MALFORMED: "MALFORMED",
34
- };
35
- /**
36
- * STRICT: case-sensitive, minimal repair. NORMAL: case-insensitive keys/tags
37
- * (default). LOOSE: maximal repair (currently identical to NORMAL — reserved).
38
- */
39
- export const Tolerance = {
40
- STRICT: "STRICT",
41
- NORMAL: "NORMAL",
42
- LOOSE: "LOOSE",
43
- };
44
- export function scalar(name, kind, required) {
45
- return { name, kind, required, array: false, enumValues: null, enumAlias: null, min: null, max: null, nested: null };
46
- }
47
- export function enumField(name, required, values, aliases) {
48
- return {
49
- name,
50
- kind: FieldKind.ENUM,
51
- required,
52
- array: false,
53
- enumValues: values == null ? null : [...values],
54
- enumAlias: aliases == null ? {} : { ...aliases },
55
- min: null,
56
- max: null,
57
- nested: null,
58
- };
59
- }
60
- export function range(name, kind, required, min, max) {
61
- return { name, kind, required, array: false, enumValues: null, enumAlias: null, min, max, nested: null };
62
- }
63
- export function object(name, required, array, nested) {
64
- return { name, kind: FieldKind.OBJECT, required, array, enumValues: null, enumAlias: null, min: null, max: null, nested };
65
- }
66
- export function recoverSchema(format, rootName, fields) {
67
- return { format, rootName, fields: fields == null ? [] : [...fields] };
68
- }
69
- export function defaults() {
70
- return { tolerance: Tolerance.NORMAL, aliases: {}, normalizers: {}, onField: null };
71
- }
72
- /** Normalize a partial / undefined options bag into a complete RecoverOptions. */
73
- export function normalizeOptions(opts) {
74
- if (opts == null)
75
- return defaults();
76
- return {
77
- tolerance: opts.tolerance ?? Tolerance.NORMAL,
78
- aliases: opts.aliases == null ? {} : { ...opts.aliases },
79
- normalizers: opts.normalizers == null ? {} : { ...opts.normalizers },
80
- onField: opts.onField ?? null,
81
- };
82
- }
83
- /** Mutable accumulator of per-field recovery classification, the degenerate-response flag, and coercion notes. */
84
- export class RecoveryReport {
85
- // Insertion-ordered (Map preserves insertion order, mirroring Java LinkedHashMap).
86
- _states = new Map();
87
- _coercions = [];
88
- _empty = false;
89
- set(fieldPath, state) {
90
- this._states.set(fieldPath, state);
91
- }
92
- addCoercion(c) {
93
- this._coercions.push(c);
94
- }
95
- markEmpty() {
96
- this._empty = true;
97
- }
98
- isEmpty() {
99
- return this._empty;
100
- }
101
- states() {
102
- return new Map(this._states);
103
- }
104
- coercions() {
105
- return [...this._coercions];
106
- }
107
- lostRequired() {
108
- return this.byState(FieldRecovery.LOST_REQUIRED);
109
- }
110
- malformed() {
111
- return this.byState(FieldRecovery.MALFORMED);
112
- }
113
- hasLostRequired() {
114
- return this.lostRequired().length > 0;
115
- }
116
- byState(s) {
117
- const out = [];
118
- for (const [k, v] of this._states)
119
- if (v === s)
120
- out.push(k);
121
- return out;
122
- }
123
- }
124
- //# sourceMappingURL=types.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/recover/types.ts"],"names":[],"mappings":"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"}
@@ -1,35 +0,0 @@
1
- # FR-010 TypeScript recover engine — known gaps & intentional cross-port divergences
2
-
3
- Scope: the tolerant `recover` pipeline (`src/recover/`). The Java engine
4
- (`server/java/render/.../recover/`) is the cross-port reference; `fixtures/recover-conformance/`
5
- is the oracle. All 10 corpus cases pass.
6
-
7
- ## Additive capability (TS + C#, beyond Java/Kotlin)
8
-
9
- - **Nested-object recover 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 (→ RECOVERED); 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 recovers via `asStringList`).
35
- - `asInt`/`asLong` both return `number | null` (JS has one number type) and truncate toward zero.
@@ -1,141 +0,0 @@
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, RecoverOptions, RecoveryReport } from "./types.js";
13
-
14
- /** Sentinel: the value was present but could not be coerced to the declared kind/vocabulary. */
15
- export const MALFORMED: unique symbol = Symbol("recover.coerce.MALFORMED");
16
-
17
- export function coerceValue(
18
- raw: string | null,
19
- spec: FieldSpec,
20
- opts: RecoverOptions,
21
- fieldPath: string,
22
- report: RecoveryReport,
23
- ): unknown | typeof MALFORMED {
24
- if (raw == null) return MALFORMED;
25
-
26
- if (opts.onField != null) {
27
- const hooked = opts.onField(fieldPath, raw, spec);
28
- if (hooked != null) {
29
- report.addCoercion({ fieldPath, from: raw, to: stringify(hooked), kind: "onField" });
30
- return hooked;
31
- }
32
- }
33
-
34
- // Per-field runtime normalizer (bounded 20% surface). Keyed by field path, then simple name.
35
- const norm = opts.normalizers[fieldPath] ?? opts.normalizers[spec.name];
36
- if (norm != null) {
37
- const normalized = norm(raw);
38
- if (normalized != null) {
39
- report.addCoercion({ fieldPath, from: raw, to: stringify(normalized), kind: "normalizer" });
40
- return normalized;
41
- }
42
- }
43
-
44
- const ci = opts.tolerance !== Tolerance.STRICT;
45
- switch (spec.kind) {
46
- case FieldKind.ENUM:
47
- return coerceEnum(raw, spec, opts, fieldPath, report, ci);
48
- case FieldKind.INT:
49
- case FieldKind.LONG:
50
- return coerceInt(raw, spec, fieldPath, report);
51
- case FieldKind.DOUBLE:
52
- return coerceDouble(raw, spec, fieldPath, report);
53
- case FieldKind.BOOLEAN:
54
- return coerceBool(raw, ci);
55
- default:
56
- return raw;
57
- }
58
- }
59
-
60
- function coerceEnum(
61
- raw: string,
62
- spec: FieldSpec,
63
- opts: RecoverOptions,
64
- path: string,
65
- report: RecoveryReport,
66
- ci: boolean,
67
- ): unknown | typeof MALFORMED {
68
- if (spec.enumValues != null) {
69
- for (const v of spec.enumValues) {
70
- if (v === raw) return v;
71
- if (ci && v.toLowerCase() === raw.toLowerCase()) {
72
- report.addCoercion({ fieldPath: path, from: raw, to: v, kind: "case" });
73
- return v;
74
- }
75
- }
76
- }
77
- const schemaTarget = spec.enumAlias == null ? undefined : spec.enumAlias[raw];
78
- const runtimeTarget = opts.aliases[raw];
79
- if (runtimeTarget != null) {
80
- const kind = schemaTarget != null && schemaTarget !== runtimeTarget ? "runtime-alias-override" : "alias";
81
- report.addCoercion({ fieldPath: path, from: raw, to: runtimeTarget, kind });
82
- return runtimeTarget;
83
- }
84
- if (schemaTarget != null) {
85
- report.addCoercion({ fieldPath: path, from: raw, to: schemaTarget, kind: "alias" });
86
- return schemaTarget;
87
- }
88
- return MALFORMED;
89
- }
90
-
91
- function coerceInt(raw: string, spec: FieldSpec, path: string, report: RecoveryReport): unknown | typeof MALFORMED {
92
- const n = parseFiniteNumber(raw);
93
- if (n === null) return MALFORMED;
94
- return clamp(Math.trunc(n), spec, path, report);
95
- }
96
-
97
- function coerceDouble(raw: string, spec: FieldSpec, path: string, report: RecoveryReport): unknown | typeof MALFORMED {
98
- const n = parseFiniteNumber(raw);
99
- if (n === null) return MALFORMED;
100
- return clamp(n, spec, path, report);
101
- }
102
-
103
- /** Parse a trimmed numeric string; null if empty, non-numeric, or non-finite (NaN/±Infinity). */
104
- function parseFiniteNumber(raw: string): number | null {
105
- const t = raw.trim();
106
- if (t.length === 0) return null;
107
- // Reject JS-only radix-prefixed literals (0x.., 0b.., 0o..) that Number() would
108
- // accept but Java/C# numeric parsing rejects → MALFORMED. Keeps cross-port parity.
109
- if (/^[+-]?0[xXbBoO]/.test(t)) return null;
110
- const n = Number(t); // Number("") === 0, hence the empty guard above
111
- return Number.isFinite(n) ? n : null;
112
- }
113
-
114
- function clamp(n: number, spec: FieldSpec, path: string, report: RecoveryReport): number {
115
- let c = n;
116
- if (spec.min != null && c < spec.min) c = spec.min;
117
- if (spec.max != null && c > spec.max) c = spec.max;
118
- if (c !== n) report.addCoercion({ fieldPath: path, from: stringify(n), to: stringify(c), kind: "clamp" });
119
- return c;
120
- }
121
-
122
- function coerceBool(raw: string, ci: boolean): boolean | typeof MALFORMED {
123
- const t = ci ? raw.trim().toLowerCase() : raw.trim();
124
- switch (t) {
125
- case "true":
126
- case "yes":
127
- case "1":
128
- return true;
129
- case "false":
130
- case "no":
131
- case "0":
132
- return false;
133
- default:
134
- return MALFORMED;
135
- }
136
- }
137
-
138
- /** Canonical string form (locale-independent), mirroring Java String.valueOf for the corpus. */
139
- function stringify(v: unknown): string {
140
- return String(v);
141
- }
@@ -1,146 +0,0 @@
1
- // Public entry point. Runs the staged pipeline; NEVER throws. Mirrors Java Recover.
2
-
3
- import {
4
- Format,
5
- FieldKind,
6
- FieldRecovery,
7
- Tolerance,
8
- normalizeOptions,
9
- } from "./types.js";
10
- import type { FieldSpec, RecoverOptions, RecoverOutcome, RecoverSchema } from "./types.js";
11
- import { RecoveryReport } from "./types.js";
12
- import { strip } from "./strip.js";
13
- import { locateJson, locateXml } from "./locate.js";
14
- import { readJson, TRUNCATED } from "./json-forgiving-reader.js";
15
- import { readXml } from "./xml-forgiving-reader.js";
16
- import { coerceValue, MALFORMED } from "./coerce.js";
17
-
18
- /** The forgiving entry point: recover dirty `text` against `schema`. Never throws. */
19
- export function recover(
20
- text: string | null | undefined,
21
- schema: RecoverSchema,
22
- opts?: Partial<RecoverOptions> | null,
23
- ): RecoverOutcome {
24
- const o = normalizeOptions(opts);
25
- const report = new RecoveryReport();
26
- const data: Record<string, unknown> = {};
27
-
28
- const stripped = strip(text);
29
- const ci = o.tolerance !== Tolerance.STRICT;
30
-
31
- const span =
32
- schema.format === Format.JSON ? locateJson(stripped) : locateXml(stripped, schema.rootName, ci);
33
-
34
- let raw: Record<string, unknown>;
35
- if (span == null) {
36
- raw = {};
37
- } else if (schema.format === Format.JSON) {
38
- raw = readJson(span);
39
- } else {
40
- raw = readXml(span, ci);
41
- }
42
-
43
- if (isEmptyRecord(raw) && (stripped.length === 0 || span == null)) {
44
- report.markEmpty();
45
- }
46
-
47
- extract(schema.fields, raw, "", data, report, o, ci);
48
- return { data, report };
49
- }
50
-
51
- function extract(
52
- fields: readonly FieldSpec[],
53
- raw: Record<string, unknown>,
54
- prefix: string,
55
- data: Record<string, unknown>,
56
- report: RecoveryReport,
57
- o: RecoverOptions,
58
- ci: boolean,
59
- ): void {
60
- for (const f of fields) {
61
- const path = prefix.length === 0 ? f.name : `${prefix}.${f.name}`;
62
- const present = lookup(raw, f.name, ci);
63
- if (present === undefined) {
64
- report.set(path, f.required ? FieldRecovery.LOST_REQUIRED : FieldRecovery.LOST_OPTIONAL);
65
- continue;
66
- }
67
- if (present === TRUNCATED) {
68
- // present-but-garbled (empty/cut-off value)
69
- report.set(path, FieldRecovery.MALFORMED);
70
- continue;
71
- }
72
- if (f.array) {
73
- // An array field: a single non-list value is treated as a one-element array
74
- // (e.g. a single repeated-XML tag). Each element is coerced/recursed independently.
75
- const elements: unknown[] = Array.isArray(present) ? present : [present];
76
- const out: unknown[] = [];
77
- let anyMalformed = false;
78
- for (let idx = 0; idx < elements.length; idx++) {
79
- const v = extractValue(f, elements[idx], `${path}[${idx}]`, report, o, ci);
80
- if (v === MALFORMED) anyMalformed = true;
81
- else out.push(v);
82
- }
83
- // Cross-port contract: a MALFORMED array still places its successfully-coerced
84
- // elements into data (partial recovery), UNLIKE a MALFORMED scalar which is absent.
85
- data[f.name] = out;
86
- report.set(path, anyMalformed ? FieldRecovery.MALFORMED : FieldRecovery.RECOVERED);
87
- continue;
88
- }
89
- if (Array.isArray(present)) {
90
- // a list where a singular value was expected
91
- report.set(path, FieldRecovery.MALFORMED);
92
- continue;
93
- }
94
- const v = extractValue(f, present, path, report, o, ci);
95
- if (v === MALFORMED) {
96
- report.set(path, FieldRecovery.MALFORMED);
97
- } else {
98
- data[f.name] = v;
99
- report.set(path, FieldRecovery.RECOVERED);
100
- }
101
- }
102
- }
103
-
104
- /** Coerce one (non-array) element: nested-object recursion or scalar coercion. Returns MALFORMED on failure. */
105
- function extractValue(
106
- f: FieldSpec,
107
- present: unknown,
108
- path: string,
109
- report: RecoveryReport,
110
- o: RecoverOptions,
111
- ci: boolean,
112
- ): unknown | typeof MALFORMED {
113
- if (f.kind === FieldKind.OBJECT) {
114
- if (f.nested != null && isPlainObject(present)) {
115
- const nestedData: Record<string, unknown> = {};
116
- extract(f.nested.fields, present as Record<string, unknown>, path, nestedData, report, o, ci);
117
- return nestedData;
118
- }
119
- return MALFORMED; // object expected but scalar/non-map present
120
- }
121
- const rawStr = typeof present === "string" ? present : stringifyScalar(present);
122
- return coerceValue(rawStr, f, o, path, report);
123
- }
124
-
125
- /** Case-folding lookup honoring tolerance. Returns `undefined` for absent (mirrors Java null). */
126
- function lookup(raw: Record<string, unknown>, name: string, ci: boolean): unknown {
127
- if (Object.prototype.hasOwnProperty.call(raw, name)) return raw[name];
128
- if (ci) {
129
- const lower = name.toLowerCase();
130
- for (const k of Object.keys(raw)) if (k.toLowerCase() === lower) return raw[k];
131
- }
132
- return undefined;
133
- }
134
-
135
- function isPlainObject(o: unknown): boolean {
136
- return typeof o === "object" && o !== null && !Array.isArray(o);
137
- }
138
-
139
- function isEmptyRecord(o: Record<string, unknown>): boolean {
140
- return Object.keys(o).length === 0;
141
- }
142
-
143
- /** Mirror Java String.valueOf for non-string forgiving-reader scalars. */
144
- function stringifyScalar(v: unknown): string {
145
- return String(v);
146
- }
@@ -1,217 +0,0 @@
1
- // FR-010 recover engine — types & model (Tier-2 idiomatic TS port).
2
- //
3
- // Cross-port REFERENCE is the Java engine
4
- // (server/java/render/.../recover/). This file ports the Java records/enums to
5
- // idiomatic TS: enums become string-union `as const` objects (values match the
6
- // corpus / Java enum names exactly), records become readonly interfaces +
7
- // factory functions, and the mutable RecoveryReport is a class.
8
-
9
- /** Output format the dirty text claims to be. Corpus schema.json uses "JSON"/"XML". */
10
- export const Format = {
11
- JSON: "JSON",
12
- XML: "XML",
13
- } as const;
14
- export type Format = (typeof Format)[keyof typeof Format];
15
-
16
- /** The coercion target kinds the engine understands. OBJECT = nested RecoverSchema. */
17
- export const FieldKind = {
18
- STRING: "STRING",
19
- INT: "INT",
20
- LONG: "LONG",
21
- DOUBLE: "DOUBLE",
22
- BOOLEAN: "BOOLEAN",
23
- ENUM: "ENUM",
24
- OBJECT: "OBJECT",
25
- } as const;
26
- export type FieldKind = (typeof FieldKind)[keyof typeof FieldKind];
27
-
28
- /**
29
- * FROZEN cross-port per-field recovery classification. Do not reorder or add
30
- * without an ADR. These string values are SERIALIZED in the conformance corpus.
31
- */
32
- export const FieldRecovery = {
33
- RECOVERED: "RECOVERED",
34
- // DEFAULTED is reserved (a future @default-backed value); the engine does not emit it yet.
35
- DEFAULTED: "DEFAULTED",
36
- LOST_OPTIONAL: "LOST_OPTIONAL",
37
- LOST_REQUIRED: "LOST_REQUIRED",
38
- MALFORMED: "MALFORMED",
39
- } as const;
40
- export type FieldRecovery = (typeof FieldRecovery)[keyof typeof FieldRecovery];
41
-
42
- /**
43
- * STRICT: case-sensitive, minimal repair. NORMAL: case-insensitive keys/tags
44
- * (default). LOOSE: maximal repair (currently identical to NORMAL — reserved).
45
- */
46
- export const Tolerance = {
47
- STRICT: "STRICT",
48
- NORMAL: "NORMAL",
49
- LOOSE: "LOOSE",
50
- } as const;
51
- export type Tolerance = (typeof Tolerance)[keyof typeof Tolerance];
52
-
53
- /** A recorded normalization/coercion. kind e.g. "alias", "clamp", "case", "runtime-alias-override". */
54
- export interface Coercion {
55
- readonly fieldPath: string;
56
- readonly from: string;
57
- readonly to: string;
58
- readonly kind: string;
59
- }
60
-
61
- /**
62
- * One field's recover descriptor. enumValues/enumAlias non-null only for ENUM;
63
- * min/max non-null only for numeric range constraints; nested non-null only for OBJECT.
64
- */
65
- export interface FieldSpec {
66
- readonly name: string;
67
- readonly kind: FieldKind;
68
- readonly required: boolean;
69
- readonly array: boolean;
70
- readonly enumValues: readonly string[] | null;
71
- readonly enumAlias: Readonly<Record<string, string>> | null;
72
- readonly min: number | null;
73
- readonly max: number | null;
74
- readonly nested: RecoverSchema | null;
75
- }
76
-
77
- export function scalar(name: string, kind: FieldKind, required: boolean): FieldSpec {
78
- return { name, kind, required, array: false, enumValues: null, enumAlias: null, min: null, max: null, nested: null };
79
- }
80
-
81
- export function enumField(
82
- name: string,
83
- required: boolean,
84
- values: readonly string[] | null,
85
- aliases: Readonly<Record<string, string>> | null,
86
- ): FieldSpec {
87
- return {
88
- name,
89
- kind: FieldKind.ENUM,
90
- required,
91
- array: false,
92
- enumValues: values == null ? null : [...values],
93
- enumAlias: aliases == null ? {} : { ...aliases },
94
- min: null,
95
- max: null,
96
- nested: null,
97
- };
98
- }
99
-
100
- export function range(
101
- name: string,
102
- kind: FieldKind,
103
- required: boolean,
104
- min: number | null,
105
- max: number | null,
106
- ): FieldSpec {
107
- return { name, kind, required, array: false, enumValues: null, enumAlias: null, min, max, nested: null };
108
- }
109
-
110
- export function object(name: string, required: boolean, array: boolean, nested: RecoverSchema | null): FieldSpec {
111
- return { name, kind: FieldKind.OBJECT, required, array, enumValues: null, enumAlias: null, min: null, max: null, nested };
112
- }
113
-
114
- /** Top-level recover descriptor. rootName = the XML root tag / logical JSON root name. */
115
- export interface RecoverSchema {
116
- readonly format: Format;
117
- readonly rootName: string;
118
- readonly fields: readonly FieldSpec[];
119
- }
120
-
121
- export function recoverSchema(format: Format, rootName: string, fields: readonly FieldSpec[] | null): RecoverSchema {
122
- return { format, rootName, fields: fields == null ? [] : [...fields] };
123
- }
124
-
125
- /**
126
- * ctx carries the field path and the FieldSpec; return null to fall through to
127
- * default coercion. The single bespoke-coercion hook (the bounded "20%").
128
- */
129
- export type OnField = (fieldPath: string, rawValue: string, spec: FieldSpec) => unknown | null;
130
-
131
- /**
132
- * Bounded runtime override surface. aliases/normalizers are MERGED with the
133
- * schema's, runtime winning on key conflict. onField is the single hook.
134
- */
135
- export interface RecoverOptions {
136
- readonly tolerance: Tolerance;
137
- readonly aliases: Readonly<Record<string, string>>;
138
- readonly normalizers: Readonly<Record<string, (raw: string) => unknown | null>>;
139
- readonly onField: OnField | null;
140
- }
141
-
142
- export function defaults(): RecoverOptions {
143
- return { tolerance: Tolerance.NORMAL, aliases: {}, normalizers: {}, onField: null };
144
- }
145
-
146
- /** Normalize a partial / undefined options bag into a complete RecoverOptions. */
147
- export function normalizeOptions(opts?: Partial<RecoverOptions> | null): RecoverOptions {
148
- if (opts == null) return defaults();
149
- return {
150
- tolerance: opts.tolerance ?? Tolerance.NORMAL,
151
- aliases: opts.aliases == null ? {} : { ...opts.aliases },
152
- normalizers: opts.normalizers == null ? {} : { ...opts.normalizers },
153
- onField: opts.onField ?? null,
154
- };
155
- }
156
-
157
- /** Engine return. data is a forgiving record; Phase-2 codegen wraps it into a typed RecoveryResult<T>. */
158
- export interface RecoverOutcome {
159
- readonly data: Record<string, unknown>;
160
- readonly report: RecoveryReport;
161
- }
162
-
163
- /** Typed result of a generated recover(...): best-effort value (null where lost/malformed) + report. */
164
- export interface RecoveryResult<T> {
165
- readonly data: T | null;
166
- readonly report: RecoveryReport;
167
- }
168
-
169
- /** Mutable accumulator of per-field recovery classification, the degenerate-response flag, and coercion notes. */
170
- export class RecoveryReport {
171
- // Insertion-ordered (Map preserves insertion order, mirroring Java LinkedHashMap).
172
- private readonly _states = new Map<string, FieldRecovery>();
173
- private readonly _coercions: Coercion[] = [];
174
- private _empty = false;
175
-
176
- set(fieldPath: string, state: FieldRecovery): void {
177
- this._states.set(fieldPath, state);
178
- }
179
-
180
- addCoercion(c: Coercion): void {
181
- this._coercions.push(c);
182
- }
183
-
184
- markEmpty(): void {
185
- this._empty = true;
186
- }
187
-
188
- isEmpty(): boolean {
189
- return this._empty;
190
- }
191
-
192
- states(): ReadonlyMap<string, FieldRecovery> {
193
- return new Map(this._states);
194
- }
195
-
196
- coercions(): readonly Coercion[] {
197
- return [...this._coercions];
198
- }
199
-
200
- lostRequired(): string[] {
201
- return this.byState(FieldRecovery.LOST_REQUIRED);
202
- }
203
-
204
- malformed(): string[] {
205
- return this.byState(FieldRecovery.MALFORMED);
206
- }
207
-
208
- hasLostRequired(): boolean {
209
- return this.lostRequired().length > 0;
210
- }
211
-
212
- private byState(s: FieldRecovery): string[] {
213
- const out: string[] = [];
214
- for (const [k, v] of this._states) if (v === s) out.push(k);
215
- return out;
216
- }
217
- }
File without changes
File without changes
File without changes
File without changes
File without changes