@metaobjectsdev/render 0.7.0 → 0.8.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 (72) hide show
  1. package/dist/index.d.ts +8 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +8 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/prompt/output-format-renderer.d.ts +8 -0
  6. package/dist/prompt/output-format-renderer.d.ts.map +1 -0
  7. package/dist/prompt/output-format-renderer.js +164 -0
  8. package/dist/prompt/output-format-renderer.js.map +1 -0
  9. package/dist/prompt/output-format-spec.d.ts +18 -0
  10. package/dist/prompt/output-format-spec.d.ts.map +1 -0
  11. package/dist/prompt/output-format-spec.js +3 -0
  12. package/dist/prompt/output-format-spec.js.map +1 -0
  13. package/dist/prompt/prompt-field.d.ts +22 -0
  14. package/dist/prompt/prompt-field.d.ts.map +1 -0
  15. package/dist/prompt/prompt-field.js +3 -0
  16. package/dist/prompt/prompt-field.js.map +1 -0
  17. package/dist/prompt/prompt-overrides.d.ts +16 -0
  18. package/dist/prompt/prompt-overrides.d.ts.map +1 -0
  19. package/dist/prompt/prompt-overrides.js +13 -0
  20. package/dist/prompt/prompt-overrides.js.map +1 -0
  21. package/dist/prompt/prompt-style.d.ts +17 -0
  22. package/dist/prompt/prompt-style.d.ts.map +1 -0
  23. package/dist/prompt/prompt-style.js +34 -0
  24. package/dist/prompt/prompt-style.js.map +1 -0
  25. package/dist/recover/coerce.d.ts +5 -0
  26. package/dist/recover/coerce.d.ts.map +1 -0
  27. package/dist/recover/coerce.js +124 -0
  28. package/dist/recover/coerce.js.map +1 -0
  29. package/dist/recover/json-forgiving-reader.d.ts +5 -0
  30. package/dist/recover/json-forgiving-reader.d.ts.map +1 -0
  31. package/dist/recover/json-forgiving-reader.js +178 -0
  32. package/dist/recover/json-forgiving-reader.js.map +1 -0
  33. package/dist/recover/locate.d.ts +5 -0
  34. package/dist/recover/locate.d.ts.map +1 -0
  35. package/dist/recover/locate.js +75 -0
  36. package/dist/recover/locate.js.map +1 -0
  37. package/dist/recover/recover-map.d.ts +7 -0
  38. package/dist/recover/recover-map.d.ts.map +1 -0
  39. package/dist/recover/recover-map.js +36 -0
  40. package/dist/recover/recover-map.js.map +1 -0
  41. package/dist/recover/recover.d.ts +4 -0
  42. package/dist/recover/recover.d.ts.map +1 -0
  43. package/dist/recover/recover.js +115 -0
  44. package/dist/recover/recover.js.map +1 -0
  45. package/dist/recover/strip.d.ts +2 -0
  46. package/dist/recover/strip.d.ts.map +1 -0
  47. package/dist/recover/strip.js +17 -0
  48. package/dist/recover/strip.js.map +1 -0
  49. package/dist/recover/types.d.ts +117 -0
  50. package/dist/recover/types.d.ts.map +1 -0
  51. package/dist/recover/types.js +124 -0
  52. package/dist/recover/types.js.map +1 -0
  53. package/dist/recover/xml-forgiving-reader.d.ts +2 -0
  54. package/dist/recover/xml-forgiving-reader.d.ts.map +1 -0
  55. package/dist/recover/xml-forgiving-reader.js +79 -0
  56. package/dist/recover/xml-forgiving-reader.js.map +1 -0
  57. package/package.json +1 -1
  58. package/src/index.ts +42 -0
  59. package/src/prompt/output-format-renderer.ts +179 -0
  60. package/src/prompt/output-format-spec.ts +20 -0
  61. package/src/prompt/prompt-field.ts +24 -0
  62. package/src/prompt/prompt-overrides.ts +27 -0
  63. package/src/prompt/prompt-style.ts +36 -0
  64. package/src/recover/KNOWN_GAPS.md +35 -0
  65. package/src/recover/coerce.ts +141 -0
  66. package/src/recover/json-forgiving-reader.ts +167 -0
  67. package/src/recover/locate.ts +72 -0
  68. package/src/recover/recover-map.ts +39 -0
  69. package/src/recover/recover.ts +146 -0
  70. package/src/recover/strip.ts +17 -0
  71. package/src/recover/types.ts +217 -0
  72. package/src/recover/xml-forgiving-reader.ts +82 -0
