@sun-asterisk/sungen 3.0.0-beta.74 → 3.0.0-beta.75

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 (41) hide show
  1. package/dist/cli/commands/audit.d.ts.map +1 -1
  2. package/dist/cli/commands/audit.js +17 -3
  3. package/dist/cli/commands/audit.js.map +1 -1
  4. package/dist/generators/gherkin-parser/index.d.ts +2 -0
  5. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  6. package/dist/generators/gherkin-parser/index.js +15 -0
  7. package/dist/generators/gherkin-parser/index.js.map +1 -1
  8. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-element.hbs +8 -0
  9. package/dist/generators/test-generator/patterns/capture-patterns.d.ts +5 -0
  10. package/dist/generators/test-generator/patterns/capture-patterns.d.ts.map +1 -1
  11. package/dist/generators/test-generator/patterns/capture-patterns.js +33 -0
  12. package/dist/generators/test-generator/patterns/capture-patterns.js.map +1 -1
  13. package/dist/harness/audit.d.ts +5 -1
  14. package/dist/harness/audit.d.ts.map +1 -1
  15. package/dist/harness/audit.js +13 -2
  16. package/dist/harness/audit.js.map +1 -1
  17. package/dist/harness/parse.d.ts +1 -0
  18. package/dist/harness/parse.d.ts.map +1 -1
  19. package/dist/harness/parse.js +3 -0
  20. package/dist/harness/parse.js.map +1 -1
  21. package/dist/harness/provenance.d.ts +6 -0
  22. package/dist/harness/provenance.d.ts.map +1 -0
  23. package/dist/harness/provenance.js +65 -0
  24. package/dist/harness/provenance.js.map +1 -0
  25. package/dist/harness/sensors.d.ts +30 -0
  26. package/dist/harness/sensors.d.ts.map +1 -1
  27. package/dist/harness/sensors.js +122 -0
  28. package/dist/harness/sensors.js.map +1 -1
  29. package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +12 -0
  30. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +12 -0
  31. package/package.json +3 -3
  32. package/src/cli/commands/audit.ts +13 -3
  33. package/src/generators/gherkin-parser/index.ts +17 -0
  34. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-element.hbs +8 -0
  35. package/src/generators/test-generator/patterns/capture-patterns.ts +38 -0
  36. package/src/harness/audit.ts +18 -4
  37. package/src/harness/parse.ts +4 -0
  38. package/src/harness/provenance.ts +33 -0
  39. package/src/harness/sensors.ts +189 -0
  40. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +12 -0
  41. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +12 -0
@@ -277,3 +277,192 @@ export function traceability(scenarios: ScenarioInfo[], viewpoints: ViewpointEnt
277
277
  : 'Traceable.',
278
278
  };
279
279
  }
