@sungen/driver-data-factory 3.2.10-beta.11

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.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Data Factory runtime binding — `@dataFactory:<recipe>` precondition codegen (P4, beta).
3
+ *
4
+ * A scenario tagged `@dataFactory:<recipe>` needs a standardized set of seed values available at
5
+ * runtime (M1 "missing data setup"). This emits a compile-time `testData.bind('<recipe>', {...})`
6
+ * with **valid** representative values drawn deterministically from the field-type catalog (+ the
7
+ * project's `.overwrite`), so the scenario's create steps (`@api:create…` or a UI form) consume
8
+ * `{{recipe.field}}` instead of hand-picked literals.
9
+ *
10
+ * Scope (beta): this binds the standardized DATA. The actual record CREATE is performed by the
11
+ * scenario's own steps — Data Factory does NOT open a DB/API connection or issue writes itself
12
+ * (that stays with the api/db drivers, whose datasource owns execution). Fully deterministic and
13
+ * backend-free: same recipe + catalog → same bound values. Full backend record-orchestration
14
+ * (calling a create endpoint from the recipe) is a later enhancement.
15
+ */
16
+ import { readFileSync, existsSync } from 'fs';
17
+ import { join } from 'path';
18
+ import { parse as parseYaml } from 'yaml';
19
+ import { loadEffectiveCatalog } from './catalog-loader';
20
+ import { expandField, type FieldDef } from './generator';
21
+
22
+ interface Recipe {
23
+ description?: string;
24
+ fields: FieldDef[];
25
+ }
26
+ interface RecipesFile { recipes?: Record<string, Recipe>; }
27
+
28
+ const TAG = /^@dataFactory:([A-Za-z_][A-Za-z0-9_]*)$/;
29
+
30
+ function loadRecipes(cwd: string): Record<string, Recipe> {
31
+ const p = join(cwd, 'qa', 'data-factory', 'recipes.yaml');
32
+ if (!existsSync(p)) return {};
33
+ try { return (parseYaml(readFileSync(p, 'utf8')) as RecipesFile)?.recipes || {}; } catch { return {}; }
34
+ }
35
+
36
+ /** Pick the first concrete valid value for a field from the catalog (deterministic). */
37
+ function validValue(field: FieldDef, catalog: ReturnType<typeof loadEffectiveCatalog>['catalog']): string | null {
38
+ const ex = expandField(field, catalog);
39
+ if (ex.blindSpot) return null;
40
+ const v = ex.points.find((p) => p.group === 'valid' && p.value != null);
41
+ return v?.value ?? null;
42
+ }
43
+
44
+ export function dfPreconditionCodegen(input: { tags: string[]; screenName: string; cwd: string }): Array<{ comment?: string; code: string; boundVars?: string[] }> {
45
+ const recipeTags = input.tags.map((t) => t.match(TAG)).filter(Boolean).map((m) => (m as RegExpMatchArray)[1]);
46
+ if (!recipeTags.length) return [];
47
+
48
+ const recipes = loadRecipes(input.cwd);
49
+ const { catalog } = loadEffectiveCatalog(input.cwd);
50
+ const out: Array<{ comment?: string; code: string; boundVars?: string[] }> = [];
51
+
52
+ for (const name of recipeTags) {
53
+ const recipe = recipes[name];
54
+ if (!recipe || !Array.isArray(recipe.fields)) {
55
+ // Fail-soft with a visible comment (mirrors how a missing catalog entry surfaces) — the scenario
56
+ // still compiles; the bind is empty so `{{name.*}}` resolves to nothing and the run flags it.
57
+ out.push({ comment: `@dataFactory:${name} — no recipe "${name}" in qa/data-factory/recipes.yaml (add it, or the bind is empty)`, code: `testData.bind(${JSON.stringify(name)}, {});`, boundVars: [name] });
58
+ continue;
59
+ }
60
+ const obj: Record<string, string | null> = {};
61
+ for (const f of recipe.fields) obj[f.name] = validValue(f, catalog);
62
+ const label = recipe.description ? `recipe "${name}" — ${recipe.description}` : `recipe "${name}"`;
63
+ out.push({
64
+ comment: `@dataFactory:${name} → bind {{${name}.*}} with standardized valid values (${label})`,
65
+ code: `testData.bind(${JSON.stringify(name)}, ${JSON.stringify(obj)});`,
66
+ boundVars: [name],
67
+ });
68
+ }
69
+ return out;
70
+ }
package/src/sensor.ts ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Data Factory coverage sensor — an ADVISORY harness sensor (runs at `sungen generate`, never blocks).
3
+ *
4
+ * For a screen/flow that has a field-map (`qa/data-factory/<name>.fields.yaml`), it runs the same
5
+ * coverage lint as `sungen data lint` and surfaces the gaps in the generate output — so weak
6
+ * test-data coverage (missing boundary/invalid, unresolved values, unmapped error codes, blind
7
+ * spots) is visible in the standard harness flow, not only when you remember to run `data lint`.
8
+ * A unit with no field-map produces no findings (Data Factory is opt-in per unit).
9
+ *
10
+ * Advisory, not gate: the ContextRouter scopes gate sensors by the annotation tags a feature carries,
11
+ * and test-data generation is a design-time concern with no `.feature` tag — so gating here would
12
+ * never route. Escalation to a gate axis (intent-aware threshold) is a later phase.
13
+ */
14
+ import { basename, join } from 'path';
15
+ import { existsSync, readFileSync } from 'fs';
16
+ import { parse as parseYaml } from 'yaml';
17
+ import type { Sensor } from '@sun-asterisk/sungen';
18
+ import { loadEffectiveCatalog } from './catalog-loader';
19
+ import { lintFieldMap, summarize } from './lint';
20
+ import type { FieldMap } from './generator';
21
+
22
+ export const dataFactoryCoverageSensor: Sensor<{ dir: string; cwd: string }> = {
23
+ id: 'data-factory-coverage',
24
+ capability: 'data-factory',
25
+ kind: 'advisory',
26
+ run: ({ dir, cwd }) => {
27
+ const name = basename(dir);
28
+ const fieldMapPath = join(cwd, 'qa', 'data-factory', `${name}.fields.yaml`);
29
+ if (!existsSync(fieldMapPath)) return []; // opt-in per unit — no field-map, nothing to say
30
+ let map: FieldMap;
31
+ try { map = parseYaml(readFileSync(fieldMapPath, 'utf8')) as FieldMap; } catch { return []; }
32
+ if (!map || !Array.isArray(map.fields)) return [];
33
+
34
+ const { catalog, warnings } = loadEffectiveCatalog(cwd);
35
+ const { issues } = lintFieldMap(map, catalog);
36
+ const findings = issues.map((i) => ({
37
+ sensorId: 'data-factory-coverage',
38
+ capability: 'data-factory',
39
+ scenario: i.field,
40
+ message: `DATA-FACTORY-${i.code}: ${i.message}`,
41
+ severity: i.severity, // error → shown; still advisory (generate never blocks)
42
+ }));
43
+ for (const w of warnings) {
44
+ findings.push({
45
+ sensorId: 'data-factory-coverage',
46
+ capability: 'data-factory',
47
+ scenario: w.fieldType,
48
+ message: `DATA-FACTORY-OVERWRITE-STALE: ${w.detail} → ${w.suggestion}`,
49
+ severity: 'warn',
50
+ });
51
+ }
52
+ if (findings.length) {
53
+ const s = summarize(issues);
54
+ findings.unshift({
55
+ sensorId: 'data-factory-coverage',
56
+ capability: 'data-factory',
57
+ scenario: name,
58
+ message: `DATA-FACTORY-COVERAGE: ${name} test-data — ${s.errors} error · ${s.warns} warn · ${s.infos} info (run \`sungen data lint --screen ${name}\`)`,
59
+ severity: s.errors ? 'warn' : 'info',
60
+ });
61
+ }
62
+ return findings;
63
+ },
64
+ };
package/src/types.ts ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Data Factory — catalog types. These mirror the YAML seed files under `common/`.
3
+ * Design-time only; no runtime deps.
4
+ */
5
+
6
+ /** One representative test value in a field-type's valid/boundary/invalid group. */
7
+ export interface DataPoint {
8
+ /** Traceability id, e.g. CHK-EMAIL-01. Unique within a field-type after overlay merge. */
9
+ id: string;
10
+ /** Literal value (present for concrete cases). */
11
+ value?: string;
12
+ /** Human instruction to synthesize the value when it depends on project constraints (e.g. "local@ đúng maxLocal"). */
13
+ gen?: string;
14
+ /** Why this value is invalid (invalid group only). */
15
+ reason?: string;
16
+ /** Placeholder message-code (Mxx). Mapped onto the project's real codes at generation; if the spec has none it becomes a chua-chot (open) point. */
17
+ errorRef?: string;
18
+ /** Pull values from a security bank (e.g. `security/sqli`) into this group. */
19
+ from?: string;
20
+ note?: string;
21
+ }
22
+
23
+ export interface FieldType {
24
+ type: string;
25
+ description?: string;
26
+ format?: string;
27
+ constraints?: Record<string, unknown>;
28
+ /** Security banks whose payloads are appended to the `invalid` group. */
29
+ uses_security?: string[];
30
+ valid?: DataPoint[];
31
+ boundary?: DataPoint[];
32
+ invalid?: DataPoint[];
33
+ notes?: string[];
34
+ }
35
+
36
+ export interface SecurityBank {
37
+ class: string;
38
+ description?: string;
39
+ payloads: Array<Record<string, unknown> & { id: string }>;
40
+ oracle?: string;
41
+ }
42
+
43
+ export interface Technique {
44
+ technique: string;
45
+ description?: string;
46
+ rule?: string;
47
+ applies_to?: string[];
48
+ [k: string]: unknown;
49
+ }
50
+
51
+ export interface Catalog {
52
+ fieldTypes: Record<string, FieldType>;
53
+ security: Record<string, SecurityBank>;
54
+ techniques: Record<string, Technique>;
55
+ }
56
+
57
+ /** A warning that a project's common.overwrite.yaml entry looks stale against the shipped common. */
58
+ export interface OverwriteWarning {
59
+ fieldType: string;
60
+ detail: string;
61
+ suggestion: string;
62
+ }