@sun-asterisk/sungen 2.7.0-beta.1 → 3.0.0-beta.72
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/README.md +1 -1
- package/dist/cli/commands/add.js +3 -3
- package/dist/cli/commands/add.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +3 -0
- package/dist/cli/commands/audit.d.ts.map +1 -0
- package/dist/cli/commands/audit.js +134 -0
- package/dist/cli/commands/audit.js.map +1 -0
- package/dist/cli/commands/blindspot.d.ts +3 -0
- package/dist/cli/commands/blindspot.d.ts.map +1 -0
- package/dist/cli/commands/blindspot.js +58 -0
- package/dist/cli/commands/blindspot.js.map +1 -0
- package/dist/cli/commands/capability.d.ts +3 -0
- package/dist/cli/commands/capability.d.ts.map +1 -0
- package/dist/cli/commands/capability.js +196 -0
- package/dist/cli/commands/capability.js.map +1 -0
- package/dist/cli/commands/challenge.d.ts +3 -0
- package/dist/cli/commands/challenge.d.ts.map +1 -0
- package/dist/cli/commands/challenge.js +102 -0
- package/dist/cli/commands/challenge.js.map +1 -0
- package/dist/cli/commands/feedback.d.ts +3 -0
- package/dist/cli/commands/feedback.d.ts.map +1 -0
- package/dist/cli/commands/feedback.js +72 -0
- package/dist/cli/commands/feedback.js.map +1 -0
- package/dist/cli/commands/flow-check.d.ts +3 -0
- package/dist/cli/commands/flow-check.d.ts.map +1 -0
- package/dist/cli/commands/flow-check.js +136 -0
- package/dist/cli/commands/flow-check.js.map +1 -0
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +50 -2
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/ledger.d.ts +3 -0
- package/dist/cli/commands/ledger.d.ts.map +1 -0
- package/dist/cli/commands/ledger.js +71 -0
- package/dist/cli/commands/ledger.js.map +1 -0
- package/dist/cli/commands/manifest.d.ts +3 -0
- package/dist/cli/commands/manifest.d.ts.map +1 -0
- package/dist/cli/commands/manifest.js +101 -0
- package/dist/cli/commands/manifest.js.map +1 -0
- package/dist/cli/commands/script-check.d.ts +3 -0
- package/dist/cli/commands/script-check.d.ts.map +1 -0
- package/dist/cli/commands/script-check.js +97 -0
- package/dist/cli/commands/script-check.js.map +1 -0
- package/dist/cli/commands/trace.d.ts +3 -0
- package/dist/cli/commands/trace.d.ts.map +1 -0
- package/dist/cli/commands/trace.js +110 -0
- package/dist/cli/commands/trace.js.map +1 -0
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +22 -9
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/index.js +20 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +1 -0
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-registry.d.ts +13 -0
- package/dist/generators/test-generator/adapters/adapter-registry.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-registry.js +73 -1
- package/dist/generators/test-generator/adapters/adapter-registry.js.map +1 -1
- package/dist/generators/test-generator/adapters/index.d.ts +1 -1
- package/dist/generators/test-generator/adapters/index.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/index.js +5 -1
- package/dist/generators/test-generator/adapters/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-assertion.hbs +7 -0
- package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +6 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +6 -2
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/patterns/capture-patterns.d.ts +16 -0
- package/dist/generators/test-generator/patterns/capture-patterns.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/capture-patterns.js +54 -0
- package/dist/generators/test-generator/patterns/capture-patterns.js.map +1 -0
- package/dist/generators/test-generator/patterns/form-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/form-patterns.js +3 -1
- package/dist/generators/test-generator/patterns/form-patterns.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +2 -0
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +1 -0
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.d.ts +5 -0
- package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.js +17 -0
- package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.js +4 -0
- package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +12 -6
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/harness/audit.d.ts +24 -0
- package/dist/harness/audit.d.ts.map +1 -0
- package/dist/harness/audit.js +115 -0
- package/dist/harness/audit.js.map +1 -0
- package/dist/harness/blindspot.d.ts +15 -0
- package/dist/harness/blindspot.d.ts.map +1 -0
- package/dist/harness/blindspot.js +85 -0
- package/dist/harness/blindspot.js.map +1 -0
- package/dist/harness/capability-plan.d.ts +49 -0
- package/dist/harness/capability-plan.d.ts.map +1 -0
- package/dist/harness/capability-plan.js +215 -0
- package/dist/harness/capability-plan.js.map +1 -0
- package/dist/harness/capability.d.ts +23 -0
- package/dist/harness/capability.d.ts.map +1 -0
- package/dist/harness/capability.js +98 -0
- package/dist/harness/capability.js.map +1 -0
- package/dist/harness/catalog/drivers.yaml +57 -0
- package/dist/harness/catalog/universal-viewpoints.yaml +114 -0
- package/dist/harness/challenge.d.ts +21 -0
- package/dist/harness/challenge.d.ts.map +1 -0
- package/dist/harness/challenge.js +151 -0
- package/dist/harness/challenge.js.map +1 -0
- package/dist/harness/feedback.d.ts +29 -0
- package/dist/harness/feedback.d.ts.map +1 -0
- package/dist/harness/feedback.js +106 -0
- package/dist/harness/feedback.js.map +1 -0
- package/dist/harness/flow-check.d.ts +23 -0
- package/dist/harness/flow-check.d.ts.map +1 -0
- package/dist/harness/flow-check.js +132 -0
- package/dist/harness/flow-check.js.map +1 -0
- package/dist/harness/flow-plan.d.ts +23 -0
- package/dist/harness/flow-plan.d.ts.map +1 -0
- package/dist/harness/flow-plan.js +166 -0
- package/dist/harness/flow-plan.js.map +1 -0
- package/dist/harness/intent.d.ts +11 -0
- package/dist/harness/intent.d.ts.map +1 -0
- package/dist/harness/intent.js +86 -0
- package/dist/harness/intent.js.map +1 -0
- package/dist/harness/ledger.d.ts +42 -0
- package/dist/harness/ledger.d.ts.map +1 -0
- package/dist/harness/ledger.js +171 -0
- package/dist/harness/ledger.js.map +1 -0
- package/dist/harness/manifest.d.ts +42 -0
- package/dist/harness/manifest.d.ts.map +1 -0
- package/dist/harness/manifest.js +209 -0
- package/dist/harness/manifest.js.map +1 -0
- package/dist/harness/parse.d.ts +22 -0
- package/dist/harness/parse.d.ts.map +1 -0
- package/dist/harness/parse.js +163 -0
- package/dist/harness/parse.js.map +1 -0
- package/dist/harness/script-check.d.ts +39 -0
- package/dist/harness/script-check.d.ts.map +1 -0
- package/dist/harness/script-check.js +251 -0
- package/dist/harness/script-check.js.map +1 -0
- package/dist/harness/secret-scan.d.ts +8 -0
- package/dist/harness/secret-scan.d.ts.map +1 -0
- package/dist/harness/secret-scan.js +88 -0
- package/dist/harness/secret-scan.js.map +1 -0
- package/dist/harness/sensors.d.ts +88 -0
- package/dist/harness/sensors.d.ts.map +1 -0
- package/dist/harness/sensors.js +232 -0
- package/dist/harness/sensors.js.map +1 -0
- package/dist/harness/trace.d.ts +31 -0
- package/dist/harness/trace.d.ts.map +1 -0
- package/dist/harness/trace.js +173 -0
- package/dist/harness/trace.js.map +1 -0
- package/dist/orchestrator/ai-rules-updater.d.ts +1 -0
- package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
- package/dist/orchestrator/ai-rules-updater.js +55 -11
- package/dist/orchestrator/ai-rules-updater.js.map +1 -1
- package/dist/orchestrator/figma/spec-figma-renderer.d.ts +2 -2
- package/dist/orchestrator/figma/spec-figma-renderer.js +2 -2
- package/dist/orchestrator/figma/spec-figma-section-renderers.d.ts +1 -1
- package/dist/orchestrator/figma/spec-figma-section-renderers.js +1 -1
- package/dist/orchestrator/project-initializer.d.ts +5 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +30 -6
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-agent-challenge.md +46 -0
- package/dist/orchestrator/templates/ai-instructions/claude-agent-discovery.md +32 -0
- package/dist/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +37 -0
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-flow.md +3 -3
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +5 -5
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +36 -12
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-design.md +12 -0
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-feedback.md +36 -0
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-review.md +27 -30
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +4 -1
- package/dist/orchestrator/templates/ai-instructions/claude-config.md +1 -4
- package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-mode-figma-mcp.md +82 -0
- package/dist/orchestrator/templates/ai-instructions/{github-skill-sungen-figma-source.md → claude-skill-capture-mode-figma-pat.md} +14 -48
- package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-mode-live.md +60 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-mode-local.md +38 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-capture.md +35 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +84 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +40 -1
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +3 -3
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +4 -4
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +18 -10
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-design.md +13 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-feedback.md +24 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-review.md +20 -30
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +2 -1
- package/dist/orchestrator/templates/ai-instructions/copilot-config.md +1 -4
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-figma-mcp.md +82 -0
- package/{src/orchestrator/templates/ai-instructions/claude-skill-figma-source.md → dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-figma-pat.md} +14 -48
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-live.md +60 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-local.md +38 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture.md +35 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +84 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +1 -1
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +40 -1
- package/dist/orchestrator/templates/specs-test-data.ts +20 -6
- package/dist/tools/figma/figma-auth.d.ts +5 -2
- package/dist/tools/figma/figma-auth.d.ts.map +1 -1
- package/dist/tools/figma/figma-auth.js +19 -9
- package/dist/tools/figma/figma-auth.js.map +1 -1
- package/docs/orchestration-spec.md +267 -0
- package/package.json +12 -7
- package/src/cli/commands/add.ts +3 -3
- package/src/cli/commands/audit.ts +92 -0
- package/src/cli/commands/blindspot.ts +48 -0
- package/src/cli/commands/capability.ts +160 -0
- package/src/cli/commands/challenge.ts +55 -0
- package/src/cli/commands/feedback.ts +65 -0
- package/src/cli/commands/flow-check.ts +97 -0
- package/src/cli/commands/generate.ts +47 -2
- package/src/cli/commands/ledger.ts +61 -0
- package/src/cli/commands/manifest.ts +55 -0
- package/src/cli/commands/script-check.ts +50 -0
- package/src/cli/commands/trace.ts +60 -0
- package/src/cli/commands/update.ts +30 -10
- package/src/cli/index.ts +20 -0
- package/src/generators/test-generator/adapters/adapter-interface.ts +1 -0
- package/src/generators/test-generator/adapters/adapter-registry.ts +37 -0
- package/src/generators/test-generator/adapters/index.ts +4 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-assertion.hbs +7 -0
- package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +6 -0
- package/src/generators/test-generator/code-generator.ts +6 -2
- package/src/generators/test-generator/patterns/capture-patterns.ts +59 -0
- package/src/generators/test-generator/patterns/form-patterns.ts +3 -1
- package/src/generators/test-generator/patterns/index.ts +2 -0
- package/src/generators/test-generator/step-mapper.ts +1 -0
- package/src/generators/test-generator/utils/data-resolver.ts +20 -0
- package/src/generators/test-generator/utils/runtime-data-transformer.ts +8 -0
- package/src/generators/test-generator/utils/selector-resolver.ts +13 -6
- package/src/harness/audit.ts +112 -0
- package/src/harness/blindspot.ts +51 -0
- package/src/harness/capability-plan.ts +180 -0
- package/src/harness/capability.ts +75 -0
- package/src/harness/catalog/drivers.yaml +57 -0
- package/src/harness/catalog/universal-viewpoints.yaml +114 -0
- package/src/harness/challenge.ts +131 -0
- package/src/harness/feedback.ts +84 -0
- package/src/harness/flow-check.ts +99 -0
- package/src/harness/flow-plan.ts +135 -0
- package/src/harness/intent.ts +58 -0
- package/src/harness/ledger.ts +155 -0
- package/src/harness/manifest.ts +173 -0
- package/src/harness/parse.ts +145 -0
- package/src/harness/script-check.ts +222 -0
- package/src/harness/secret-scan.ts +51 -0
- package/src/harness/sensors.ts +279 -0
- package/src/harness/trace.ts +138 -0
- package/src/orchestrator/ai-rules-updater.ts +57 -10
- package/src/orchestrator/figma/spec-figma-renderer.ts +2 -2
- package/src/orchestrator/figma/spec-figma-section-renderers.ts +1 -1
- package/src/orchestrator/project-initializer.ts +33 -7
- package/src/orchestrator/templates/ai-instructions/claude-agent-challenge.md +46 -0
- package/src/orchestrator/templates/ai-instructions/claude-agent-discovery.md +32 -0
- package/src/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +37 -0
- package/src/orchestrator/templates/ai-instructions/claude-cmd-add-flow.md +3 -3
- package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +5 -5
- package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +36 -12
- package/src/orchestrator/templates/ai-instructions/claude-cmd-design.md +12 -0
- package/src/orchestrator/templates/ai-instructions/claude-cmd-feedback.md +36 -0
- package/src/orchestrator/templates/ai-instructions/claude-cmd-review.md +27 -30
- package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +4 -1
- package/src/orchestrator/templates/ai-instructions/claude-config.md +1 -4
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture-mode-figma-mcp.md +82 -0
- package/{dist/orchestrator/templates/ai-instructions/copilot-skill-figma-source.md → src/orchestrator/templates/ai-instructions/claude-skill-capture-mode-figma-pat.md} +14 -48
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture-mode-live.md +60 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture-mode-local.md +38 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture.md +35 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +84 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +40 -1
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +3 -3
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +4 -4
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +18 -10
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-design.md +13 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-feedback.md +24 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-review.md +20 -30
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +2 -1
- package/src/orchestrator/templates/ai-instructions/copilot-config.md +1 -4
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-figma-mcp.md +82 -0
- package/{dist/orchestrator/templates/ai-instructions/claude-skill-figma-source.md → src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-figma-pat.md} +14 -48
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-live.md +60 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-local.md +38 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture.md +35 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +84 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +1 -1
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +40 -1
- package/src/orchestrator/templates/specs-test-data.ts +20 -6
- package/src/tools/figma/figma-auth.ts +20 -9
- package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-figma.md +0 -142
- package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-live.md +0 -112
- package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-local.md +0 -73
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-figma.md +0 -142
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-live.md +0 -112
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-local.md +0 -73
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture-figma.md +0 -142
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture-live.md +0 -112
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture-local.md +0 -73
- package/src/orchestrator/templates/ai-instructions/copilot-skill-figma-source.md +0 -151
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-figma.md +0 -142
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-live.md +0 -112
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-local.md +0 -73
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-figma-source.md +0 -151
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Script-check — verify the generated Playwright spec is a faithful 1:1 of the
|
|
3
|
+
* Gherkin feature, i.e. "the testcase and the test code are not two different things".
|
|
4
|
+
*
|
|
5
|
+
* Two deterministic checks:
|
|
6
|
+
* A. Structural 1:1 — every non-@manual / non-@steps scenario has exactly one
|
|
7
|
+
* matching `test('<title>')` block in the committed spec (and no extras).
|
|
8
|
+
* B. Drift — regenerate the spec from the SAME .feature + selectors + test-data
|
|
9
|
+
* into a temp dir and diff against the committed spec. Any difference means
|
|
10
|
+
* the committed spec was hand-edited or is stale (feature changed without a
|
|
11
|
+
* regenerate) → the script no longer reflects the testcase.
|
|
12
|
+
*
|
|
13
|
+
* Pure-deterministic (reuses the compiler). No AI.
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import * as os from 'os';
|
|
18
|
+
import { loadScenarios } from './parse';
|
|
19
|
+
|
|
20
|
+
export interface ScriptCheckResult {
|
|
21
|
+
screen: string;
|
|
22
|
+
specPath: string | null;
|
|
23
|
+
automatedScenarios: number; // non-manual, non-steps
|
|
24
|
+
manualScenarios: number;
|
|
25
|
+
specTestBlocks: number;
|
|
26
|
+
countMatch: boolean;
|
|
27
|
+
missingInSpec: string[]; // scenario titles with no test() block
|
|
28
|
+
extraInSpec: string[]; // test() titles with no scenario
|
|
29
|
+
drift: 'in-sync' | 'drift' | 'no-spec';
|
|
30
|
+
driftHunks: string[]; // sample differing lines (committed vs regenerated)
|
|
31
|
+
// C. Anti-bypass / faithfulness — a present test that proves nothing.
|
|
32
|
+
assertionlessTests: string[]; // non-manual test() with 0 expect()
|
|
33
|
+
hollowSteps: { test: string; step: string }[]; // a step-comment that emitted no code
|
|
34
|
+
bypass: boolean;
|
|
35
|
+
status: 'OK' | 'FAIL';
|
|
36
|
+
findings: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Split a spec into test() blocks (title + body) via brace counting. */
|
|
40
|
+
export function extractTestBlocks(specSrc: string): { title: string; body: string[] }[] {
|
|
41
|
+
const lines = specSrc.split('\n');
|
|
42
|
+
const blocks: { title: string; body: string[] }[] = [];
|
|
43
|
+
for (let i = 0; i < lines.length; i++) {
|
|
44
|
+
const m = lines[i].match(/\btest(?:\.(?:only|skip|fixme))?\(\s*(['"`])([^'"`]+)\1/);
|
|
45
|
+
if (!m) continue;
|
|
46
|
+
let depth = 0, started = false;
|
|
47
|
+
const body: string[] = [];
|
|
48
|
+
for (let j = i; j < lines.length; j++) {
|
|
49
|
+
for (const ch of lines[j]) { if (ch === '{') { depth++; started = true; } else if (ch === '}') depth--; }
|
|
50
|
+
body.push(lines[j]);
|
|
51
|
+
if (started && depth <= 0) break;
|
|
52
|
+
}
|
|
53
|
+
blocks.push({ title: m[2].trim(), body });
|
|
54
|
+
}
|
|
55
|
+
return blocks;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const CODE_LINE = /\b(await|expect|testData|page\.)\b|=\s*[^=]/;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Faithfulness / anti-bypass: a non-@manual test that performs actions but proves
|
|
62
|
+
* nothing (0 assertions), or a step-comment that emitted no executable code (hollow
|
|
63
|
+
* — the testcase step is not run). Structural 1:1 can't see these.
|
|
64
|
+
*/
|
|
65
|
+
export function analyzeFaithfulness(specSrc: string, automatedTitles: Set<string>) {
|
|
66
|
+
const assertionlessTests: string[] = [];
|
|
67
|
+
const hollowSteps: { test: string; step: string }[] = [];
|
|
68
|
+
for (const blk of extractTestBlocks(specSrc)) {
|
|
69
|
+
if (!automatedTitles.has(blk.title)) continue; // only non-@manual scenarios
|
|
70
|
+
const body = blk.body;
|
|
71
|
+
if (!body.some((l) => /expect\(/.test(l))) assertionlessTests.push(blk.title);
|
|
72
|
+
// hollow step: a `// step` whose region (until the NEXT step-comment / block end)
|
|
73
|
+
// contains no executable code. The region — not just the next line — is checked,
|
|
74
|
+
// so block-style steps (`// Assert all … { … expect … }`) are correctly counted.
|
|
75
|
+
const stepIdx: number[] = [];
|
|
76
|
+
for (let k = 1; k < body.length; k++) if (body[k].trim().startsWith('//')) stepIdx.push(k);
|
|
77
|
+
for (let s = 0; s < stepIdx.length; s++) {
|
|
78
|
+
const start = stepIdx[s] + 1;
|
|
79
|
+
const end = s + 1 < stepIdx.length ? stepIdx[s + 1] : body.length;
|
|
80
|
+
const hasCode = body.slice(start, end).some((l) => CODE_LINE.test(l));
|
|
81
|
+
if (!hasCode) hollowSteps.push({ test: blk.title, step: body[stepIdx[s]].trim().slice(0, 70) });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { assertionlessTests, hollowSteps };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function extractTestTitles(specSrc: string): string[] {
|
|
88
|
+
// Count real test cases only: test(...), test.only/.skip/.fixme(...).
|
|
89
|
+
// Exclude test.describe / test.beforeAll / hooks (not test cases).
|
|
90
|
+
const titles: string[] = [];
|
|
91
|
+
const re = /\btest(?:\.(?:only|skip|fixme))?\(\s*(['"`])([^'"`]+)\1/g;
|
|
92
|
+
let m: RegExpExecArray | null;
|
|
93
|
+
while ((m = re.exec(specSrc))) titles.push(m[2].trim());
|
|
94
|
+
return titles;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalize(src: string): string {
|
|
98
|
+
return src
|
|
99
|
+
.split('\n')
|
|
100
|
+
.map((l) => l.replace(/\s+$/, ''))
|
|
101
|
+
.join('\n')
|
|
102
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
103
|
+
.trim();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function findSpec(dir: string, name: string, flowMode: boolean): string | null {
|
|
107
|
+
// Screens compile to <dir>/<name>/<feature>.spec.ts
|
|
108
|
+
// Flows compile to <dir>/flows/<name>/<feature>.spec.ts
|
|
109
|
+
// Scope the search to THIS target's own subdir — otherwise the first spec of
|
|
110
|
+
// ANY other screen/flow is returned, which (for an uncompiled flow) falsely
|
|
111
|
+
// reports the wrong screen's tests as drift.
|
|
112
|
+
const hits: string[] = [];
|
|
113
|
+
const walk = (d: string) => {
|
|
114
|
+
if (!fs.existsSync(d)) return;
|
|
115
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
116
|
+
const p = path.join(d, e.name);
|
|
117
|
+
if (e.isDirectory()) walk(p);
|
|
118
|
+
else if (e.name.endsWith('.spec.ts')) hits.push(p);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const scoped = flowMode ? path.join(dir, 'flows', name) : path.join(dir, name);
|
|
122
|
+
if (!fs.existsSync(scoped)) return null; // no spec for this target (e.g. not compiled yet)
|
|
123
|
+
walk(scoped);
|
|
124
|
+
return hits[0] ?? null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function runScriptCheck(screenDir: string, screenName: string, flowMode: boolean): Promise<ScriptCheckResult> {
|
|
128
|
+
const featurePath = path.join(screenDir, 'features', `${screenName}.feature`);
|
|
129
|
+
const scenarios = loadScenarios(featurePath);
|
|
130
|
+
const automated = scenarios.filter((s) => !s.manual);
|
|
131
|
+
const manual = scenarios.filter((s) => s.manual);
|
|
132
|
+
|
|
133
|
+
const committedSpec = findSpec(path.join(process.cwd(), 'specs', 'generated'), screenName, flowMode);
|
|
134
|
+
|
|
135
|
+
const findings: string[] = [];
|
|
136
|
+
let specTitles: string[] = [];
|
|
137
|
+
let specSrc = '';
|
|
138
|
+
if (committedSpec) {
|
|
139
|
+
specSrc = fs.readFileSync(committedSpec, 'utf-8');
|
|
140
|
+
specTitles = extractTestTitles(specSrc);
|
|
141
|
+
} else {
|
|
142
|
+
findings.push('No generated spec found under specs/generated/ — run `sungen generate` / `/sungen:run-test` first.');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// A. Structural 1:1
|
|
146
|
+
const specTitleSet = new Set(specTitles);
|
|
147
|
+
const scenTitleSet = new Set(automated.map((s) => s.name));
|
|
148
|
+
const missingInSpec = automated.filter((s) => !specTitleSet.has(s.name)).map((s) => s.name);
|
|
149
|
+
const extraInSpec = specTitles.filter((t) => !scenTitleSet.has(t));
|
|
150
|
+
const countMatch = committedSpec ? automated.length === specTitles.length : false;
|
|
151
|
+
if (committedSpec && !countMatch) {
|
|
152
|
+
findings.push(`Count mismatch: ${automated.length} automated scenarios vs ${specTitles.length} test() blocks.`);
|
|
153
|
+
}
|
|
154
|
+
for (const t of missingInSpec) findings.push(`MISSING in spec: scenario "${t}" has no test() block (stale spec — regenerate).`);
|
|
155
|
+
for (const t of extraInSpec) findings.push(`EXTRA in spec: test "${t}" has no matching scenario (hand-edited spec).`);
|
|
156
|
+
|
|
157
|
+
// B. Drift — regenerate to temp and diff
|
|
158
|
+
let drift: ScriptCheckResult['drift'] = committedSpec ? 'in-sync' : 'no-spec';
|
|
159
|
+
const driftHunks: string[] = [];
|
|
160
|
+
if (committedSpec) {
|
|
161
|
+
try {
|
|
162
|
+
const { CodeGenerator } = require('../generators/test-generator/code-generator');
|
|
163
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sungen-scriptcheck-'));
|
|
164
|
+
const qaSourceDir = path.join(process.cwd(), 'qa', flowMode ? 'flows' : 'screens');
|
|
165
|
+
const gen = new CodeGenerator({ framework: 'playwright', screenName, runtimeData: true, flowMode });
|
|
166
|
+
await gen.generateAllTests(qaSourceDir, tmp, [featurePath]);
|
|
167
|
+
const fresh = findSpec(tmp, screenName, flowMode);
|
|
168
|
+
if (fresh) {
|
|
169
|
+
const a = normalize(specSrc);
|
|
170
|
+
const b = normalize(fs.readFileSync(fresh, 'utf-8'));
|
|
171
|
+
if (a !== b) {
|
|
172
|
+
drift = 'drift';
|
|
173
|
+
// collect a few differing lines
|
|
174
|
+
const al = a.split('\n'), bl = b.split('\n');
|
|
175
|
+
const max = Math.max(al.length, bl.length);
|
|
176
|
+
for (let i = 0, shown = 0; i < max && shown < 6; i++) {
|
|
177
|
+
if (al[i] !== bl[i]) {
|
|
178
|
+
driftHunks.push(` L${i + 1}\n committed: ${(al[i] ?? '∅').trim().slice(0, 100)}\n expected : ${(bl[i] ?? '∅').trim().slice(0, 100)}`);
|
|
179
|
+
shown++;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
findings.push('DRIFT: committed spec differs from a fresh regenerate → spec was hand-edited or the .feature changed without `sungen generate`. The test code no longer reflects the Gherkin.');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
186
|
+
} catch (e) {
|
|
187
|
+
findings.push(`Drift check skipped (regenerate failed): ${e instanceof Error ? e.message : e}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// C. Anti-bypass / faithfulness
|
|
192
|
+
const { assertionlessTests, hollowSteps } = committedSpec
|
|
193
|
+
? analyzeFaithfulness(specSrc, scenTitleSet)
|
|
194
|
+
: { assertionlessTests: [], hollowSteps: [] };
|
|
195
|
+
for (const t of assertionlessTests) {
|
|
196
|
+
findings.push(`BYPASS: test "${t}" has 0 assertions (action-only — proves nothing). The testcase is not really automated.`);
|
|
197
|
+
}
|
|
198
|
+
for (const h of hollowSteps) {
|
|
199
|
+
findings.push(`BYPASS: in "${h.test}", step "${h.step}" emitted no code (hollow — the testcase step is not executed).`);
|
|
200
|
+
}
|
|
201
|
+
const bypass = assertionlessTests.length > 0 || hollowSteps.length > 0;
|
|
202
|
+
|
|
203
|
+
const ok = !!committedSpec && countMatch && missingInSpec.length === 0 && extraInSpec.length === 0 && drift === 'in-sync' && !bypass;
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
screen: screenName,
|
|
207
|
+
specPath: committedSpec,
|
|
208
|
+
automatedScenarios: automated.length,
|
|
209
|
+
manualScenarios: manual.length,
|
|
210
|
+
specTestBlocks: specTitles.length,
|
|
211
|
+
countMatch,
|
|
212
|
+
missingInSpec,
|
|
213
|
+
extraInSpec,
|
|
214
|
+
drift,
|
|
215
|
+
driftHunks,
|
|
216
|
+
assertionlessTests,
|
|
217
|
+
hollowSteps,
|
|
218
|
+
bypass,
|
|
219
|
+
status: ok ? 'OK' : 'FAIL',
|
|
220
|
+
findings,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret scanner (security S0) — warn (never block) when test-data appears to
|
|
3
|
+
* contain a REAL secret rather than a safe placeholder/test value.
|
|
4
|
+
*
|
|
5
|
+
* test-data/*.yaml is committed by design, so a real API key / prod token there is
|
|
6
|
+
* a leak waiting to happen. We flag high-confidence signals only, to avoid nagging
|
|
7
|
+
* about ordinary test passwords ("Test@123") or `{{vars}}`.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
|
|
12
|
+
export interface SecretHit { file: string; line: number; reason: string }
|
|
13
|
+
|
|
14
|
+
// High-confidence vendor token prefixes (very low false-positive rate).
|
|
15
|
+
const VENDOR_PREFIXES = /\b(figd_[A-Za-z0-9_-]{12,}|ghp_[A-Za-z0-9]{20,}|gho_[A-Za-z0-9]{20,}|glpat-[A-Za-z0-9_-]{16,}|sk-[A-Za-z0-9]{16,}|AKIA[0-9A-Z]{12,}|xox[baprs]-[A-Za-z0-9-]{10,}|AIza[0-9A-Za-z_-]{30,})/;
|
|
16
|
+
// A secret-named key assigned a long, non-placeholder value.
|
|
17
|
+
const SECRET_KEY = /\b(password|passwd|secret|token|api[_-]?key|access[_-]?key|private[_-]?key|client[_-]?secret)\b\s*:\s*(.+)$/i;
|
|
18
|
+
const PLACEHOLDERish = /\{\{|<.*>|changeme|example|dummy|test\b|placeholder|xxxx|\*\*\*/i;
|
|
19
|
+
|
|
20
|
+
function scanText(text: string, file: string): SecretHit[] {
|
|
21
|
+
const hits: SecretHit[] = [];
|
|
22
|
+
text.split('\n').forEach((raw, i) => {
|
|
23
|
+
const line = i + 1;
|
|
24
|
+
if (VENDOR_PREFIXES.test(raw)) {
|
|
25
|
+
hits.push({ file, line, reason: 'looks like a real vendor API token/key' });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const m = raw.match(SECRET_KEY);
|
|
29
|
+
if (m) {
|
|
30
|
+
const val = m[2].trim().replace(/^["']|["']$/g, '');
|
|
31
|
+
// Long, high-entropy-ish, not an obvious placeholder/test value.
|
|
32
|
+
if (val.length >= 20 && !PLACEHOLDERish.test(raw) && /[A-Za-z]/.test(val) && /[0-9]/.test(val)) {
|
|
33
|
+
hits.push({ file, line, reason: `secret-named key "${m[1]}" with a long literal value` });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return hits;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Scan a screen/flow dir's test-data/*.yaml for likely real secrets. */
|
|
41
|
+
export function scanTestDataSecrets(baseDir: string): SecretHit[] {
|
|
42
|
+
const tdDir = path.join(baseDir, 'test-data');
|
|
43
|
+
if (!fs.existsSync(tdDir)) return [];
|
|
44
|
+
const hits: SecretHit[] = [];
|
|
45
|
+
for (const f of fs.readdirSync(tdDir)) {
|
|
46
|
+
if (!/\.ya?ml$/i.test(f)) continue;
|
|
47
|
+
const p = path.join(tdDir, f);
|
|
48
|
+
try { hits.push(...scanText(fs.readFileSync(p, 'utf-8'), path.join('test-data', f))); } catch { /* ignore */ }
|
|
49
|
+
}
|
|
50
|
+
return hits;
|
|
51
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness Sensors — deterministic quality measurement over test-design artifacts.
|
|
3
|
+
* Each sensor returns a structured finding the Orchestrator/Repair loop can act on.
|
|
4
|
+
*
|
|
5
|
+
* NOTE (honesty): the Viewpoint Gate and Duplicate sensors involve semantics, so
|
|
6
|
+
* they are HEURISTIC (keyword / skeleton based), not provably exhaustive. They use
|
|
7
|
+
* a curated seed catalog as the matching reference. See docs/orchestration-spec.md §5.2.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { parse as parseYaml } from 'yaml';
|
|
12
|
+
import { ScenarioInfo, ViewpointEntry } from './parse';
|
|
13
|
+
|
|
14
|
+
// Business-critical category codes (project VP-<CAT> prefixes). Configurable later.
|
|
15
|
+
const BUSINESS_CRITICAL_CATS = ['LIST', 'CART', 'PRODUCT', 'FILTER', 'CHECKOUT', 'ORDER'];
|
|
16
|
+
|
|
17
|
+
// Buckets for coverage-balance.
|
|
18
|
+
const BUCKETS: Record<string, string[]> = {
|
|
19
|
+
'business-core': BUSINESS_CRITICAL_CATS,
|
|
20
|
+
'presentation': ['UI'],
|
|
21
|
+
'validation-security': ['VAL', 'SEC', 'SUB'],
|
|
22
|
+
'behavior': ['LOGIC'],
|
|
23
|
+
'navigation': ['NAV'],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export interface ThemeDepth {
|
|
27
|
+
requires: string; // 'data-assertion' → scenarios on this theme must assert DATA
|
|
28
|
+
cross_screen?: boolean; // genuine depth needs another screen → flow / @manual-deferred
|
|
29
|
+
keywords?: string[]; // precise data-noun keywords for depth matching
|
|
30
|
+
template?: string; // the deep step the generator should emit by default
|
|
31
|
+
}
|
|
32
|
+
export interface CatalogTheme { theme: string; keywords: string[]; depth?: ThemeDepth }
|
|
33
|
+
export interface Catalog {
|
|
34
|
+
page_types: Record<string, { detect_keywords: string[]; must_cover: CatalogTheme[] }>;
|
|
35
|
+
universal: CatalogTheme[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function loadCatalog(): Catalog {
|
|
39
|
+
const p = path.join(__dirname, 'catalog', 'universal-viewpoints.yaml');
|
|
40
|
+
return parseYaml(fs.readFileSync(p, 'utf-8')) as Catalog;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const has = (haystacks: string[], kw: string) => {
|
|
44
|
+
const k = kw.toLowerCase();
|
|
45
|
+
return haystacks.some((h) => h.includes(k));
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ---------- Sensor 1: Viewpoint Gate (catalog-driven) ----------
|
|
49
|
+
|
|
50
|
+
export interface GateResult {
|
|
51
|
+
pageType: string | null;
|
|
52
|
+
themesTotal: number;
|
|
53
|
+
themesCovered: number; // deeply covered (has a data assertion)
|
|
54
|
+
coverageRatio: number;
|
|
55
|
+
gaps: { theme: string; keywords: string[]; status: 'missing' | 'shallow' }[];
|
|
56
|
+
universalGaps: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function viewpointGate(scenarios: ScenarioInfo[], viewpoints: ViewpointEntry[], catalog: Catalog): GateResult {
|
|
60
|
+
const haystacks = [
|
|
61
|
+
...scenarios.map((s) => s.haystack),
|
|
62
|
+
...viewpoints.map((v) => `${v.id} ${v.reason}`.toLowerCase()),
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
// Detect page-type by detect_keywords hit count
|
|
66
|
+
let pageType: string | null = null;
|
|
67
|
+
let best = 0;
|
|
68
|
+
for (const [pt, def] of Object.entries(catalog.page_types)) {
|
|
69
|
+
const hits = def.detect_keywords.filter((k) => has(haystacks, k)).length;
|
|
70
|
+
if (hits > best) { best = hits; pageType = pt; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const gaps: GateResult['gaps'] = [];
|
|
74
|
+
let total = 0, covered = 0;
|
|
75
|
+
if (pageType) {
|
|
76
|
+
for (const t of catalog.page_types[pageType].must_cover) {
|
|
77
|
+
total++;
|
|
78
|
+
// Scenarios that cover this theme by keyword (include @manual — a manual
|
|
79
|
+
// scenario with a real assertion still covers the viewpoint for design).
|
|
80
|
+
const coverers = scenarios.filter((s) => t.keywords.some((k) => s.haystack.includes(k.toLowerCase())));
|
|
81
|
+
const deep = coverers.some((s) => s.hasDataAssertion);
|
|
82
|
+
if (deep) covered++;
|
|
83
|
+
else if (coverers.length > 0) gaps.push({ theme: t.theme, keywords: t.keywords, status: 'shallow' });
|
|
84
|
+
else gaps.push({ theme: t.theme, keywords: t.keywords, status: 'missing' });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const universalGaps = catalog.universal
|
|
89
|
+
.filter((t) => !t.keywords.some((k) => has(scenarios.map((s) => s.haystack), k)))
|
|
90
|
+
.map((t) => t.theme);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
pageType,
|
|
94
|
+
themesTotal: total,
|
|
95
|
+
themesCovered: covered,
|
|
96
|
+
coverageRatio: total ? covered / total : 1,
|
|
97
|
+
gaps,
|
|
98
|
+
universalGaps,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------- Sensor 2: Assertion depth ----------
|
|
103
|
+
|
|
104
|
+
export type DepthVerdict = 'pass' | 'warn' | 'fail';
|
|
105
|
+
|
|
106
|
+
export interface DepthResult {
|
|
107
|
+
total: number;
|
|
108
|
+
shallowTotal: number;
|
|
109
|
+
shallowRatio: number;
|
|
110
|
+
businessCriticalTotal: number; // = depth-required scenarios (catalog data-themes)
|
|
111
|
+
businessCriticalShallow: number; // = depth-required scenarios that are shallow
|
|
112
|
+
bcDepthRatio: number; // fraction of depth-required scenarios with a real data assertion
|
|
113
|
+
shallowBusinessCritical: { name: string; category?: string }[];
|
|
114
|
+
// Depth-as-Gate (harness-roadmap P1)
|
|
115
|
+
focus: string; // intent focus driving the threshold
|
|
116
|
+
threshold: number; // required bcDepthRatio for this focus
|
|
117
|
+
verdict: DepthVerdict; // pass | warn | fail
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Intent → required depth ratio + whether a miss WARNs (smoke) or FAILs the gate.
|
|
121
|
+
// P3 will read `focus` from qa/context.md; P1 uses 'functional' by default.
|
|
122
|
+
const DEPTH_THRESHOLDS: Record<string, number> = {
|
|
123
|
+
functional: 0.7, 'e-commerce': 0.7, security: 0.8, smoke: 0.4,
|
|
124
|
+
};
|
|
125
|
+
const WARN_ONLY_FOCUS = new Set(['smoke']);
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Depth = do DATA-correctness scenarios actually assert DATA (not just visibility)?
|
|
129
|
+
* "Depth-required" is CATALOG-DRIVEN: only scenarios matching a theme whose
|
|
130
|
+
* `depth.requires === 'data-assertion'` are measured. Navigation viewpoints
|
|
131
|
+
* (category NAV) are excluded — landing on a page IS their correct assertion.
|
|
132
|
+
* This avoids the old keyword-based false-positives (e.g. "API list page").
|
|
133
|
+
*/
|
|
134
|
+
export function assertionDepth(
|
|
135
|
+
scenarios: ScenarioInfo[],
|
|
136
|
+
dataThemes: CatalogTheme[] = [],
|
|
137
|
+
focus = 'functional',
|
|
138
|
+
): DepthResult {
|
|
139
|
+
const nonManual = scenarios.filter((s) => !s.manual);
|
|
140
|
+
const shallow = nonManual.filter((s) => s.shallow);
|
|
141
|
+
|
|
142
|
+
// Precise depth keywords come from each data-theme's depth.keywords (fallback: theme keywords).
|
|
143
|
+
const depthKeywords = dataThemes.flatMap((t) => (t.depth?.keywords?.length ? t.depth.keywords : t.keywords));
|
|
144
|
+
// Dismiss/close behaviors ("Continue Shopping closes the modal", "Escape dismisses the
|
|
145
|
+
// dialog") assert a HIDDEN state — that IS the correct, data-light assertion for the
|
|
146
|
+
// behavior. They only match a data keyword incidentally (e.g. "added"), so exclude them.
|
|
147
|
+
const isDismissBehavior = (s: ScenarioInfo) =>
|
|
148
|
+
/\bis hidden\b|\bare hidden\b|\bdismiss|\bdisappear|\bno longer (visible|shown|present|displayed)\b|\bcloses? the (modal|dialog|popup|overlay|panel|menu)\b/.test(s.haystack);
|
|
149
|
+
const isDepthRequired = (s: ScenarioInfo) =>
|
|
150
|
+
s.category !== 'NAV' && !isDismissBehavior(s) && depthKeywords.some((k) => s.haystack.includes(k.toLowerCase()));
|
|
151
|
+
|
|
152
|
+
const required = nonManual.filter(isDepthRequired);
|
|
153
|
+
const reqShallow = required.filter((s) => s.shallow);
|
|
154
|
+
// No data-theme scenarios on this screen → depth is not the binding constraint
|
|
155
|
+
// (the viewpoint gate already flags missing data themes). Don't double-penalize.
|
|
156
|
+
const ratio = required.length ? 1 - reqShallow.length / required.length : 1;
|
|
157
|
+
|
|
158
|
+
const threshold = DEPTH_THRESHOLDS[focus] ?? DEPTH_THRESHOLDS.functional;
|
|
159
|
+
let verdict: DepthVerdict = 'pass';
|
|
160
|
+
if (ratio < threshold) verdict = WARN_ONLY_FOCUS.has(focus) ? 'warn' : 'fail';
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
total: nonManual.length,
|
|
164
|
+
shallowTotal: shallow.length,
|
|
165
|
+
shallowRatio: nonManual.length ? shallow.length / nonManual.length : 0,
|
|
166
|
+
businessCriticalTotal: required.length,
|
|
167
|
+
businessCriticalShallow: reqShallow.length,
|
|
168
|
+
bcDepthRatio: ratio,
|
|
169
|
+
shallowBusinessCritical: reqShallow.map((s) => ({ name: s.name, category: s.category })),
|
|
170
|
+
focus,
|
|
171
|
+
threshold,
|
|
172
|
+
verdict,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Collect data-correctness themes (depth.requires) for a page-type + universal. */
|
|
177
|
+
export function dataThemesFor(catalog: Catalog, pageType: string | null): CatalogTheme[] {
|
|
178
|
+
const themes: CatalogTheme[] = [];
|
|
179
|
+
if (pageType && catalog.page_types[pageType]) themes.push(...catalog.page_types[pageType].must_cover);
|
|
180
|
+
themes.push(...catalog.universal);
|
|
181
|
+
return themes.filter((t) => t.depth?.requires === 'data-assertion');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------- Sensor 3: Coverage balance ----------
|
|
185
|
+
|
|
186
|
+
export interface BalanceResult {
|
|
187
|
+
byBucket: Record<string, number>;
|
|
188
|
+
byCategory: Record<string, number>;
|
|
189
|
+
coreCount: number;
|
|
190
|
+
secondaryCount: number;
|
|
191
|
+
imbalanced: boolean;
|
|
192
|
+
note: string;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function coverageBalance(scenarios: ScenarioInfo[]): BalanceResult {
|
|
196
|
+
const byCategory: Record<string, number> = {};
|
|
197
|
+
const byBucket: Record<string, number> = {};
|
|
198
|
+
for (const b of Object.keys(BUCKETS)) byBucket[b] = 0;
|
|
199
|
+
byBucket['other'] = 0;
|
|
200
|
+
|
|
201
|
+
for (const s of scenarios) {
|
|
202
|
+
const cat = s.category || 'NONE';
|
|
203
|
+
byCategory[cat] = (byCategory[cat] || 0) + 1;
|
|
204
|
+
const bucket = Object.entries(BUCKETS).find(([, cats]) => cats.includes(cat))?.[0] || 'other';
|
|
205
|
+
byBucket[bucket]++;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const core = byBucket['business-core'];
|
|
209
|
+
const secondary = byBucket['presentation'] + byBucket['validation-security'];
|
|
210
|
+
const imbalanced = secondary > core * 1.5 && core > 0;
|
|
211
|
+
return {
|
|
212
|
+
byBucket,
|
|
213
|
+
byCategory,
|
|
214
|
+
coreCount: core,
|
|
215
|
+
secondaryCount: secondary,
|
|
216
|
+
imbalanced,
|
|
217
|
+
note: imbalanced
|
|
218
|
+
? `Secondary viewpoints (presentation+validation/security = ${secondary}) outweigh business-core (${core}) by >1.5x.`
|
|
219
|
+
: 'Balanced.',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---------- Sensor 4: Duplicate clusters ----------
|
|
224
|
+
|
|
225
|
+
export interface DuplicateResult {
|
|
226
|
+
clusters: { skeleton: string; scenarios: string[]; sameDataLikely: boolean }[];
|
|
227
|
+
exactDuplicateCount: number;
|
|
228
|
+
sameShapeCount: number;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function duplicateClusters(scenarios: ScenarioInfo[]): DuplicateResult {
|
|
232
|
+
const map = new Map<string, ScenarioInfo[]>();
|
|
233
|
+
for (const s of scenarios) {
|
|
234
|
+
const arr = map.get(s.stepSkeleton) || [];
|
|
235
|
+
arr.push(s);
|
|
236
|
+
map.set(s.stepSkeleton, arr);
|
|
237
|
+
}
|
|
238
|
+
const clusters = [...map.entries()]
|
|
239
|
+
.filter(([, arr]) => arr.length > 1)
|
|
240
|
+
.map(([skeleton, arr]) => ({
|
|
241
|
+
skeleton: skeleton.length > 120 ? skeleton.slice(0, 117) + '...' : skeleton,
|
|
242
|
+
scenarios: arr.map((s) => s.name),
|
|
243
|
+
// Same skeleton with data placeholders → likely an EP/data family (intentional), not a true dup.
|
|
244
|
+
sameDataLikely: !skeleton.includes('{}'),
|
|
245
|
+
}));
|
|
246
|
+
return {
|
|
247
|
+
clusters,
|
|
248
|
+
exactDuplicateCount: clusters.filter((c) => c.sameDataLikely).reduce((n, c) => n + (c.scenarios.length - 1), 0),
|
|
249
|
+
sameShapeCount: clusters.reduce((n, c) => n + c.scenarios.length, 0),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ---------- Sensor 5: Traceability ----------
|
|
254
|
+
|
|
255
|
+
export interface TraceResult {
|
|
256
|
+
total: number;
|
|
257
|
+
withVpCode: number;
|
|
258
|
+
mappedToOverview: number;
|
|
259
|
+
withVpCodeRatio: number;
|
|
260
|
+
mappedRatio: number;
|
|
261
|
+
note: string;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function traceability(scenarios: ScenarioInfo[], viewpoints: ViewpointEntry[]): TraceResult {
|
|
265
|
+
const overviewIds = new Set(viewpoints.map((v) => v.id.toUpperCase()));
|
|
266
|
+
const withCode = scenarios.filter((s) => s.vpCode);
|
|
267
|
+
// A scenario maps to overview if its full VP code OR its category-derived id exists in overview.
|
|
268
|
+
const mapped = withCode.filter((s) => overviewIds.has(s.vpCode!) || [...overviewIds].some((id) => id.includes(s.category || '###')));
|
|
269
|
+
return {
|
|
270
|
+
total: scenarios.length,
|
|
271
|
+
withVpCode: withCode.length,
|
|
272
|
+
mappedToOverview: mapped.length,
|
|
273
|
+
withVpCodeRatio: scenarios.length ? withCode.length / scenarios.length : 0,
|
|
274
|
+
mappedRatio: scenarios.length ? mapped.length / scenarios.length : 0,
|
|
275
|
+
note: mapped.length < withCode.length * 0.5
|
|
276
|
+
? 'Scenarios use ad-hoc VP-<CAT>-NNN codes not linked to viewpoint-overview ids (weak traceability — see review Gate 4).'
|
|
277
|
+
: 'Traceable.',
|
|
278
|
+
};
|
|
279
|
+
}
|