280
+
281
+ // ---------- Claim-Proof (harness-roadmap §0.5 Q1) ----------
282
+ //
283
+ // A scenario's TITLE makes a claim; its STEPS must prove it. This is the
284
+ // design-layer sibling of script-check's anti-bypass (code↔Gherkin): here it is
285
+ // title↔steps. We only measure scenarios whose title carries a claim keyword, so
286
+ // plain visibility/navigation scenarios are never falsely pressured. @manual
287
+ // scenarios are skipped — their claim is deferred to a flow (XS/flow-check owns it).
288
+
289
+ export interface ClaimUnproven {
290
+ name: string;
291
+ claim: string; // the claim keyword matched in the title
292
+ need: string; // the assertion shape the steps must contain
293
+ hint: string; // how to fix
294
+ severity: 'fail' | 'warn';
295
+ }
296
+
297
+ export interface ClaimProofResult {
298
+ total: number; // non-manual scenarios
299
+ withClaims: number; // scenarios whose title carries a claim
300
+ proven: number;
301
+ unproven: ClaimUnproven[];
302
+ ratio: number; // proven / withClaims (1 if withClaims === 0)
303
+ focus: string;
304
+ threshold: number;
305
+ verdict: DepthVerdict;
306
+ }
307
+
308
+ interface ClaimRule {
309
+ claim: string;
310
+ title: RegExp; // claim keyword(s) in the title
311
+ proof: RegExp; // required assertion shape in the steps
312
+ need: string;
313
+ hint: string;
314
+ severity: 'fail' | 'warn';
315
+ // Optional: even when `proof` matches, if `underproof` also matches the proof is
316
+ // PARTIAL (the right shape exists but on the wrong/another element) → a warn-level
317
+ // finding. e.g. an "each card exposes <action>" claim that has a see-all on price
318
+ // but asserts the actions with singular presence.
319
+ underproof?: RegExp;
320
+ underNeed?: string;
321
+ underHint?: string;
322
+ }
323
+
324
+ // Ordered by specificity; the first matching rule per scenario wins.
325
+ const CLAIM_RULES: ClaimRule[] = [
326
+ {
327
+ claim: 'all/every/each',
328
+ title: /\b(all|every|each)\b/,
329
+ proof: /\bsee all\b/,
330
+ need: 'a `User see all …` assertion (every element, not one)',
331
+ hint: 'assert across all matched elements — for per-card structure use the all-card DSL (roadmap Q2).',
332
+ severity: 'fail',
333
+ // singular action presence under an "each/every" claim = partial proof
334
+ underproof: /\bsee \[[^\]]+\] (link|button)\b/,
335
+ underNeed: 'an all-card assertion for the claimed action (not single presence)',
336
+ underHint: 'the actions are asserted once, not per-card — wrap them with the all-card DSL `User see all [Card] contain [Action]` (roadmap Q2).',
337
+ },
338
+ {
339
+ claim: 'only/belong-to',
340
+ title: /\b(only|belongs? to|exclusively)\b/,
341
+ proof: /\bsee all\b[^.]*\bcontain/,
342
+ need: 'all-match (`User see all … contains …`)',
343
+ hint: 'all displayed items must match the facet; "no items OUTSIDE" is UI-limited → verify via hybrid API/DB or mark @manual with reason.',
344
+ severity: 'warn',
345
+ },
346
+ {
347
+ claim: 'single/exactly-one',
348
+ title: /\b(a single|single|exactly one|just one|only one)\b/,
349
+ proof: /\bcount\b|\btable with\b|\bexactly\b|tohavecount/,
350
+ need: 'a count assertion (e.g. count is 1)',
351
+ hint: 'assert the quantity, not just presence.',
352
+ severity: 'fail',
353
+ },
354
+ {
355
+ claim: 'correct/matching/identity',
356
+ title: /\b(correct|matching|the same|same (product|name|price|value|item)|consistency|identity|lands? in|is the (product|item))\b/,
357
+ proof: /\bremember\b/,
358
+ need: 'capture + compare (`User remember … as {{v}}` then assert {{v}})',
359
+ hint: 'remember the value on the source, then assert it on the target (cross-screen → defer to a flow as @manual).',
360
+ severity: 'fail',
361
+ },
362
+ {
363
+ claim: 'changes/toggles',
364
+ title: /\b(changes?|toggles?|updates?|reflects?|reopens?)\b/,
365
+ proof: /\bremember\b|\bis hidden\b/,
366
+ need: 'a state delta (before ≠ after, or a visible→hidden contrast)',
367
+ hint: 'capture the before-state and assert the after-state differs, or assert the visible/hidden transition.',
368
+ severity: 'warn',
369
+ },
370
+ {
371
+ claim: 'hidden/rejected/not-complete',
372
+ title: /\b(hidden|closed|dismiss(es|ed)?|does not|doesn't|not complete|rejected|inert)\b/,
373
+ proof: /\bis hidden\b|\bare hidden\b|message is hidden|not complete|\bhidden\b/,
374
+ need: 'a negative / hidden assertion (`… is hidden`)',
375
+ hint: 'assert the absence/hidden state that the title claims, not just an unrelated visible element.',
376
+ severity: 'fail',
377
+ },
378
+ ];
379
+
380
+ // ---------- Viewpoint taxonomy-lint (harness-roadmap §0.5 Q3) ----------
381
+ //
382
+ // A scenario's VP-code prefix (VP-CART-001 → CART) should match what the title is
383
+ // actually about, or the coverage matrix / per-viewpoint score is skewed. Conservative:
384
+ // flag ONLY when the title carries NO signal for its own category AND exactly one OTHER
385
+ // known category's signal — so a legitimately mixed title (e.g. a LIST card that also
386
+ // mentions an action) is never flagged. LOGIC/UI are flexible lenses → not linted.
387
+
388
+ export interface TaxonomyFinding {
389
+ name: string;
390
+ current: string; // category from the VP code
391
+ suggested: string; // category the title signals
392
+ signal: string; // the phrase that triggered the suggestion
393
+ }
394
+
395
+ export interface TaxonomyResult {
396
+ total: number;
397
+ checked: number; // scenarios with a known, lintable category
398
+ mislabeled: TaxonomyFinding[];
399
+ }
400
+
401
+ const TAXONOMY_KNOWN = new Set(['LIST', 'PRODUCT', 'CART', 'FILTER', 'NAV', 'VAL', 'SEC']);
402
+
403
+ const CATEGORY_SIGNALS: { cat: string; re: RegExp }[] = [
404
+ { cat: 'FILTER', re: /\b(categor(y|ies)|sub-?categor\w*|expand\w*|collaps\w*|\bfacet\b|filter\w*|belongs? to)\b/ },
405
+ { cat: 'CART', re: /\b(add(ed|ing)? to cart|added (dialog|modal|message|confirmation)|cart (page|table|line|item)|view cart|continue shopping|quantit)\b/ },
406
+ { cat: 'PRODUCT', re: /\b(product detail|product-detail|view product|product page|same product|product name and price|product identity|detail product)\b/ },
407
+ { cat: 'LIST', re: /\b(product (list|grid)|featured product|product card|each (featured )?card|every (featured )?card)\b/ },
408
+ { cat: 'NAV', re: /\b(navigates?|returns the user|redirect\w*|logo returns|menu navigates)\b/ },
409
+ { cat: 'VAL', re: /\b(valid email|invalid\b|without (a |an )?@|without a domain|empty (email|input|field)|validation|required field)\b/ },
410
+ { cat: 'SEC', re: /\b(xss|sql|injection|payload|without authentication|unauthenticated|\binert\b|tamper\w*|malformed|url parameter|query param\w*|falls? back|gracefull?y|without a crash|not[- ]found|\b404\b|special character|abuse)\b/ },
411
+ ];
412
+
413
+ export function taxonomyLint(scenarios: ScenarioInfo[]): TaxonomyResult {
414
+ const mislabeled: TaxonomyFinding[] = [];
415
+ let checked = 0;
416
+ for (const s of scenarios) {
417
+ const cat = s.category;
418
+ if (!cat || !TAXONOMY_KNOWN.has(cat)) continue;
419
+ checked++;
420
+ const title = s.name.toLowerCase();
421
+ const matched = [...new Set(CATEGORY_SIGNALS.filter((c) => c.re.test(title)).map((c) => c.cat))];
422
+ if (matched.includes(cat)) continue; // title supports its own category → fine
423
+ const others = matched.filter((c) => c !== cat);
424
+ if (others.length === 1) { // unambiguous mismatch
425
+ const sig = CATEGORY_SIGNALS.find((c) => c.cat === others[0])!;
426
+ const m = title.match(sig.re);
427
+ mislabeled.push({ name: s.name, current: cat, suggested: others[0], signal: m ? m[0] : others[0] });
428
+ }
429
+ }
430
+ return { total: scenarios.length, checked, mislabeled };
431
+ }
432
+
433
+ export function claimProof(scenarios: ScenarioInfo[], focus = 'functional'): ClaimProofResult {
434
+ const measured = scenarios.filter((s) => !s.manual);
435
+ const unproven: ClaimUnproven[] = [];
436
+ let withClaims = 0;
437
+
438
+ for (const s of measured) {
439
+ const title = s.name.toLowerCase();
440
+ const rule = CLAIM_RULES.find((r) => r.title.test(title));
441
+ if (!rule) continue; // no claim → not measured
442
+ withClaims++;
443
+ if (!rule.proof.test(s.stepsText)) {
444
+ // proof shape entirely absent → fail at the rule's severity
445
+ unproven.push({ name: s.name, claim: rule.claim, need: rule.need, hint: rule.hint, severity: rule.severity });
446
+ } else if (rule.underproof && rule.underproof.test(s.stepsText)) {
447
+ // right shape exists but only as a partial proof → warn
448
+ unproven.push({
449
+ name: s.name, claim: rule.claim,
450
+ need: rule.underNeed ?? rule.need,
451
+ hint: rule.underHint ?? rule.hint,
452
+ severity: 'warn',
453
+ });
454
+ }
455
+ }
456
+
457
+ const proven = withClaims - unproven.length;
458
+ const ratio = withClaims ? proven / withClaims : 1;
459
+ const threshold = DEPTH_THRESHOLDS[focus] ?? DEPTH_THRESHOLDS.functional;
460
+
461
+ const hasFail = unproven.some((u) => u.severity === 'fail');
462
+ let verdict: DepthVerdict = 'pass';
463
+ if (ratio < threshold) {
464
+ verdict = WARN_ONLY_FOCUS.has(focus) || !hasFail ? 'warn' : 'fail';
465
+ }
466
+
467
+ return { total: measured.length, withClaims, proven, unproven, ratio, focus, threshold, verdict };
468
+ }
@@ -74,6 +74,18 @@ User switch to [T] frame | [main] frame
74
74
  # 8. Page: User see [T] page
