@sungen/driver-data-factory 3.2.34-beta.1 → 3.2.35

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sungen/driver-data-factory",
3
- "version": "3.2.34-beta.1",
3
+ "version": "3.2.35",
4
4
  "description": "Sungen Data Factory capability — standardized test-data generation from a field-type + security knowledge catalog (valid/boundary/invalid + traceability), the .overwrite merge layer, and (opt-in) @dataFactory runtime seeding. Design-time knowledge is runtime-free. Plugs into @sun-asterisk/sungen via the capability SPI.",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -14,7 +14,7 @@
14
14
  "author": "eqe team (engineer & quality) — Sun Asterisk",
15
15
  "license": "MIT",
16
16
  "dependencies": {
17
- "@sun-asterisk/sungen": "3.2.34-beta.1",
17
+ "@sun-asterisk/sungen": "3.2.35",
18
18
  "yaml": "^2.4.0"
19
19
  },
20
20
  "files": [
package/src/boundary.ts CHANGED
@@ -90,8 +90,9 @@ function readOffset(seg: string): { base: 'min' | 'max' | 'length' | null; off:
90
90
  let off = 0;
91
91
  if (/\+\s*1|plus one|one extra/.test(s)) off = 1;
92
92
  else if (/-\s*1|minus one/.test(s)) off = -1;
93
- else if (/<|below|under|less than|shorter/.test(s)) off = -1;
94
- else if (/>|over|above|exceed|greater than|longer|too long/.test(s)) off = 1;
93
+ // `<=` / `>=` include the bound itself (off 0), only a strict `<` / `>` steps past it (#904).
94
+ else if (/<(?!=)|below|under|less than|shorter/.test(s)) off = -1;
95
+ else if (/>(?!=)|over|above|exceed|greater than|longer|too long/.test(s)) off = 1;
95
96
  const sign = off > 0 ? '+1' : off < 0 ? '-1' : '';
96
97
  const label = (base || 'bound') + sign;
97
98
  return { base, off, label };
@@ -101,7 +102,23 @@ function readOffset(seg: string): { base: 'min' | 'max' | 'length' | null; off:
101
102
  * Expand a length-bounded string hint into one BoundaryPoint per segment.
102
103
  * min/max come from the constraints; a point OUTSIDE [min,max] is invalid, inside/on-edge is valid.
103
104
  */
104
- function expandLength(type: string, hint: string, c: Record<string, any>): BoundaryPoint[] {
105
+ /** What a length judgement needs beyond the constraints: whether the field must be filled at all. */
106
+ export interface FieldContext { required?: boolean }
107
+
108
+ /**
109
+ * Is a length within the field's rules? Length 0 is "not provided", which `required` decides — not
110
+ * `minLength` (an HTML `minlength` is not enforced on an empty optional field). Only an explicit
111
+ * `required: false` accepts it: an unset `required` counts as required, the same rule the catalog's
112
+ * own empty case (`when: { required: true }`) is admitted by, so the two never disagree on "".
113
+ * Any other length is judged by [minLength, maxLength]. A hard `n >= 1` used to make "" invalid even
114
+ * for an optional field, so a Bio that may be cleared got an "expect an error" oracle (#901).
115
+ */
116
+ function lengthValid(n: number, min: number | undefined, max: number | undefined, ctx: FieldContext): boolean {
117
+ if (n === 0) return ctx.required === false;
118
+ return (!Number.isFinite(min) || n >= (min as number)) && (!Number.isFinite(max) || n <= (max as number));
119
+ }
120
+
121
+ function expandLength(type: string, hint: string, c: Record<string, any>, ctx: FieldContext = {}): BoundaryPoint[] {
105
122
  const min = c.minLength ?? c.length ?? c.minlength;
106
123
  const max = c.maxLength ?? c.length ?? c.maxlength;
107
124
  const segs = segments(hint);
@@ -109,16 +126,23 @@ function expandLength(type: string, hint: string, c: Record<string, any>): Bound
109
126
  let idx = 0;
110
127
  for (const seg of segs) {
111
128
  const { base, off, label } = readOffset(seg);
129
+ // The suffix follows the SEGMENT, skipped or not, so `CHK-STR-I2a` is max+1 whether or not its
130
+ // min-1 sibling exists — a per-field `disable:` names points by these ids (#902).
131
+ const suffix = suffixOf(idx++);
112
132
  const anchor = base === 'max' ? max : base === 'length' ? (c.length ?? max ?? min) : min;
113
133
  if (!Number.isFinite(anchor)) {
114
- out.push({ suffix: suffixOf(idx++), value: null, validity: base === null ? 'valid' : 'valid', label, todo: 'minLength/maxLength/length' });
134
+ out.push({ suffix, value: null, validity: 'valid', label, todo: 'minLength/maxLength/length' });
115
135
  continue;
116
136
  }
117
137
  const n = anchor + off;
118
- const lo = Number.isFinite(min) ? min : 0;
119
- const hi = Number.isFinite(max) ? max : Infinity;
120
- const validity: Validity = n >= lo && n <= hi && n >= 1 ? 'valid' : 'invalid';
121
- out.push({ suffix: suffixOf(idx++), value: fillString(type, Math.max(0, n)), validity, label });
138
+ // No such length: min-1 of `minLength: 0`. It used to be clamped to "", a second empty-string
139
+ // case with the opposite meaning (#901).
140
+ if (n < 0) continue;
141
+ const valid = lengthValid(n, min, max, ctx);
142
+ // A below-min point exists to be OUT of bounds. One that is not — an optional field's empty value
143
+ // under `minLength: 1` — is no boundary violation, so it is not emitted as one.
144
+ if (off < 0 && base !== 'max' && valid) continue;
145
+ out.push({ suffix, value: fillString(type, n), validity: valid ? 'valid' : 'invalid', label });
122
146
  }
123
147
  return out.length ? out : [{ suffix: '', value: null, validity: 'valid', label: 'bound', todo: 'minLength/maxLength' }];
124
148
  }
@@ -201,9 +225,9 @@ function suffixOf(i: number): string {
201
225
  * Public entry: expand a boundary/invalid hint into structured points.
202
226
  * Returns one point per segment; each carries the value AND the oracle's expected validity.
203
227
  */
204
- export function expandBoundaryHint(hint: string, type: string, c: Record<string, any>): BoundaryPoint[] {
228
+ export function expandBoundaryHint(hint: string, type: string, c: Record<string, any>, ctx: FieldContext = {}): BoundaryPoint[] {
205
229
  if (type === 'email') return expandEmail(hint, c);
206
- if (STR_TYPES.has(type)) return expandLength(type, hint, c);
230
+ if (STR_TYPES.has(type)) return expandLength(type, hint, c, ctx);
207
231
  if (NUM_TYPES.has(type)) return expandNumeric(type, hint, c);
208
232
  return [{ suffix: '', value: null, validity: 'valid', label: 'bound', todo: 'value/gen not resolvable' }];
209
233
  }
@@ -232,12 +256,8 @@ export function emailOfTotalLength(n: number): string | null {
232
256
  return 'a@' + 'b'.repeat(rest - 2) + '.c'; // e.g. n=5 → "a@b.c", n=6 → "a@bb.c"
233
257
  }
234
258
 
235
- function satisfiesLength(type: string, value: string, c: Record<string, any>): boolean {
236
- const min = c.minLength ?? c.length;
237
- const max = c.maxLength ?? c.length;
238
- if (Number.isFinite(min) && value.length < min) return false;
239
- if (Number.isFinite(max) && value.length > max) return false;
240
- return true;
259
+ function satisfiesLength(type: string, value: string, c: Record<string, any>, ctx: FieldContext = {}): boolean {
260
+ return lengthValid(value.length, c.minLength ?? c.length, c.maxLength ?? c.length, ctx);
241
261
  }
242
262
 
243
263
  function satisfiesNumeric(type: string, value: string, c: Record<string, any>): boolean {
@@ -258,12 +278,12 @@ function satisfiesNumeric(type: string, value: string, c: Record<string, any>):
258
278
  * them (length/range/precision — NOT charset/format, which the catalog encodes as literal invalids).
259
279
  * Returns null when the type has no numeric/length constraint to check against (nothing to assert).
260
280
  */
261
- export function satisfiesConstraints(type: string, value: string | null, c: Record<string, any>): boolean | null {
281
+ export function satisfiesConstraints(type: string, value: string | null, c: Record<string, any>, ctx: FieldContext = {}): boolean | null {
262
282
  if (value == null) return null;
263
283
  if (type === 'email') return satisfiesEmail(value, c);
264
284
  if (STR_TYPES.has(type)) {
265
285
  if (!Number.isFinite(c.minLength ?? c.length) && !Number.isFinite(c.maxLength ?? c.length)) return null;
266
- return satisfiesLength(type, value, c);
286
+ return satisfiesLength(type, value, c, ctx);
267
287
  }
268
288
  if (NUM_TYPES.has(type)) {
269
289
  if (!Number.isFinite(c.min) && !Number.isFinite(c.max) && !Number.isFinite(c.decimalPlaces)) return null;
@@ -284,12 +304,14 @@ export interface BvaExtra { label: string; value: string; validity: Validity; gr
284
304
  * MISSING points (dedup by exact value), so a type whose catalog already covers a dimension (string /
285
305
  * password / number) yields nothing — no churn.
286
306
  */
287
- export function ensureBvaComplete(type: string, c: Record<string, any>, existing: Array<string | null>): BvaExtra[] {
307
+ export function ensureBvaComplete(type: string, c: Record<string, any>, existing: Array<string | null>, ctx: FieldContext = {}): BvaExtra[] {
288
308
  const have = new Set(existing.filter((v): v is string => v != null));
289
309
  const out: BvaExtra[] = [];
290
310
  const push = (label: string, value: string | null) => {
291
311
  if (value == null || have.has(value)) return;
292
- const ok = satisfiesConstraints(type, value, c);
312
+ const ok = satisfiesConstraints(type, value, c, ctx);
313
+ // A below-min point that is in range (an optional field's empty value) is not a boundary case (#901).
314
+ if (label === 'minLen-1' && ok !== false) return;
293
315
  const validity: Validity = ok === false ? 'invalid' : 'valid';
294
316
  out.push({ label, value, validity, group: validity === 'invalid' ? 'invalid' : 'boundary' });
295
317
  have.add(value);
@@ -305,7 +327,7 @@ export function ensureBvaComplete(type: string, c: Record<string, any>, existing
305
327
  }
306
328
  if (STR_TYPES.has(type)) {
307
329
  const min = c.minLength ?? c.length, max = c.maxLength ?? c.length;
308
- if (Number.isFinite(min)) { push('minLen-1', fillString(type, min - 1)); push('minLen', fillString(type, min)); push('minLen+1', fillString(type, min + 1)); }
330
+ if (Number.isFinite(min)) { if (min >= 1) push('minLen-1', fillString(type, min - 1)); push('minLen', fillString(type, min)); push('minLen+1', fillString(type, min + 1)); }
309
331
  if (Number.isFinite(max)) { push('maxLen-1', fillString(type, max - 1)); push('maxLen', fillString(type, max)); push('maxLen+1', fillString(type, max + 1)); }
310
332
  return out;
311
333
  }
@@ -22,24 +22,41 @@ export function commonDir(): string {
22
22
  return join(__dirname, '..', 'common');
23
23
  }
24
24
 
25
- function readYamlFilesDeep(dir: string): Array<{ file: string; data: any }> {
26
- const out: Array<{ file: string; data: any }> = [];
25
+ function readYamlFilesDeep(dir: string): Array<{ file: string; data: any; text: string }> {
26
+ const out: Array<{ file: string; data: any; text: string }> = [];
27
27
  if (!existsSync(dir)) return out;
28
28
  for (const name of readdirSync(dir)) {
29
29
  const full = join(dir, name);
30
30
  if (statSync(full).isDirectory()) out.push(...readYamlFilesDeep(full));
31
31
  else if (name.endsWith('.yaml') || name.endsWith('.yml')) {
32
- out.push({ file: full, data: parseYaml(readFileSync(full, 'utf8')) });
32
+ const text = readFileSync(full, 'utf8');
33
+ out.push({ file: full, data: parseYaml(text), text });
33
34
  }
34
35
  }
35
36
  return out;
36
37
  }
37
38
 
39
+ /** The keys of the top-level `constraints:` block whose line carries a `# placeholder` comment. */
40
+ export function placeholderKeys(text: string): string[] {
41
+ const keys: string[] = [];
42
+ let inBlock = false;
43
+ for (const line of text.split(/\r?\n/)) {
44
+ if (/^constraints:\s*(#.*)?$/.test(line)) { inBlock = true; continue; }
45
+ if (!inBlock || !line.trim() || /^\s*#/.test(line)) continue;
46
+ if (!/^\s/.test(line)) break; // the next top-level key ends the block
47
+ const m = line.match(/^\s+([A-Za-z_][\w-]*)\s*:.*#\s*placeholder\b/i);
48
+ if (m) keys.push(m[1]);
49
+ }
50
+ return keys;
51
+ }
52
+
38
53
  /** Load the shipped common catalog (no project overlay). */
39
54
  export function loadShippedCatalog(root = commonDir()): Catalog {
40
55
  const fieldTypes: Record<string, FieldType> = {};
41
- for (const { data } of readYamlFilesDeep(join(root, 'field-types'))) {
42
- if (data?.type) fieldTypes[data.type] = data as FieldType;
56
+ for (const { data, text } of readYamlFilesDeep(join(root, 'field-types'))) {
57
+ if (!data?.type) continue;
58
+ const placeholders = placeholderKeys(text);
59
+ fieldTypes[data.type] = { ...data, ...(placeholders.length ? { placeholderConstraints: placeholders } : {}) } as FieldType;
43
60
  }
44
61
  const security: Record<string, SecurityBank> = {};
45
62
  for (const { data } of readYamlFilesDeep(join(root, 'security'))) {
@@ -97,6 +114,8 @@ function mergeFieldType(base: FieldType, overlay: FieldTypeOverlay, prov: FieldT
97
114
  ...base,
98
115
  ...rest,
99
116
  constraints: { ...(base.constraints || {}), ...(overlay.constraints || {}) },
117
+ // A constraint the project's overwrite sets is a real value, no longer the shipped placeholder.
118
+ placeholderConstraints: (base.placeholderConstraints || []).filter((k) => !(k in (overlay.constraints || {}))),
100
119
  uses_security: overlay.uses_security ?? base.uses_security,
101
120
  valid: mergeGroup(base.valid, overlay.valid, disableSet, prov),
102
121
  boundary: mergeGroup(base.boundary, overlay.boundary, disableSet, prov),
package/src/generator.ts CHANGED
@@ -18,6 +18,12 @@ export interface FieldDef {
18
18
  /** Catalog field-type id (email/password/…). Omit for a GROUP (a field with nested `fields`). A type absent from the catalog → blind spot. */
19
19
  type?: string;
20
20
  required?: boolean;
21
+ /**
22
+ * Point ids to drop for THIS field only (#902) — an expanded id such as `CHK-STR-I2` (min-1) or
23
+ * `CHK-STR-I2a` (max+1). The overwrite's `disable:` removes a case for every field of the type;
24
+ * this removes one boundary for one field. Dropped points are reported in `excluded`, never silent.
25
+ */
26
+ disable?: string[];
21
27
  /** Project overrides merged over the catalog constraints (maxLength, min, max, …). */
22
28
  constraints?: Record<string, any>;
23
29
  /** Placeholder errorRef (Mxx) → real project message code. Unmapped refs are flagged open. */
@@ -89,6 +95,10 @@ export interface ExpandedField {
89
95
  dependsOn?: { field: string; value?: string };
90
96
  /** Cases NOT materialized for the selection context (applicability) — surfaced, never silent. */
91
97
  excluded?: Array<{ id: string; reason: string }>;
98
+ /** Catalog placeholder constraints this field relies on (it set no value of its own) — e.g. `minLength`. */
99
+ placeholders?: string[];
100
+ /** `disable:` ids that matched no point generated for this field (a typo, or a case not emitted here). */
101
+ disableUnmatched?: string[];
92
102
  }
93
103
 
94
104
  /**
@@ -112,9 +122,15 @@ function coerceValidNumeric(type: string, value: string, c: Record<string, any>)
112
122
  return String(Math.round(out));
113
123
  }
114
124
 
115
- /** A gen hint that references a numeric/length boundary the generator can expand structurally. */
125
+ /**
126
+ * A gen hint that references a numeric/length boundary the generator can expand structurally.
127
+ * `<` / `>` count only as a comparison against a named bound (`< min length`, `local > maxLocal`):
128
+ * a bare one is a character in a charset list (`special chars …<>?`, `forbidden chars …<>\?`), and
129
+ * matching it turned those cases into meaningless `bound-1 / bound` lengths (#904). A bare number
130
+ * (`> 20 chars`) is not read either — the expansion has no way to use it — so it stays an open case.
131
+ */
116
132
  function isBoundaryHint(hint: string): boolean {
117
- return /\bmin\b|\bmax\b|minimum|maximum|length|byte|exact|decimal|precision|local|total|over|below|under|exceed|\+\s*1|-\s*1|<|>/.test(
133
+ return /\bmin\b|\bmax\b|minimum|maximum|length|byte|exact|decimal|precision|local|total|over|below|under|exceed|\+\s*1|-\s*1|[<>]=?\s*(?:min|max|length|local|total)/.test(
118
134
  hint.toLowerCase(),
119
135
  );
120
136
  }
@@ -160,6 +176,8 @@ export function expandField(field: FieldDef, catalog: Catalog, ctx: SelectionCon
160
176
  const points: ExpandedPoint[] = [];
161
177
  const excluded: Array<{ id: string; reason: string }> = [];
162
178
  const facts = fieldFacts(field, c);
179
+ // Length 0 is judged by `required`, not by minLength (#901) — every oracle call gets the field context.
180
+ const fctx = { required: field.required };
163
181
 
164
182
  // Gate a catalog case by applicability (`when`) then by the selection profile. A `when` miss is
165
183
  // reported as excluded (not silently dropped); a profile miss is an explicit selection (not reported).
@@ -178,7 +196,7 @@ export function expandField(field: FieldDef, catalog: Catalog, ctx: SelectionCon
178
196
  const pushLiteral = (dp: DataPoint, group: ExpandedPoint['group'], value: string | null, opts: { validity?: Validity; label?: string; todo?: string } = {}) => {
179
197
  // A valid representative must be valid for the project: clamp an out-of-range numeric literal.
180
198
  let coerceNote: string | undefined;
181
- if (group === 'valid' && value != null && satisfiesConstraints(type, value, c) === false) {
199
+ if (group === 'valid' && value != null && satisfiesConstraints(type, value, c, fctx) === false) {
182
200
  const fixed = coerceValidNumeric(type, value, c);
183
201
  if (fixed != null) { value = fixed; coerceNote = 'adjusted to fit constraints'; }
184
202
  }
@@ -193,7 +211,7 @@ export function expandField(field: FieldDef, catalog: Catalog, ctx: SelectionCon
193
211
  let validity: Validity;
194
212
  if (dp.validity) validity = dp.validity;
195
213
  else if (group === 'boundary') {
196
- const oracle = satisfiesConstraints(type, isUniqueValid ? value : v, c);
214
+ const oracle = satisfiesConstraints(type, isUniqueValid ? value : v, c, fctx);
197
215
  validity = oracle == null ? (opts.validity ?? 'valid') : oracle ? 'valid' : 'invalid';
198
216
  } else validity = opts.validity ?? groupValidity(group);
199
217
  emit({
@@ -217,7 +235,7 @@ export function expandField(field: FieldDef, catalog: Catalog, ctx: SelectionCon
217
235
  const hint = `${dp.gen || ''} ${dp.note || ''}`.trim();
218
236
  // A boundary/length/numeric hint → expand to one case PER boundary point, each with its validity.
219
237
  if (hint && isBoundaryHint(hint)) {
220
- const pts = expandBoundaryHint(hint, type, c);
238
+ const pts = expandBoundaryHint(hint, type, c, fctx);
221
239
  for (const bp of pts) {
222
240
  emit({
223
241
  id: dp.id + bp.suffix,
@@ -270,7 +288,7 @@ export function expandField(field: FieldDef, catalog: Catalog, ctx: SelectionCon
270
288
  // email `minLength` the catalog didn't model). Only in-range points are `boundary`; out-of-range are
271
289
  // `invalid`. Appended after the catalog cases so it never overrides an authored one.
272
290
  const idCode = type.toUpperCase().replace(/[^A-Z0-9]/g, '');
273
- for (const ex of ensureBvaComplete(type, c, points.map((p) => p.value))) {
291
+ for (const ex of ensureBvaComplete(type, c, points.map((p) => p.value), fctx)) {
274
292
  points.push({
275
293
  id: `CHK-${idCode}-BVA-${ex.label}`,
276
294
  group: ex.group,
@@ -282,13 +300,22 @@ export function expandField(field: FieldDef, catalog: Catalog, ctx: SelectionCon
282
300
  });
283
301
  }
284
302
 
303
+ // Field-level disable (#902): drop exactly the named points, and say so.
304
+ const disabled = new Set(field.disable || []);
305
+ const kept = disabled.size ? points.filter((p) => !disabled.has(p.id)) : points;
306
+ for (const p of points) if (disabled.has(p.id)) excluded.push({ id: p.id, reason: 'disabled for this field' });
307
+ const disableUnmatched = [...disabled].filter((id) => !points.some((p) => p.id === id));
308
+
309
+ const placeholders = (ft.placeholderConstraints || []).filter((k) => !(field.constraints && k in field.constraints));
285
310
  return {
286
311
  name: field.name,
287
312
  type: field.type,
288
313
  blindSpot: false,
289
- points,
314
+ points: kept,
290
315
  ...(field.dependsOn ? { dependsOn: field.dependsOn } : {}),
291
316
  ...(excluded.length ? { excluded } : {}),
317
+ ...(placeholders.length ? { placeholders } : {}),
318
+ ...(disableUnmatched.length ? { disableUnmatched } : {}),
292
319
  };
293
320
  }
294
321
 
package/src/lint.ts CHANGED
@@ -85,6 +85,18 @@ export function lintFieldMap(map: FieldMap, catalog: Catalog, ctx: SelectionCont
85
85
  message: `${live.join(', ')} is a real mailbox provider — a registration or reset test using it sends mail to whoever owns that address. Use the project domain (@sun-asterisk.com) or the reserved @example.com.` });
86
86
  }
87
87
 
88
+ // A generic catalog stand-in (string minLength 1, phone 8..15 …) silently became this field's rule.
89
+ if (f.placeholders?.length) {
90
+ const vals = f.placeholders.map((k) => `${k}: ${JSON.stringify(f.type ? catalog.fieldTypes[f.type]?.constraints?.[k] : undefined)}`);
91
+ issues.push({ field: path, severity: 'warn', code: 'PLACEHOLDER_CONSTRAINT',
92
+ message: `uses the catalog's placeholder ${vals.join(', ')} — set the real value from the spec in the field-map's constraints (the boundary cases are built from it)` });
93
+ }
94
+
95
+ if (f.disableUnmatched?.length) {
96
+ issues.push({ field: path, severity: 'warn', code: 'DISABLE_UNMATCHED',
97
+ message: `disable: ${f.disableUnmatched.join(', ')} matches no case generated for this field — check the id (a boundary point carries its suffix, e.g. CHK-STR-I2a)` });
98
+ }
99
+
88
100
  // Applicability (M2b): cases NOT materialized for this field/context — surfaced, never silent.
89
101
  if (f.excluded?.length) {
90
102
  issues.push({ field: path, severity: 'info', code: 'NOT_APPLICABLE',
package/src/types.ts CHANGED
@@ -52,6 +52,12 @@ export interface FieldType {
52
52
  description?: string;
53
53
  format?: string;
54
54
  constraints?: Record<string, unknown>;
55
+ /**
56
+ * Constraint keys the shipped catalog marks `# placeholder` (a generic stand-in such as string
57
+ * `minLength: 1`). Read from the yaml text by the loader — the parsed yaml has no comments. A field
58
+ * that relies on one without its own value is warned about (lint PLACEHOLDER_CONSTRAINT).
59
+ */
60
+ placeholderConstraints?: string[];
55
61
  /** Security banks whose payloads are appended to the `invalid` group. */
56
62
  uses_security?: string[];
57
63
  valid?: DataPoint[];
package/src/validate.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import type { Catalog, SelectionContext } from './types';
12
12
  import { expandFieldMap, type FieldMap, type FieldDef, type ExpandedField, type ExpandedPoint } from './generator';
13
- import { satisfiesConstraints } from './boundary';
13
+ import { satisfiesConstraints, type FieldContext } from './boundary';
14
14
 
15
15
  export interface ValidationFinding {
16
16
  field: string;
@@ -21,12 +21,12 @@ export interface ValidationFinding {
21
21
  }
22
22
 
23
23
  /** Validate the values a set of expanded points, given the effective constraints for that field. */
24
- export function validatePoints(field: string, type: string | undefined, points: ExpandedPoint[], c: Record<string, any>): ValidationFinding[] {
24
+ export function validatePoints(field: string, type: string | undefined, points: ExpandedPoint[], c: Record<string, any>, ctx: FieldContext = {}): ValidationFinding[] {
25
25
  const out: ValidationFinding[] = [];
26
26
  if (!type) return out;
27
27
  for (const p of points) {
28
28
  if (p.value == null) continue; // open/todo — lint handles these
29
- const oracle = satisfiesConstraints(type, p.value, c);
29
+ const oracle = satisfiesConstraints(type, p.value, c, ctx);
30
30
  if (oracle == null) continue; // no length/range constraint to judge against
31
31
  if (p.expectedValidity === 'valid' && oracle === false) {
32
32
  out.push({
@@ -69,7 +69,7 @@ export function validateGenerated(map: FieldMap, catalog: Catalog, ctx: Selectio
69
69
  if (ef.children) { walk(ef.children, def?.fields || [], path); continue; }
70
70
  const ft = ef.type ? catalog.fieldTypes[ef.type] : undefined;
71
71
  const c = { ...(ft?.constraints || {}), ...(def?.constraints || {}) };
72
- out.push(...validatePoints(path, ef.type, ef.points, c));
72
+ out.push(...validatePoints(path, ef.type, ef.points, c, { required: def?.required }));
73
73
  }
74
74
  };
75
75
  walk(expanded, map.fields, '');