@sungen/driver-data-factory 3.2.10-beta.14 → 3.2.10-beta.2

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.10-beta.14",
3
+ "version": "3.2.10-beta.2",
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.10-beta.14",
17
+ "@sun-asterisk/sungen": "3.2.10-beta.2",
18
18
  "yaml": "^2.4.0"
19
19
  },
20
20
  "files": [
package/src/cli.ts CHANGED
@@ -13,8 +13,6 @@ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
13
13
  import { loadEffectiveCatalog } from './catalog-loader';
14
14
  import { expandFieldMap, renderTestData, type FieldMap } from './generator';
15
15
  import { lintFieldMap, summarize } from './lint';
16
- import { generateFixture } from './fixture';
17
- import { ensureDataFactoryReadme } from './readme';
18
16
 
19
17
  const FIELDMAP_DIR = (cwd: string) => join(cwd, 'qa', 'data-factory');
20
18
  const fieldMapPath = (cwd: string, name: string) => join(FIELDMAP_DIR(cwd), `${name}.fields.yaml`);
@@ -57,31 +55,14 @@ async function genOne(cwd: string, name: string, opts: { force?: boolean }): Pro
57
55
  const out = testDataPath(cwd, name);
58
56
  if (!out) { console.error(` ✗ ${name}: no qa/screens/${name} or qa/flows/${name}`); return 'missing'; }
59
57
 
60
- const dfData = renderTestData(expanded) as Record<string, any>;
61
- const exists = existsSync(out);
62
-
63
- // MERGE, don't clobber. Data Factory owns only the keys it generates (`_generated_by` + each
64
- // field name); every OTHER key in the existing test-data (create-test's data, flow-namespaced
65
- // keys like `login.email`, @cases datasets, overrides) is preserved. A regenerated field key is
66
- // replaced by its standardized version (that's the point). Fixes the whole-file clobber.
67
- let final = dfData;
68
- let mergedInto = false;
69
- if (exists) {
70
- if (!opts.force) {
71
- const a = (await ask(` ? ${name}: test-data exists — merge standardized field data in (keeps other keys)? [y/N] `)).toLowerCase();
72
- if (a !== 'y' && a !== 'yes') { console.log(` · ${name}: skipped`); return 'skipped'; }
73
- }
74
- let existing: Record<string, any> = {};
75
- try { existing = (parseYaml(readFileSync(out, 'utf8')) as Record<string, any>) || {}; } catch { existing = {}; }
76
- final = { ...existing, ...dfData }; // DF-owned keys win; all other existing keys preserved
77
- mergedInto = true;
58
+ if (existsSync(out) && !opts.force) {
59
+ const a = (await ask(` ? ${name}: test-data already exists — overwrite? [y/N] `)).toLowerCase();
60
+ if (a !== 'y' && a !== 'yes') { console.log(` · ${name}: skipped`); return 'skipped'; }
78
61
  }
79
62
  mkdirSync(dirname(out), { recursive: true });
80
- writeFileSync(out, stringifyYaml(final), 'utf8');
63
+ writeFileSync(out, stringifyYaml(renderTestData(expanded)), 'utf8');
81
64
  const blind = expanded.filter((f) => f.blindSpot).map((f) => f.name);
82
- const verb = mergedInto ? 'merged into' : 'wrote';
83
- const fieldKeys = expanded.map((f) => f.name).length;
84
- console.log(` ✓ ${name}: ${verb} ${out.replace(cwd + '/', '')} (${fieldKeys} field key(s)${mergedInto ? ', other keys preserved' : ''})${blind.length ? ` — blind spots: ${blind.join(', ')}` : ''}`);
65
+ console.log(` ✓ ${name}: wrote ${out.replace(cwd + '/', '')}${blind.length ? ` (blind spots: ${blind.join(', ')})` : ''}`);
85
66
  return 'written';
86
67
  }
87
68
 
@@ -97,7 +78,6 @@ export function registerDataFactoryCommand(program: any): void {
97
78
  .option('--force', 'Overwrite existing test-data without asking (implies --yes)')
98
79
  .action(async (opts: { screen?: string; all?: boolean; yes?: boolean; force?: boolean }) => {
99
80
  const cwd = process.cwd();
100
- ensureDataFactoryReadme(cwd);
101
81
  if (opts.screen) { await genOne(cwd, opts.screen, opts); return; }
102
82
  if (!opts.all) {
103
83
  console.error('Specify --screen <name> for one unit, or --all for every unit (with confirmation).');
@@ -125,7 +105,6 @@ export function registerDataFactoryCommand(program: any): void {
125
105
  .option('--screen <name>', 'Lint one unit (default: all field-maps)')
126
106
  .action((opts: { screen?: string }) => {
127
107
  const cwd = process.cwd();
128
- ensureDataFactoryReadme(cwd);
129
108
  const names = opts.screen ? [opts.screen] : listFieldMaps(cwd);
130
109
  if (names.length === 0) { console.error('No field-maps under qa/data-factory/*.fields.yaml.'); process.exitCode = 1; return; }
131
110
  const { catalog } = loadEffectiveCatalog(cwd);
@@ -145,32 +124,4 @@ export function registerDataFactoryCommand(program: any): void {
145
124
  }
146
125
  if (totalErrors) process.exitCode = 1;
147
126
  });
148
-
149
- data
150
- .command('fixture')
151
- .description('Generate a CSV import fixture from the field-map (bulk-import / file-upload testing)')
152
- .requiredOption('--screen <name>', 'Unit whose field-map to build a fixture from')
153
- .option('--rows <n>', 'Number of valid rows', '3')
154
- .option('--invalid', 'Also write <name>.invalid.csv (one seeded-bad row per field + _violation column)')
155
- .option('--force', 'Overwrite existing fixture file(s) without asking')
156
- .action(async (opts: { screen: string; rows?: string; invalid?: boolean; force?: boolean }) => {
157
- const cwd = process.cwd();
158
- ensureDataFactoryReadme(cwd);
159
- const map = loadFieldMap(cwd, opts.screen);
160
- if (!map) { console.error(`✗ ${opts.screen}: no field-map at qa/data-factory/${opts.screen}.fields.yaml`); process.exitCode = 1; return; }
161
- const { catalog } = loadEffectiveCatalog(cwd);
162
- const res = generateFixture(map, catalog, { rows: parseInt(opts.rows || '3', 10), includeInvalid: !!opts.invalid });
163
- const dir = join(cwd, 'qa', 'fixtures');
164
- mkdirSync(dir, { recursive: true });
165
- const validPath = join(dir, `${opts.screen}.csv`);
166
- const writes: Array<[string, string]> = [[validPath, res.valid]];
167
- if (res.invalid) writes.push([join(dir, `${opts.screen}.invalid.csv`), res.invalid]);
168
- for (const [p] of writes) {
169
- if (existsSync(p) && !opts.force) {
170
- const a = (await ask(` ? ${p.replace(cwd + '/', '')} exists — overwrite? [y/N] `)).toLowerCase();
171
- if (a !== 'y' && a !== 'yes') { console.log(' · skipped'); return; }
172
- }
173
- }
174
- for (const [p, content] of writes) { writeFileSync(p, content, 'utf8'); console.log(` ✓ wrote ${p.replace(cwd + '/', '')} (${res.headers.length} columns)`); }
175
- });
176
127
  }
package/src/generator.ts CHANGED
@@ -13,31 +13,13 @@ import type { Catalog, FieldType, DataPoint } from './types';
13
13
 
14
14
  export interface FieldDef {
15
15
  name: string;
16
- /** Catalog field-type id (email/password/…). Omit for a GROUP (a field with nested `fields`). A type absent from the catalog → blind spot. */
17
- type?: string;
16
+ /** Catalog field-type id (email/password/…). A type absent from the catalog → blind spot. */
17
+ type: string;
18
18
  required?: boolean;
19
19
  /** Project overrides merged over the catalog constraints (maxLength, min, max, …). */
20
20
  constraints?: Record<string, any>;
21
21
  /** Placeholder errorRef (Mxx) → real project message code. Unmapped refs are flagged open. */
22
22
  errorMap?: Record<string, string>;
23
- /**
24
- * Field must be fresh/unique per test run (e.g. a register email/username that can't collide
25
- * with a record left by a previous run). When true, the VALID value(s) get a runtime `{{$timestamp}}`
26
- * suffix so `resolveDynamicVars` produces a new value every run — no cross-run cache, no flakiness.
27
- */
28
- unique?: boolean;
29
- /**
30
- * Nested / group field (complex form): sub-fields expanded recursively under this name
31
- * (e.g. `address` → `city`, `postcode`). A field with `fields` is a group and needs no `type`.
32
- */
33
- fields?: FieldDef[];
34
- /**
35
- * Inter-field dependency: this field's rule/format depends on another field's value
36
- * (e.g. postcode format `dependsOn: { field: country, value: JP }`). Carried through as metadata
37
- * so the scenario weave + QA honor the relationship — the generator does not resolve the runtime
38
- * condition (that is scenario/@cases logic), it records it.
39
- */
40
- dependsOn?: { field: string; value?: string };
41
23
  }
42
24
 
43
25
  export interface FieldMap {
@@ -61,13 +43,9 @@ export interface ExpandedPoint {
61
43
 
62
44
  export interface ExpandedField {
63
45
  name: string;
64
- type?: string;
46
+ type: string;
65
47
  blindSpot: boolean;
66
48
  points: ExpandedPoint[];
67
- /** Nested sub-fields (a group field). When present, `points` is empty and children hold the data. */
68
- children?: ExpandedField[];
69
- /** Recorded inter-field dependency (metadata only — not resolved deterministically). */
70
- dependsOn?: { field: string; value?: string };
71
49
  }
72
50
 
73
51
  /** Deterministic filler: a string of exactly `n` chars from a charset seed. */
@@ -148,53 +126,23 @@ function resolveError(dp: DataPoint, errorMap?: Record<string, string>): { expec
148
126
  return { expectedError: dp.errorRef, open: true }; // placeholder survives; flagged open to confirm
149
127
  }
150
128
 
151
- /**
152
- * Make a valid value fresh-per-run by injecting a `{{$timestamp}}` token (resolved at runtime by the
153
- * TestDataLoader's resolveDynamicVars). Email keeps validity via a `+tag` in the local part; other
154
- * types append the token. Deterministic at compile time (same token string) — uniqueness is at run time.
155
- */
156
- function makeUnique(value: string, type: string): string {
157
- if (value.includes('{{$')) return value; // already dynamic
158
- if (type === 'email' && value.includes('@')) {
159
- const at = value.indexOf('@');
160
- return `${value.slice(0, at)}+{{$timestamp}}${value.slice(at)}`;
161
- }
162
- return `${value}{{$timestamp}}`;
163
- }
164
-
165
129
  /** Expand one field into valid/boundary/invalid data points using the effective catalog. */
166
130
  export function expandField(field: FieldDef, catalog: Catalog): ExpandedField {
167
- // Group / nested field: no own type — expand each sub-field recursively.
168
- if (field.fields && field.fields.length) {
169
- return {
170
- name: field.name,
171
- type: undefined,
172
- blindSpot: false,
173
- points: [],
174
- children: field.fields.map((sub) => expandField(sub, catalog)),
175
- ...(field.dependsOn ? { dependsOn: field.dependsOn } : {}),
176
- };
177
- }
178
- const ft: FieldType | undefined = field.type ? catalog.fieldTypes[field.type] : undefined;
179
- if (!ft) return { name: field.name, type: field.type, blindSpot: true, points: [], ...(field.dependsOn ? { dependsOn: field.dependsOn } : {}) };
131
+ const ft: FieldType | undefined = catalog.fieldTypes[field.type];
132
+ if (!ft) return { name: field.name, type: field.type, blindSpot: true, points: [] };
180
133
 
181
134
  const c = { ...(ft.constraints || {}), ...(field.constraints || {}) };
182
135
  const points: ExpandedPoint[] = [];
183
136
 
184
137
  const push = (dp: DataPoint, group: ExpandedPoint['group']) => {
185
138
  const s = synthesize(dp, field.type, c);
186
- // Uniqueness: only the VALID (happy-path create) value gets a fresh-per-run suffix. Boundary/invalid
187
- // stay literal — they test rules, not record creation.
188
- const value = group === 'valid' && field.unique && s.value != null
189
- ? makeUnique(s.value, field.type)
190
- : s.value;
191
139
  points.push({
192
140
  id: dp.id,
193
141
  group,
194
- value,
142
+ value: s.value,
195
143
  todo: s.todo,
196
144
  reason: dp.reason,
197
- note: field.unique && group === 'valid' && s.value != null ? `${dp.note ? dp.note + ' · ' : ''}fresh per run` : dp.note,
145
+ note: dp.note,
198
146
  ...resolveError(dp, field.errorMap),
199
147
  });
200
148
  };
@@ -222,7 +170,7 @@ export function expandField(field: FieldDef, catalog: Catalog): ExpandedField {
222
170
  }
223
171
  }
224
172
 
225
- return { name: field.name, type: field.type, blindSpot: false, points, ...(field.dependsOn ? { dependsOn: field.dependsOn } : {}) };
173
+ return { name: field.name, type: field.type, blindSpot: false, points };
226
174
  }
227
175
 
228
176
  /** Expand a whole field-map. */
@@ -238,30 +186,21 @@ export function renderTestData(expanded: ExpandedField[]): Record<string, any> {
238
186
  const out: Record<string, any> = {
239
187
  _generated_by: 'sungen data gen — standardized test-data (valid/boundary/invalid + CHK trace). Do not hand-edit values; edit the .fields.yaml or common.overwrite.yaml and re-run.',
240
188
  };
241
- for (const f of expanded) out[f.name] = renderOne(f);
242
- return out;
243
- }
244
-
245
- /** Render one expanded field — recurses for nested groups; carries a dependsOn note. */
246
- function renderOne(f: ExpandedField): Record<string, any> {
247
- const dep = f.dependsOn ? { _depends_on: f.dependsOn.value != null ? `${f.dependsOn.field} = ${f.dependsOn.value}` : f.dependsOn.field } : {};
248
- if (f.children) {
249
- const group: Record<string, any> = { ...dep };
250
- for (const c of f.children) group[c.name] = renderOne(c);
251
- return group;
252
- }
253
- if (f.blindSpot) {
254
- return { _blind_spot: `type "${f.type}" not in Data Factory catalog — provide qa/data-factory/project/${f.name}.yaml`, ...dep };
189
+ for (const f of expanded) {
190
+ if (f.blindSpot) {
191
+ out[f.name] = { _blind_spot: `type "${f.type}" not in Data Factory catalog — provide qa/data-factory/project/${f.name}.yaml` };
192
+ continue;
193
+ }
194
+ const valid = f.points.filter((p) => p.group === 'valid');
195
+ const boundary = f.points.filter((p) => p.group === 'boundary');
196
+ const invalid = f.points.filter((p) => p.group === 'invalid');
197
+ out[f.name] = {
198
+ valid: valid[0]?.value ?? null,
199
+ boundary: boundary.map((p) => trim({ id: p.id, value: p.value, note: p.note, todo: p.todo })),
200
+ invalid: invalid.map((p) => trim({ id: p.id, value: p.value, expected_error: p.expectedError, open: p.open, reason: p.reason, todo: p.todo })),
201
+ };
255
202
  }
256
- const valid = f.points.filter((p) => p.group === 'valid');
257
- const boundary = f.points.filter((p) => p.group === 'boundary');
258
- const invalid = f.points.filter((p) => p.group === 'invalid');
259
- return {
260
- ...dep,
261
- valid: valid[0]?.value ?? null,
262
- boundary: boundary.map((p) => trim({ id: p.id, value: p.value, note: p.note, todo: p.todo })),
263
- invalid: invalid.map((p) => trim({ id: p.id, value: p.value, expected_error: p.expectedError, open: p.open, reason: p.reason, todo: p.todo })),
264
- };
203
+ return out;
265
204
  }
266
205
 
267
206
  function trim(o: Record<string, any>): Record<string, any> {
package/src/index.ts CHANGED
@@ -24,8 +24,6 @@ export type { FieldDef, FieldMap, ExpandedField, ExpandedPoint } from './generat
24
24
  export { lintFieldMap, summarize } from './lint';
25
25
  export type { LintIssue } from './lint';
26
26
  export { registerDataFactoryCommand } from './cli';
27
- export { generateFixture } from './fixture';
28
- export type { FixtureResult } from './fixture';
29
27
  export { dataFactoryCoverageSensor } from './sensor';
30
28
  export { dfPreconditionCodegen } from './precondition';
31
29
  export type { Catalog, FieldType, SecurityBank, Technique, DataPoint, OverwriteWarning } from './types';
package/src/lint.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * stale `.overwrite` warnings. Pure functions; no side effects.
8
8
  */
9
9
  import type { Catalog } from './types';
10
- import type { FieldMap, ExpandedField, FieldDef } from './generator';
10
+ import type { FieldMap, ExpandedField } from './generator';
11
11
  import { expandFieldMap } from './generator';
12
12
 
13
13
  export interface LintIssue {
@@ -22,22 +22,11 @@ const BOUNDED = new Set(['email', 'string', 'password', 'phone', 'postcode', 'ka
22
22
  export function lintFieldMap(map: FieldMap, catalog: Catalog): { issues: LintIssue[]; expanded: ExpandedField[] } {
23
23
  const expanded = expandFieldMap(map, catalog);
24
24
  const issues: LintIssue[] = [];
25
+ const byName = new Map(map.fields.map((f) => [f.name, f]));
25
26
 
26
- // Flatten nested groups to leaf fields (dotted path), pairing each with its FieldDef (for `required`).
27
- const leaves: Array<{ f: ExpandedField; def?: FieldDef; path: string }> = [];
28
- const walk = (efs: ExpandedField[], defs: FieldDef[], prefix: string) => {
29
- for (const ef of efs) {
30
- const def = defs.find((d) => d.name === ef.name);
31
- const path = prefix ? `${prefix}.${ef.name}` : ef.name;
32
- if (ef.children) walk(ef.children, def?.fields || [], path);
33
- else leaves.push({ f: ef, def, path });
34
- }
35
- };
36
- walk(expanded, map.fields, '');
37
-
38
- for (const { f, def, path } of leaves) {
27
+ for (const f of expanded) {
39
28
  if (f.blindSpot) {
40
- issues.push({ field: path, severity: 'error', code: 'BLIND_SPOT',
29
+ issues.push({ field: f.name, severity: 'error', code: 'BLIND_SPOT',
41
30
  message: `type "${f.type}" is not in the Data Factory catalog — supply qa/data-factory/project/${f.name}.yaml (valid set, invalid/nonexistent values, error code, dependencies)` });
42
31
  continue;
43
32
  }
@@ -45,25 +34,26 @@ export function lintFieldMap(map: FieldMap, catalog: Catalog): { issues: LintIss
45
34
  const boundary = f.points.filter((p) => p.group === 'boundary');
46
35
  const invalid = f.points.filter((p) => p.group === 'invalid');
47
36
 
48
- if (valid.length === 0) issues.push({ field: path, severity: 'error', code: 'NO_VALID', message: 'no valid representative value' });
49
- if (invalid.length === 0) issues.push({ field: path, severity: 'error', code: 'NO_INVALID', message: 'no invalid value — every rule needs at least one' });
50
- if (f.type && BOUNDED.has(f.type) && boundary.length === 0) {
51
- issues.push({ field: path, severity: 'warn', code: 'NO_BOUNDARY', message: `type "${f.type}" usually has min/max — add boundary values or state why not (phuong-phap §6)` });
37
+ if (valid.length === 0) issues.push({ field: f.name, severity: 'error', code: 'NO_VALID', message: 'no valid representative value' });
38
+ if (invalid.length === 0) issues.push({ field: f.name, severity: 'error', code: 'NO_INVALID', message: 'no invalid value — every rule needs at least one' });
39
+ if (BOUNDED.has(f.type) && boundary.length === 0) {
40
+ issues.push({ field: f.name, severity: 'warn', code: 'NO_BOUNDARY', message: `type "${f.type}" usually has min/max — add boundary values or state why not (phuong-phap §6)` });
52
41
  }
53
42
 
54
43
  // Unresolved (todo) values — a boundary/gen the constraints could not synthesize.
55
44
  for (const p of f.points) {
56
- if (p.todo) issues.push({ field: path, severity: 'warn', code: 'UNRESOLVED',
45
+ if (p.todo) issues.push({ field: f.name, severity: 'warn', code: 'UNRESOLVED',
57
46
  message: `${p.id} (${p.group}) needs input: ${p.todo}${p.note ? ` — ${p.note}` : ''}` });
58
47
  }
59
48
  // Placeholder error codes still unmapped.
60
49
  const openErrs = invalid.filter((p) => p.open);
61
50
  if (openErrs.length) {
62
- issues.push({ field: path, severity: 'info', code: 'OPEN_ERRORREF',
51
+ issues.push({ field: f.name, severity: 'info', code: 'OPEN_ERRORREF',
63
52
  message: `${openErrs.length} invalid case(s) use placeholder error codes (${[...new Set(openErrs.map((p) => p.expectedError))].join(', ')}) — map to real codes via errorMap in the field-map, or confirm with BA/Dev` });
64
53
  }
54
+ const def = byName.get(f.name);
65
55
  if (def?.required && valid.length === 0) {
66
- issues.push({ field: path, severity: 'error', code: 'REQUIRED_NO_VALID', message: 'required field with no valid value' });
56
+ issues.push({ field: f.name, severity: 'error', code: 'REQUIRED_NO_VALID', message: 'required field with no valid value' });
67
57
  }
68
58
  }
69
59
  return { issues, expanded };
package/src/fixture.ts DELETED
@@ -1,79 +0,0 @@
1
- /**
2
- * Data Factory — CSV/file fixture generator (5b).
3
- *
4
- * Turns a field-map into a **bulk-import fixture file** (a CSV whose columns are the fields and whose
5
- * rows are standardized data), for testing CSV/Excel import, bulk upload, etc. — the `CSV / Import /
6
- * Export` checklist area. Two products:
7
- * • the VALID fixture — N rows of valid values (should import cleanly). Unique fields get a
8
- * per-row suffix so rows don't collide.
9
- * • the INVALID fixture — one row per field's first invalid case (all-valid except that one field),
10
- * plus a trailing `_violation` column naming the field + reason + expected error — so a row-level
11
- * import-rejection test knows which row should fail and why.
12
- *
13
- * Deterministic: same field-map + catalog → same CSV. Columns are the flattened leaf paths (nested
14
- * groups → dotted headers, e.g. `address.city`). Reference the file from an `@api` upload (`files:`)
15
- * or a UI file-input step.
16
- */
17
- import type { Catalog } from './types';
18
- import { expandFieldMap, type FieldMap, type ExpandedField } from './generator';
19
-
20
- interface Leaf { path: string; field: ExpandedField }
21
-
22
- /** Flatten nested groups to leaf fields with dotted-path headers. */
23
- function leaves(expanded: ExpandedField[], prefix = ''): Leaf[] {
24
- const out: Leaf[] = [];
25
- for (const f of expanded) {
26
- const path = prefix ? `${prefix}.${f.name}` : f.name;
27
- if (f.children) out.push(...leaves(f.children, path));
28
- else out.push({ path, field: f });
29
- }
30
- return out;
31
- }
32
-
33
- const validOf = (f: ExpandedField): string => f.points.find((p) => p.group === 'valid' && p.value != null)?.value ?? '';
34
- const firstInvalid = (f: ExpandedField) => f.points.find((p) => p.group === 'invalid' && p.value != null);
35
-
36
- /** RFC-4180-ish CSV cell quoting. */
37
- function cell(v: string): string {
38
- return /[",\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v;
39
- }
40
- function row(cells: string[]): string {
41
- return cells.map(cell).join(',');
42
- }
43
-
44
- export interface FixtureResult {
45
- valid: string; // CSV text (header + N valid rows)
46
- invalid?: string; // CSV text (header + _violation + one bad row per field)
47
- headers: string[];
48
- }
49
-
50
- /** Build the CSV fixture(s) from a field-map. */
51
- export function generateFixture(map: FieldMap, catalog: Catalog, opts: { rows?: number; includeInvalid?: boolean } = {}): FixtureResult {
52
- const rows = Math.max(1, opts.rows ?? 3);
53
- const ls = leaves(expandFieldMap(map, catalog));
54
- const headers = ls.map((l) => l.path);
55
-
56
- // VALID rows — resolve the valid value per field; a `unique` field's `{{$timestamp}}` token is a
57
- // runtime var, not valid in a static file, so replace it with the row index to keep rows distinct.
58
- const validRows: string[] = [];
59
- for (let i = 1; i <= rows; i++) {
60
- validRows.push(row(ls.map((l) => validOf(l.field).replace(/\{\{\$timestamp\}\}/g, String(i)))));
61
- }
62
- const valid = [row(headers), ...validRows].join('\n') + '\n';
63
-
64
- let invalid: string | undefined;
65
- if (opts.includeInvalid) {
66
- const invHeaders = [...headers, '_violation'];
67
- const invRows: string[] = [];
68
- for (const target of ls) {
69
- const bad = firstInvalid(target.field);
70
- if (!bad) continue; // no concrete invalid value to seed
71
- const cells = ls.map((l) => (l === target ? (bad.value as string) : validOf(l.field).replace(/\{\{\$timestamp\}\}/g, '1')));
72
- const why = `${target.path}: ${bad.reason || 'invalid'}${bad.expectedError ? ` (${bad.expectedError})` : ''}`;
73
- invRows.push(row([...cells, why]));
74
- }
75
- invalid = [row(invHeaders), ...invRows].join('\n') + '\n';
76
- }
77
-
78
- return { valid, invalid, headers };
79
- }
package/src/readme.ts DELETED
@@ -1,52 +0,0 @@
1
- /**
2
- * Drops a `qa/data-factory/README.md` into the user's project so the folder explains itself.
3
- * Write-if-absent (never clobbers a user-edited README), like a config file.
4
- */
5
- import { existsSync, mkdirSync, writeFileSync } from 'fs';
6
- import { join } from 'path';
7
-
8
- const README = `# \`qa/data-factory/\` — Data Factory (your project's test-data knowledge)
9
-
10
- This folder is your project's **test-data standard**: machine-readable data rules that
11
- \`sungen data gen\` / \`/sungen:create-data-test\` (and \`/sungen:create-test\` step 5.3) expand into
12
- standardized test-data — **valid / boundary / invalid** values, each carrying a \`CHK-*\` trace id back
13
- to the spec. It is a reusable asset that **survives \`sungen update\`** (only the driver's shipped
14
- catalog is refreshed; nothing here is touched).
15
-
16
- It is **not** a copy of your spec — it is the structured, reusable data-rules layer the deterministic
17
- generator needs, complementary to \`requirements/spec.md\` (which is prose spec per screen/flow).
18
-
19
- ## What lives here
20
-
21
- | File | What it is |
22
- |---|---|
23
- | \`<name>.fields.yaml\` | **Field-map** for a screen/flow — one entry per input field: its catalog \`type\`, the project's real \`constraints\`, an \`errorMap\` (catalog placeholder code → your real message code), and optionally \`unique: true\`, nested \`fields:\`, \`dependsOn:\`. Authored from \`requirements/spec.md\`; expanded deterministically. A unit with **no input fields** (navigation/list) has no field-map — that's correct. |
24
- | \`common.overwrite.yaml\` | **Project-wide overrides** to the shipped catalog — your real length limits, real error codes, banned values, extra invalid cases. Deep-merged over the common (later-wins, keyed by \`type\` + data-point \`id\`). Genuinely new project knowledge the shipped catalog can't have. |
25
- | \`project/<entity>.yaml\` | **Blind-spot answers** — business enums, cross-entity rules, data models the catalog can't know. You fill these when Data Factory hits a field \`type\` it doesn't have (instead of it guessing). |
26
- | \`recipes.yaml\` | Seed recipes for the \`@dataFactory:<recipe>\` annotation — named field sets bound into \`{{recipe.*}}\` at runtime. |
27
-
28
- ## Good to know
29
-
30
- - **\`sungen data gen\` merges** into \`test-data/<name>.yaml\` — it replaces only the field keys it owns
31
- and preserves everything else (create-test data, flow-namespaced keys, \`@cases\`, overrides).
32
- - **\`unique: true\`** makes a field's valid value fresh per run (\`{{$timestamp}}\`) so a register
33
- email/username never collides — no cross-run cache.
34
- - \`sungen data lint\` checks coverage; \`sungen data fixture\` builds a CSV import fixture under
35
- \`qa/fixtures/\`.
36
- - Edit the field-map / \`.overwrite\` and re-run — never hand-edit the generated \`test-data\` values.
37
-
38
- Full guide: https://sungen.sun-asterisk.vn/docs/guides/data-factory
39
- `;
40
-
41
- /** Ensure `qa/data-factory/README.md` exists (write-if-absent). */
42
- export function ensureDataFactoryReadme(cwd: string): void {
43
- const dir = join(cwd, 'qa', 'data-factory');
44
- const path = join(dir, 'README.md');
45
- if (existsSync(path)) return;
46
- try {
47
- mkdirSync(dir, { recursive: true });
48
- writeFileSync(path, README, 'utf8');
49
- } catch {
50
- /* best-effort — a missing README must never fail a data command */
51
- }
52
- }