@@ -0,0 +1,217 @@
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
+ }
@@ -0,0 +1,82 @@
1
+ // Stage-4 tolerant XML reader for the bounded corpus malformation set. Never throws.
2
+ // Mirrors Java XmlForgivingReader. Must not index-out-of-range on a leading close tag.
3
+
4
+ function quote(s: string): string {
5
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6
+ }
7
+
8
+ /** Find first regex match at index >= `from` (emulates Java Matcher.find(int)). */
9
+ function matchFrom(source: string, flags: string, text: string, from: number): RegExpExecArray | null {
10
+ const g = new RegExp(source, flags.includes("g") ? flags : flags + "g");
11
+ g.lastIndex = from;
12
+ return g.exec(text);
13
+ }
14
+
15
+ const OPEN_TAG_SRC = "<([A-Za-z_][A-Za-z0-9_]*)(\\s[^>]*)?>";
16
+
17
+ export function readXml(span: string | null | undefined, caseInsensitive: boolean): Record<string, unknown> {
18
+ const out: Record<string, unknown> = {};
19
+ if (span == null || span.trim().length === 0) return out;
20
+ const gt = span.indexOf(">");
21
+ if (gt < 0) return out;
22
+ const rootEnd = span.lastIndexOf("</");
23
+ const inner = span.substring(gt + 1, rootEnd < 0 || rootEnd <= gt ? span.length : rootEnd);
24
+ parseChildren(inner, caseInsensitive, out);
25
+ return out;
26
+ }
27
+
28
+ function parseChildren(inner: string, ci: boolean, out: Record<string, unknown>): void {
29
+ const flags = ci ? "i" : "";
30
+ let pos = 0;
31
+ for (;;) {
32
+ const m = matchFrom(OPEN_TAG_SRC, flags, inner, pos);
33
+ if (m == null) break;
34
+ const tag = m[1] ?? "";
35
+ const key = ci ? tag.toLowerCase() : tag;
36
+ const contentStart = m.index + m[0].length;
37
+
38
+ const closeRe = `</${quote(tag)}\\s*>`;
39
+ const close = matchFrom(closeRe, flags, inner, contentStart);
40
+
41
+ let contentEnd: number;
42
+ let next: number;
43
+ if (close != null) {
44
+ contentEnd = close.index;
45
+ next = close.index + close[0].length;
46
+ } else {
47
+ // unclosed tag: recover text up to the next sibling open tag
48
+ const sib = matchFrom(OPEN_TAG_SRC, flags, inner, contentStart);
49
+ if (sib != null) {
50
+ contentEnd = sib.index;
51
+ next = contentEnd;
52
+ } else {
53
+ contentEnd = inner.length;
54
+ next = inner.length;
55
+ }
56
+ }
57
+
58
+ const content = inner.substring(contentStart, contentEnd);
59
+ const value: unknown = content.includes("<") ? nestedOrText(content, ci) : content.trim();
60
+ accumulate(out, key, value);
61
+ pos = next;
62
+ }
63
+ }
64
+
65
+ function nestedOrText(content: string, ci: boolean): unknown {
66
+ const nested: Record<string, unknown> = {};
67
+ parseChildren(content, ci, nested);
68
+ return Object.keys(nested).length === 0 ? content.trim() : nested;
69
+ }
70
+
71
+ function accumulate(out: Record<string, unknown>, key: string, value: unknown): void {
72
+ if (!Object.prototype.hasOwnProperty.call(out, key)) {
73
+ out[key] = value;
74
+ return;
75
+ }
76
+ const existing = out[key];
77
+ if (Array.isArray(existing)) {
78
+ existing.push(value);
79
+ } else {
80
+ out[key] = [existing, value];
81
+ }
82
+ }