75
75
  ```
76
76
 
77
+ ### Collection / all-card (P5)
78
+
79
+ ```
80
+ # Every element's TEXT matches a value (filter correctness, all items belong):
81
+ User see all [Result Product Name] text contains {{category_term}}
82
+ # Every CONTAINER holds a CHILD element (structural per-card proof — prove "each card has X"):
83
+ User see all [Product Card] contain [Product Name]
84
+ User see all [Product Card] contain [Add To Cart] button
85
+ ```
86
+
87
+ Use the all-card form whenever a title claims *every / each* card/row exposes something — a single `User see [Add To Cart] button` does NOT prove "each card" and the harness Claim-Proof gate will flag it.
88
+
77
89
  ### Table
78
90
 
79
91
  ```
@@ -74,6 +74,18 @@ User switch to [T] frame | [main] frame
74
74
  # 8. Page: User see [T] page
75
75
  ```
76
76
 
77
+ ### Collection / all-card (P5)
78
+
79
+ ```
80
+ # Every element's TEXT matches a value (filter correctness, all items belong):
81
+ User see all [Result Product Name] text contains {{category_term}}
82
+ # Every CONTAINER holds a CHILD element (structural per-card proof — prove "each card has X"):
83
+ User see all [Product Card] contain [Product Name]
84
+ User see all [Product Card] contain [Add To Cart] button
85
+ ```
86
+
87
+ Use the all-card form whenever a title claims *every / each* card/row exposes something — a single `User see [Add To Cart] button` does NOT prove "each card" and the harness Claim-Proof gate will flag it.
88
+
77
89
  ### Table
78
90
 
79
91
  ```