@sungen/driver-data-factory 3.2.12-beta.7 → 3.2.12-beta.9

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.12-beta.7",
3
+ "version": "3.2.12-beta.9",
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.12-beta.7",
17
+ "@sun-asterisk/sungen": "3.2.12-beta.9",
18
18
  "yaml": "^2.4.0"
19
19
  },
20
20
  "files": [
package/src/cli.ts CHANGED
@@ -13,8 +13,9 @@ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
13
13
  import { loadEffectiveCatalog, describeProvenance } from './catalog-loader';
14
14
  import { expandFieldMap, renderTestData, type FieldMap } from './generator';
15
15
  import { buildRequest, fingerprint } from './contract';
16
- import { loadStateRecipes, validateStateRecipe, buildStatePlan, renderPreconditionBlock, hasDatasource } from './state';
16
+ import { loadStateRecipes, validateStateRecipe, buildStatePlan, renderPreconditionBlock, hasDatasource, scaffoldRecipe, renderRecipeYaml } from './state';
17
17
  import { crosscheckUnit, crosscheckShared } from './crosscheck';
18
+ import { detectPreconditionSignals } from './detect';
18
19
  import { lintFieldMap, summarize } from './lint';
19
20
  import { validateGenerated } from './validate';
20
21
  import { generateFixture } from './fixture';
@@ -259,9 +260,34 @@ export function registerDataFactoryCommand(program: any): void {
259
260
  .command('state')
260
261
  .description('Domain/stateful data (M3-A Layer 0): validate the state-recipes in recipes.yaml and render each as a standardized manual precondition block (+ a resolved .state.yaml plan)')
261
262
  .option('--recipe <name>', 'Only this recipe (default: all recipes with a `resources`/`shared` graph)')
262
- .action((opts: { recipe?: string }) => {
263
+ .option('--scaffold <unit>', 'Auto-draft a state-recipe SKELETON from the unit\'s spec.md precondition signals (Layer-C auto-draft), then review/fill it')
264
+ .option('--force', 'With --scaffold: overwrite an existing recipe of the same name')
265
+ .action((opts: { recipe?: string; scaffold?: string; force?: boolean }) => {
263
266
  const cwd = process.cwd();
264
267
  ensureDataFactoryReadme(cwd);
268
+
269
+ // --scaffold: auto-draft a recipe skeleton from the unit's spec precondition signals.
270
+ if (opts.scaffold) {
271
+ const unit = opts.scaffold;
272
+ const specPath = ['screens', 'flows'].map((k) => join(cwd, 'qa', k, unit, 'requirements', 'spec.md')).find(existsSync);
273
+ const specText = specPath ? readFileSync(specPath, 'utf8') : '';
274
+ const signals = detectPreconditionSignals(specText);
275
+ const recipe = scaffoldRecipe(unit, signals);
276
+ if (!recipe) { console.log(`No stateful-precondition signals found in ${unit}'s spec — nothing to scaffold (a screen may simply have no precondition).`); return; }
277
+ const recipesPath = join(FIELDMAP_DIR(cwd), 'recipes.yaml');
278
+ const doc: any = existsSync(recipesPath) ? (parseYaml(readFileSync(recipesPath, 'utf8')) || {}) : {};
279
+ doc.recipes = doc.recipes || {};
280
+ if (doc.recipes[recipe.name] && !opts.force) { console.error(`✗ recipe "${recipe.name}" already exists — edit it, or pass --force to overwrite the scaffold.`); process.exitCode = 1; return; }
281
+ // Splice the human-friendly (commented) block rather than a bare re-serialize, to preserve TODO hints.
282
+ const header = existsSync(recipesPath) ? readFileSync(recipesPath, 'utf8').replace(/\s*$/, '') + '\n' : 'recipes:\n';
283
+ const block = renderRecipeYaml(recipe);
284
+ writeFileSync(recipesPath, (header.includes('recipes:') ? header : 'recipes:\n' + header) + block, 'utf8');
285
+ console.log(` ✓ scaffolded recipe "${recipe.name}" → ${recipesPath.replace(cwd + '/', '')} (${signals.length} signal(s))`);
286
+ console.log(` → REVIEW: name each entity, set desiredState/bindings, confirm domain values with QA, then \`sungen data state --recipe ${recipe.name}\`.`);
287
+ for (const s of signals) console.log(` · signal: ${s}`);
288
+ return;
289
+ }
290
+
265
291
  let recipes = loadStateRecipes(cwd);
266
292
  if (opts.recipe) recipes = recipes.filter((r) => r.name === opts.recipe);
267
293
  if (recipes.length === 0) { console.error('No state-recipes (recipes with a `resources` or `shared` graph) in qa/data-factory/recipes.yaml.'); process.exitCode = 1; return; }
package/src/detect.ts ADDED
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Data Factory — auto-detection (Layer A of the auto-orchestration design).
3
+ *
4
+ * Makes Sungen NOTICE what test-data work a unit needs, without QA having to know to run a command:
5
+ * - a unit that HAS input fields but has NO field-map → its data was never standardized;
6
+ * - a spec that describes a STATEFUL PRECONDITION (a required entity/state) with no state-recipe and
7
+ * no Background/@manual precondition → the stateful setup is undeclared.
8
+ * Both are surfaced as advisory findings at `sungen generate`/`sungen audit` (the DF advisory sensor).
9
+ * Deterministic heuristics over spec.md + the feature; no runtime, no LLM. Advisory only — a nudge +
10
+ * an auto-draft prompt, never a hard fail (the fuzzy precondition scan must not block).
11
+ */
12
+ import { join } from 'path';
13
+ import { existsSync, readFileSync, readdirSync } from 'fs';
14
+ import { loadStateRecipes } from './state';
15
+
16
+ export interface DetectFinding {
17
+ code: 'UNSTANDARDIZED' | 'PRECONDITION_UNDECLARED';
18
+ scenario: string;
19
+ message: string;
20
+ severity: 'warn' | 'info';
21
+ }
22
+
23
+ function readIfExists(path: string): string {
24
+ try { return existsSync(path) ? readFileSync(path, 'utf8') : ''; } catch { return ''; }
25
+ }
26
+
27
+ function readSpec(dir: string): string {
28
+ return readIfExists(join(dir, 'requirements', 'spec.md'));
29
+ }
30
+
31
+ function readFeature(dir: string): string {
32
+ const fdir = join(dir, 'features');
33
+ if (!existsSync(fdir)) return '';
34
+ try {
35
+ return readdirSync(fdir).filter((f) => f.endsWith('.feature')).map((f) => readIfExists(join(fdir, f))).join('\n');
36
+ } catch { return ''; }
37
+ }
38
+
39
+ /**
40
+ * Does the unit take user input? Strongest signal: a Gherkin `fill [X] field` / `type` step; plus a
41
+ * spec field table row typed as input/select/textarea. A pure navigation / list / capture screen has
42
+ * none → we must NOT nag it ("no input fields → nothing to generate").
43
+ */
44
+ export function hasInputFields(specText: string, featureText: string): boolean {
45
+ if (/\b(fill|type|enter|input|select|choose)\b\s+\[[^\]]+\]\s*(field|input|dropdown|textbox)?/i.test(featureText)) return true;
46
+ if (/\|\s*(input|select|textarea|checkbox|radio|combobox)\b/i.test(specText)) return true;
47
+ if (/\binput\s*\(|\bselect\s*\(|\btextarea\b/i.test(specText)) return true;
48
+ return false;
49
+ }
50
+
51
+ /** A field-map exists for this unit. */
52
+ export function hasFieldMap(cwd: string, name: string): boolean {
53
+ return existsSync(join(cwd, 'qa', 'data-factory', `${name}.fields.yaml`));
54
+ }
55
+
56
+ /** Precondition signal phrases — a spec sentence hinting the test needs a pre-existing entity/state. */
57
+ const PRECONDITION_SIGNALS = [
58
+ 'requires', 'required before', 'precondition', 'pre-condition', 'prerequisite',
59
+ 'logged in', 'authenticated', 'signed in', 'must be logged',
60
+ 'already exists', 'already registered', 'existing ', 'pre-existing', 'must exist',
61
+ 'reset token', 'verification', 'provisional account', 'session-storage', 'session storage',
62
+ 'buffer', 'seeded', 'must have', 'previously', 'valid token', 'auth token',
63
+ ];
64
+
65
+ /** Extract spec sentences that look like a stateful precondition (deduped, capped). */
66
+ export function detectPreconditionSignals(specText: string): string[] {
67
+ if (!specText) return [];
68
+ const hits: string[] = [];
69
+ const seen = new Set<string>();
70
+ // Split into sentence-ish units (lines / bullet items / sentences).
71
+ const units = specText.split(/\n|(?<=[.。])\s+/).map((s) => s.replace(/\s+/g, ' ').trim()).filter(Boolean);
72
+ for (const u of units) {
73
+ const low = u.toLowerCase();
74
+ // Skip markdown headings/table separators, and spec-template meta lines (US labels, rationale)
75
+ // that mention auth/priority only descriptively — not an actual precondition.
76
+ if (/^#{1,6}\s|^\|?\s*-{3,}/.test(u)) continue;
77
+ if (/^\*\*(why|independent test|acceptance|business context|target users|priority)\b/i.test(u)) continue;
78
+ if (/\bpriority\b/i.test(low) && /\bp[0-9]\b/i.test(low)) continue;
79
+ const sig = PRECONDITION_SIGNALS.find((s) => low.includes(s));
80
+ if (sig) {
81
+ const key = low.slice(0, 60);
82
+ if (!seen.has(key)) { seen.add(key); hits.push(u.length > 160 ? u.slice(0, 157) + '…' : u); }
83
+ }
84
+ if (hits.length >= 4) break;
85
+ }
86
+ return hits;
87
+ }
88
+
89
+ /** Is a stateful precondition already declared — a state-recipe with resources, or a Background/@manual? */
90
+ export function hasStateDeclaration(cwd: string, featureText: string): boolean {
91
+ if (/^\s*Background:/m.test(featureText) || /@manual\b/.test(featureText)) return true;
92
+ return loadStateRecipes(cwd).some((r) => (r.resources && r.resources.length) || r.shared);
93
+ }
94
+
95
+ /** Run Layer-A detection for one unit dir. Returns advisory findings (possibly empty). */
96
+ export function detectUnit(cwd: string, dir: string, name: string): DetectFinding[] {
97
+ const spec = readSpec(dir);
98
+ const feature = readFeature(dir);
99
+ const out: DetectFinding[] = [];
100
+
101
+ // 1. Inputs exist but never standardized.
102
+ if (!hasFieldMap(cwd, name) && hasInputFields(spec, feature)) {
103
+ out.push({
104
+ code: 'UNSTANDARDIZED',
105
+ scenario: name,
106
+ severity: 'warn',
107
+ message: `DATA-FACTORY-UNSTANDARDIZED: ${name} has input fields but no standardized test-data (no qa/data-factory/${name}.fields.yaml) — run \`/sungen:create-data-test ${name}\` (or it is authored inline by \`/sungen:create-test\`) so values are standardized valid/boundary/invalid, not hand-invented.`,
108
+ });
109
+ }
110
+
111
+ // 2. Stateful precondition described in the spec but not declared anywhere.
112
+ const signals = detectPreconditionSignals(spec);
113
+ if (signals.length && !hasStateDeclaration(cwd, feature)) {
114
+ out.push({
115
+ code: 'PRECONDITION_UNDECLARED',
116
+ scenario: name,
117
+ severity: 'info',
118
+ message: `DATA-FACTORY-PRECONDITION-UNDECLARED: ${name}'s spec describes a stateful precondition but the test declares none (no state-recipe, no Background/@manual). Declare it with a state-recipe + \`sungen data state\` so it renders a @manual:data-setup Background. Signals: ${signals.map((s) => `"${s}"`).join('; ')}`,
119
+ });
120
+ }
121
+ return out;
122
+ }
package/src/index.ts CHANGED
@@ -30,10 +30,12 @@ export type { Validity, BoundaryPoint } from './boundary';
30
30
  export { evaluateWhen, inProfile, fieldFacts } from './applicability';
31
31
  export { generate, buildRequest, validateRequest, fingerprint, GENERATOR_VERSION, SCHEMA_VERSION } from './contract';
32
32
  export type { DataFactoryRequest, DataFactoryResult } from './contract';
33
- export { loadStateRecipes, validateStateRecipe, buildStatePlan, renderPreconditionBlock, topoSort, hasDatasource } from './state';
33
+ export { loadStateRecipes, validateStateRecipe, buildStatePlan, renderPreconditionBlock, topoSort, hasDatasource, scaffoldRecipe, renderRecipeYaml, guessProvenance } from './state';
34
34
  export type { StateRecipe, StateResource, StatePlan, StateFinding, Provenance, Lifecycle } from './state';
35
35
  export { crosscheckUnit, crosscheckShared, crosscheckAll, referencedVars } from './crosscheck';
36
36
  export type { CrossFinding } from './crosscheck';
37
+ export { detectUnit, hasInputFields, hasFieldMap, detectPreconditionSignals, hasStateDeclaration } from './detect';
38
+ export type { DetectFinding } from './detect';
37
39
  export { registerDataFactoryCommand } from './cli';
38
40
  export { generateFixture } from './fixture';
39
41
  export type { FixtureResult, FixtureExpected, ExpectedRow } from './fixture';
package/src/sensor.ts CHANGED
@@ -5,7 +5,10 @@
5
5
  * coverage lint as `sungen data lint` and surfaces the gaps in the generate output — so weak
6
6
  * test-data coverage (missing boundary/invalid, unresolved values, unmapped error codes, blind
7
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).
8
+ *
9
+ * Auto-detection (Layer A): even with NO field-map, it NOTICES what's missing — a unit with input
10
+ * fields but no standardized data, or a spec that describes a stateful precondition the test never
11
+ * declares — so Sungen nudges QA instead of staying silent (was: "no field-map → nothing to say").
9
12
  *
10
13
  * Advisory, not gate: the ContextRouter scopes gate sensors by the annotation tags a feature carries,
11
14
  * and test-data generation is a design-time concern with no `.feature` tag — so gating here would
@@ -18,6 +21,7 @@ import type { Sensor } from '@sun-asterisk/sungen';
18
21
  import { loadEffectiveCatalog } from './catalog-loader';
19
22
  import { lintFieldMap, summarize } from './lint';
20
23
  import { validateGenerated } from './validate';
24
+ import { detectUnit } from './detect';
21
25
  import type { FieldMap } from './generator';
22
26
 
23
27
  export const dataFactoryCoverageSensor: Sensor<{ dir: string; cwd: string }> = {
@@ -26,11 +30,21 @@ export const dataFactoryCoverageSensor: Sensor<{ dir: string; cwd: string }> = {
26
30
  kind: 'advisory',
27
31
  run: ({ dir, cwd }) => {
28
32
  const name = basename(dir);
33
+ // Layer A auto-detection — runs regardless of whether a field-map exists, so a unit that needs
34
+ // standardization or a declared precondition is NOTICED, not silently skipped.
35
+ const detected = detectUnit(cwd, dir, name).map((d) => ({
36
+ sensorId: 'data-factory-coverage',
37
+ capability: 'data-factory',
38
+ scenario: d.scenario,
39
+ message: d.message,
40
+ severity: d.severity,
41
+ }));
42
+
29
43
  const fieldMapPath = join(cwd, 'qa', 'data-factory', `${name}.fields.yaml`);
30
- if (!existsSync(fieldMapPath)) return []; // opt-in per unit — no field-map, nothing to say
44
+ if (!existsSync(fieldMapPath)) return detected; // no field-map: report only what detection noticed
31
45
  let map: FieldMap;
32
- try { map = parseYaml(readFileSync(fieldMapPath, 'utf8')) as FieldMap; } catch { return []; }
33
- if (!map || !Array.isArray(map.fields)) return [];
46
+ try { map = parseYaml(readFileSync(fieldMapPath, 'utf8')) as FieldMap; } catch { return detected; }
47
+ if (!map || !Array.isArray(map.fields)) return detected;
34
48
 
35
49
  const { catalog, warnings } = loadEffectiveCatalog(cwd);
36
50
  const { issues } = lintFieldMap(map, catalog);
@@ -71,6 +85,8 @@ export const dataFactoryCoverageSensor: Sensor<{ dir: string; cwd: string }> = {
71
85
  severity: s.errors ? 'warn' : 'info',
72
86
  });
73
87
  }
88
+ // Detection findings (e.g. an undeclared stateful precondition) are still relevant with a field-map.
89
+ findings.push(...detected);
74
90
  return findings;
75
91
  },
76
92
  };
package/src/state.ts CHANGED
@@ -171,6 +171,53 @@ export function hasDatasource(cwd: string): boolean {
171
171
  return existsSync(join(cwd, 'datasources.yaml')) || existsSync(join(cwd, 'qa', 'datasources.yaml'));
172
172
  }
173
173
 
174
+ /** Guess a provenance from a precondition sentence — best-effort; QA confirms. */
175
+ export function guessProvenance(signal: string): Provenance {
176
+ const s = signal.toLowerCase();
177
+ if (/session-storage|session storage|local ?storage|cookie|buffer/.test(s)) return 'client-storage';
178
+ if (/token|reset|verification|otp|invitation|seeded/.test(s)) return 'fixture';
179
+ if (/logged in|authenticated|signed in|auth token|session/.test(s)) return 'server-context';
180
+ if (/already|existing|registered|must exist|previously|prior/.test(s)) return 'prior-flow';
181
+ return 'manual';
182
+ }
183
+
184
+ /**
185
+ * Auto-scaffold a state-recipe SKELETON from a spec's precondition sentences (Layer-C auto-draft).
186
+ * Deterministic: one resource per detected signal, provenance guessed, entity left as a TODO for the
187
+ * AI/QA to fill (never fabricates a domain entity). Returns null when no precondition signal is found.
188
+ */
189
+ export function scaffoldRecipe(unit: string, signals: string[]): StateRecipe | null {
190
+ if (!signals.length) return null;
191
+ const resources: StateResource[] = signals.slice(0, 4).map((sig, i) => ({
192
+ ref: `r${i + 1}`,
193
+ entity: 'TODO', // ← name the real entity (from the signal); do not invent domain values
194
+ provenance: guessProvenance(sig),
195
+ lifecycle: 'reusable',
196
+ note: sig,
197
+ }));
198
+ return {
199
+ name: `${unit}_precondition`,
200
+ description: `stateful precondition for ${unit} — AUTO-SCAFFOLDED from spec; REVIEW: name each entity, set desiredState/bindings, confirm with QA`,
201
+ resources,
202
+ };
203
+ }
204
+
205
+ /** Serialize a scaffolded recipe as a YAML block to splice under `recipes:` (2-space indented). */
206
+ export function renderRecipeYaml(recipe: StateRecipe): string {
207
+ const lines: string[] = [` ${recipe.name}:`];
208
+ if (recipe.description) lines.push(` description: "${recipe.description}"`);
209
+ lines.push(' resources:');
210
+ for (const r of recipe.resources || []) {
211
+ lines.push(` - ref: ${r.ref}`);
212
+ lines.push(` entity: ${r.entity} # ← name the real entity`);
213
+ lines.push(` provenance: ${r.provenance} # guessed — confirm (prior-flow|fixture|client-storage|server-context|manual|api|db)`);
214
+ if (r.lifecycle) lines.push(` lifecycle: ${r.lifecycle}`);
215
+ if (r.note) lines.push(` note: ${JSON.stringify(r.note)}`);
216
+ }
217
+ lines.push(' # bindings: { someId: "${r1.id}" } # ← wire ids the test needs');
218
+ return lines.join('\n') + '\n';
219
+ }
220
+
174
221
  /** Render the precondition block for weaving into a Gherkin Background (with the @manual tag if needed). */
175
222
  export function renderPreconditionBlock(plan: StatePlan): string {
176
223
  const tag = plan.manual ? '# @manual:data-setup — required state (auto-provisions when an api/db datasource is added)' : '# required state (Layer 1: auto-provisioned via the referenced endpoints)';