@sun-asterisk/sungen 3.2.24 → 3.2.26
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/dist/capabilities/context-router.d.ts.map +1 -1
- package/dist/capabilities/context-router.js +15 -7
- package/dist/capabilities/context-router.js.map +1 -1
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +23 -4
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/capability.d.ts.map +1 -1
- package/dist/cli/commands/capability.js +6 -48
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/changed.d.ts +14 -0
- package/dist/cli/commands/changed.d.ts.map +1 -0
- package/dist/cli/commands/changed.js +128 -0
- package/dist/cli/commands/changed.js.map +1 -0
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +25 -1
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/generate.js +1 -1
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/results-merge.d.ts +19 -0
- package/dist/cli/commands/results-merge.d.ts.map +1 -0
- package/dist/cli/commands/results-merge.js +191 -0
- package/dist/cli/commands/results-merge.js.map +1 -0
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +100 -12
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/exporters/api-catalog-loader.d.ts.map +1 -1
- package/dist/exporters/api-catalog-loader.js +10 -3
- package/dist/exporters/api-catalog-loader.js.map +1 -1
- package/dist/exporters/api-testcase-formatter.d.ts +9 -0
- package/dist/exporters/api-testcase-formatter.d.ts.map +1 -1
- package/dist/exporters/api-testcase-formatter.js +55 -3
- package/dist/exporters/api-testcase-formatter.js.map +1 -1
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +10 -1
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/json-exporter.d.ts.map +1 -1
- package/dist/exporters/json-exporter.js +6 -1
- package/dist/exporters/json-exporter.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +14 -1
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/export.js +1 -1
- package/dist/exporters/matrix/export.js.map +1 -1
- package/dist/exporters/matrix/map-loader.d.ts.map +1 -1
- package/dist/exporters/matrix/map-loader.js +5 -0
- package/dist/exporters/matrix/map-loader.js.map +1 -1
- package/dist/exporters/matrix/render-csv.d.ts.map +1 -1
- package/dist/exporters/matrix/render-csv.js +5 -1
- package/dist/exporters/matrix/render-csv.js.map +1 -1
- package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
- package/dist/exporters/matrix/render-xlsx.js +4 -1
- package/dist/exporters/matrix/render-xlsx.js.map +1 -1
- package/dist/exporters/matrix/types.d.ts +18 -0
- package/dist/exporters/matrix/types.d.ts.map +1 -1
- package/dist/exporters/matrix/types.js.map +1 -1
- package/dist/exporters/playwright-report-parser.d.ts +17 -2
- package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
- package/dist/exporters/playwright-report-parser.js +68 -8
- package/dist/exporters/playwright-report-parser.js.map +1 -1
- package/dist/exporters/results-merge.d.ts +46 -0
- package/dist/exporters/results-merge.d.ts.map +1 -0
- package/dist/exporters/results-merge.js +148 -0
- package/dist/exporters/results-merge.js.map +1 -0
- package/dist/exporters/selector-key-resolver.d.ts +4 -0
- package/dist/exporters/selector-key-resolver.d.ts.map +1 -1
- package/dist/exporters/selector-key-resolver.js +101 -1
- package/dist/exporters/selector-key-resolver.js.map +1 -1
- package/dist/exporters/types.d.ts +8 -1
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
- package/dist/exporters/xlsx-report-builder.js +5 -4
- package/dist/exporters/xlsx-report-builder.js.map +1 -1
- package/dist/generators/gherkin-parser/index.d.ts +2 -0
- package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
- package/dist/generators/gherkin-parser/index.js +52 -1
- package/dist/generators/gherkin-parser/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +20 -10
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +2 -11
- package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/appium-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/imports.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/date-picker-unsupported.hbs +4 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/lifecycle-missing-app-id.hbs +5 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/matches-format-assertion.hbs +15 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +5 -2
- package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +185 -26
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +2 -11
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +17 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +6 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +18 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +5 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/matches-format-assertion.hbs +5 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +4 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +4 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -1
- package/dist/generators/test-generator/code-generator.d.ts +6 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +80 -7
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +60 -6
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +59 -5
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/patterns/expect-patterns.d.ts +7 -0
- package/dist/generators/test-generator/patterns/expect-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/expect-patterns.js +36 -2
- package/dist/generators/test-generator/patterns/expect-patterns.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts +19 -12
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +58 -38
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/patterns/types.d.ts +23 -0
- package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/unsupported-step-code.d.ts +18 -0
- package/dist/generators/test-generator/patterns/unsupported-step-code.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/unsupported-step-code.js +23 -0
- package/dist/generators/test-generator/patterns/unsupported-step-code.js.map +1 -0
- package/dist/generators/test-generator/recipe-expander.js +9 -1
- package/dist/generators/test-generator/recipe-expander.js.map +1 -1
- package/dist/generators/test-generator/reset-tier.d.ts +68 -0
- package/dist/generators/test-generator/reset-tier.d.ts.map +1 -0
- package/dist/generators/test-generator/reset-tier.js +94 -0
- package/dist/generators/test-generator/reset-tier.js.map +1 -0
- package/dist/generators/test-generator/step-mapper.d.ts +39 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +144 -5
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +2 -10
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +16 -2
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/format-pattern.d.ts +35 -0
- package/dist/generators/test-generator/utils/format-pattern.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/format-pattern.js +82 -0
- package/dist/generators/test-generator/utils/format-pattern.js.map +1 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.d.ts +42 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.js +142 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.js.map +1 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.d.ts +18 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.js +25 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.js.map +1 -0
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +9 -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 +124 -63
- 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 +20 -1
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/generators/test-generator/utils/spec-syntax-check.d.ts +14 -0
- package/dist/generators/test-generator/utils/spec-syntax-check.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/spec-syntax-check.js +26 -0
- package/dist/generators/test-generator/utils/spec-syntax-check.js.map +1 -0
- package/dist/harness/annotation-overrides.d.ts +0 -12
- package/dist/harness/annotation-overrides.d.ts.map +1 -1
- package/dist/harness/annotation-overrides.js +9 -5
- package/dist/harness/annotation-overrides.js.map +1 -1
- package/dist/harness/audit.d.ts +7 -0
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +78 -10
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability-plan.d.ts +2 -1
- package/dist/harness/capability-plan.d.ts.map +1 -1
- package/dist/harness/capability-plan.js +8 -3
- package/dist/harness/capability-plan.js.map +1 -1
- package/dist/harness/capability.d.ts +11 -0
- package/dist/harness/capability.d.ts.map +1 -1
- package/dist/harness/capability.js +8 -0
- package/dist/harness/capability.js.map +1 -1
- package/dist/harness/catalog/drivers.yaml +5 -0
- package/dist/harness/flow-contract.d.ts +18 -1
- package/dist/harness/flow-contract.d.ts.map +1 -1
- package/dist/harness/flow-contract.js +72 -9
- package/dist/harness/flow-contract.js.map +1 -1
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +5 -1
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/quality-gates.d.ts +12 -1
- package/dist/harness/quality-gates.d.ts.map +1 -1
- package/dist/harness/quality-gates.js +62 -7
- package/dist/harness/quality-gates.js.map +1 -1
- package/dist/harness/run-manifest.d.ts +58 -0
- package/dist/harness/run-manifest.d.ts.map +1 -0
- package/dist/harness/run-manifest.js +264 -0
- package/dist/harness/run-manifest.js.map +1 -0
- package/dist/harness/spec-branches.d.ts +88 -0
- package/dist/harness/spec-branches.d.ts.map +1 -0
- package/dist/harness/spec-branches.js +280 -0
- package/dist/harness/spec-branches.js.map +1 -0
- package/dist/harness/spec-coverage.d.ts +1 -1
- package/dist/harness/spec-coverage.js +4 -4
- package/dist/harness/spec-coverage.js.map +1 -1
- package/dist/harness/viewpoint-baseline.d.ts +9 -0
- package/dist/harness/viewpoint-baseline.d.ts.map +1 -1
- package/dist/harness/viewpoint-baseline.js +33 -3
- package/dist/harness/viewpoint-baseline.js.map +1 -1
- package/dist/harness/viewpoint-ledger.d.ts +1 -1
- package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
- package/dist/harness/viewpoint-ledger.js +88 -9
- package/dist/harness/viewpoint-ledger.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/lockstep-version.d.ts +22 -0
- package/dist/orchestrator/lockstep-version.d.ts.map +1 -0
- package/dist/orchestrator/lockstep-version.js +58 -0
- package/dist/orchestrator/lockstep-version.js.map +1 -0
- package/dist/orchestrator/project-deps-sync.d.ts +39 -0
- package/dist/orchestrator/project-deps-sync.d.ts.map +1 -0
- package/dist/orchestrator/project-deps-sync.js +175 -0
- package/dist/orchestrator/project-deps-sync.js.map +1 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +5 -2
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/add-flow.md +16 -0
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +9 -0
- package/dist/orchestrator/templates/ai-src/commands/delivery.md +9 -2
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +109 -0
- package/dist/orchestrator/templates/ai-src/config/claude.md +1 -1
- package/dist/orchestrator/templates/ai-src/config/copilot.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +49 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -3
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +110 -6
- package/dist/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +8 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +40 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
- package/dist/orchestrator/templates/env.appium.example +10 -0
- package/dist/orchestrator/templates/specs-api.d.ts +4 -2
- package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-api.js +233 -10
- package/dist/orchestrator/templates/specs-api.js.map +1 -1
- package/dist/orchestrator/templates/specs-api.ts +214 -11
- package/dist/orchestrator/templates/specs-regex-escape.d.ts +13 -0
- package/dist/orchestrator/templates/specs-regex-escape.d.ts.map +1 -0
- package/dist/orchestrator/templates/specs-regex-escape.js +19 -0
- package/dist/orchestrator/templates/specs-regex-escape.js.map +1 -0
- package/dist/orchestrator/templates/specs-regex-escape.ts +16 -0
- package/dist/orchestrator/templates/specs-test-data.ts +53 -6
- package/dist/orchestrator/templates/wdio.conf.ts +28 -9
- package/package.json +8 -6
- package/src/capabilities/context-router.ts +13 -5
- package/src/cli/commands/audit.ts +22 -3
- package/src/cli/commands/capability.ts +4 -38
- package/src/cli/commands/changed.ts +104 -0
- package/src/cli/commands/delivery.ts +27 -2
- package/src/cli/commands/generate.ts +1 -1
- package/src/cli/commands/results-merge.ts +153 -0
- package/src/cli/commands/update.ts +72 -13
- package/src/cli/index.ts +4 -0
- package/src/exporters/api-catalog-loader.ts +10 -2
- package/src/exporters/api-testcase-formatter.ts +59 -3
- package/src/exporters/csv-exporter.ts +11 -2
- package/src/exporters/json-exporter.ts +7 -2
- package/src/exporters/matrix/build.ts +13 -1
- package/src/exporters/matrix/export.ts +1 -1
- package/src/exporters/matrix/map-loader.ts +5 -0
- package/src/exporters/matrix/render-csv.ts +5 -1
- package/src/exporters/matrix/render-xlsx.ts +4 -1
- package/src/exporters/matrix/types.ts +18 -0
- package/src/exporters/playwright-report-parser.ts +68 -9
- package/src/exporters/results-merge.ts +150 -0
- package/src/exporters/selector-key-resolver.ts +81 -1
- package/src/exporters/types.ts +8 -1
- package/src/exporters/xlsx-report-builder.ts +5 -4
- package/src/generators/gherkin-parser/index.ts +55 -1
- package/src/generators/test-generator/adapters/adapter-interface.ts +23 -1
- package/src/generators/test-generator/adapters/appium/appium-adapter.ts +2 -2
- package/src/generators/test-generator/adapters/appium/templates/imports.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/date-picker-unsupported.hbs +4 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/lifecycle-missing-app-id.hbs +5 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/matches-format-assertion.hbs +15 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +5 -2
- package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +185 -26
- package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +2 -2
- package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +17 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +6 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +18 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +5 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/matches-format-assertion.hbs +5 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +4 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +4 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -1
- package/src/generators/test-generator/code-generator.ts +86 -7
- package/src/generators/test-generator/diagnostics.ts +60 -6
- package/src/generators/test-generator/patterns/expect-patterns.ts +37 -2
- package/src/generators/test-generator/patterns/index.ts +58 -33
- package/src/generators/test-generator/patterns/types.ts +36 -0
- package/src/generators/test-generator/patterns/unsupported-step-code.ts +19 -0
- package/src/generators/test-generator/recipe-expander.ts +9 -1
- package/src/generators/test-generator/reset-tier.ts +93 -0
- package/src/generators/test-generator/step-mapper.ts +149 -6
- package/src/generators/test-generator/template-engine.ts +18 -3
- package/src/generators/test-generator/utils/format-pattern.ts +79 -0
- package/src/generators/test-generator/utils/js-literal-scanner.ts +132 -0
- package/src/generators/test-generator/utils/mask-refs-for-matching.ts +21 -0
- package/src/generators/test-generator/utils/runtime-data-transformer.ts +126 -76
- package/src/generators/test-generator/utils/selector-resolver.ts +22 -1
- package/src/generators/test-generator/utils/spec-syntax-check.ts +22 -0
- package/src/harness/annotation-overrides.ts +10 -5
- package/src/harness/audit.ts +82 -12
- package/src/harness/capability-plan.ts +10 -5
- package/src/harness/capability.ts +16 -0
- package/src/harness/catalog/drivers.yaml +5 -0
- package/src/harness/flow-contract.ts +87 -9
- package/src/harness/parse.ts +5 -1
- package/src/harness/quality-gates.ts +64 -6
- package/src/harness/run-manifest.ts +254 -0
- package/src/harness/spec-branches.ts +346 -0
- package/src/harness/spec-coverage.ts +4 -4
- package/src/harness/viewpoint-baseline.ts +41 -6
- package/src/harness/viewpoint-ledger.ts +82 -9
- package/src/index.ts +1 -0
- package/src/orchestrator/lockstep-version.ts +46 -0
- package/src/orchestrator/project-deps-sync.ts +175 -0
- package/src/orchestrator/project-initializer.ts +5 -2
- package/src/orchestrator/templates/ai-src/commands/add-flow.md +16 -0
- package/src/orchestrator/templates/ai-src/commands/create-test.md +9 -0
- package/src/orchestrator/templates/ai-src/commands/delivery.md +9 -2
- package/src/orchestrator/templates/ai-src/commands/run-test.md +109 -0
- package/src/orchestrator/templates/ai-src/config/claude.md +1 -1
- package/src/orchestrator/templates/ai-src/config/copilot.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +49 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -3
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +110 -6
- package/src/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +8 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +40 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
- package/src/orchestrator/templates/env.appium.example +10 -0
- package/src/orchestrator/templates/specs-api.ts +214 -11
- package/src/orchestrator/templates/specs-regex-escape.ts +16 -0
- package/src/orchestrator/templates/specs-test-data.ts +53 -6
- package/src/orchestrator/templates/wdio.conf.ts +28 -9
|
@@ -40,6 +40,13 @@ export interface FlowDecl {
|
|
|
40
40
|
reason?: string;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** One declared journey phase. `scenarios:` is the author naming its evidence directly. */
|
|
44
|
+
export interface FlowPhaseDecl {
|
|
45
|
+
id: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
scenarios?: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
const FLOW_STATUSES = new Set(['covered', 'deferred', 'pending-clarification', 'out-of-scope']);
|
|
44
51
|
|
|
45
52
|
export interface FlowContract {
|
|
@@ -58,6 +65,12 @@ export interface FlowContract {
|
|
|
58
65
|
/** Journey phases this flow declares. Default [HP, ER, EH]; UI is allowed but
|
|
59
66
|
* never demanded (presentation is the balance axis's business, not coverage's). */
|
|
60
67
|
phases: string[];
|
|
68
|
+
/**
|
|
69
|
+
* The phases as DECLARED, when the contract uses the object form. A phase may name the
|
|
70
|
+
* scenarios that carry it — direct evidence, rather than the harness inferring coverage from
|
|
71
|
+
* id segments — and that is what a project which writes `phases:` this way is telling us.
|
|
72
|
+
*/
|
|
73
|
+
phaseDetails?: FlowPhaseDecl[];
|
|
61
74
|
/**
|
|
62
75
|
* The use case's declared flow inventory — the answer to "how many flows does this use
|
|
63
76
|
* case HAVE?". `phases:` alone cannot answer it: a phase is present as soon as ONE
|
|
@@ -94,6 +107,8 @@ export interface FlowQualityResult {
|
|
|
94
107
|
/** Off-goal categories, for the split suggestion ("VP-FILTER-* looks like its own flow"). */
|
|
95
108
|
offGoalCategories: string[];
|
|
96
109
|
phases: { phase: string; covered: boolean; automated: boolean }[];
|
|
110
|
+
/** Phase-declared scenario refs that match no scenario — a phase whose evidence does not exist. */
|
|
111
|
+
danglingPhaseRefs: Array<{ phase: string; ref: string }>;
|
|
97
112
|
/** Covered-and-automated phases / declared phases (UI excluded) — the flow coverage axis. */
|
|
98
113
|
phaseRatio: number;
|
|
99
114
|
/** Cross-namespace transitions followed by an assertion / all transitions. */
|
|
@@ -130,9 +145,34 @@ export function loadFlowContract(unitDir: string): { contract: FlowContract | nu
|
|
|
130
145
|
errors.push('missing `outcome.screen:` — the screen namespace that carries the final proof');
|
|
131
146
|
}
|
|
132
147
|
if (errors.length > 0) return { contract: null, errors };
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
148
|
+
// `phases:` comes in two shapes, and the loader used to assume the first. A real project
|
|
149
|
+
// declared the RICHER one — each phase an object with `name`, `description` and an explicit
|
|
150
|
+
// `scenarios:` list — and `String(x).toUpperCase()` turned every one of them into
|
|
151
|
+
// `[OBJECT OBJECT]`. Nothing matched, phase coverage read 0%, the score fell to 6.9, and the
|
|
152
|
+
// report said only that the phases were uncovered. Garbage in place of an error is the worst
|
|
153
|
+
// of both: the author sees a broken number with no cause (#657).
|
|
154
|
+
//
|
|
155
|
+
// The object form is the better declaration — it names the scenarios that carry the phase
|
|
156
|
+
// instead of leaving the harness to infer them from ids — so it is read, not merely tolerated.
|
|
157
|
+
const phaseDetails: FlowPhaseDecl[] = Array.isArray(raw.phases)
|
|
158
|
+
? (raw.phases as unknown[]).map((x, i) => {
|
|
159
|
+
if (x !== null && typeof x === 'object') {
|
|
160
|
+
const o = x as Record<string, unknown>;
|
|
161
|
+
const name = o.name ?? o.id ?? o.phase;
|
|
162
|
+
if (name === undefined) {
|
|
163
|
+
errors.push(`phases[${i}] is an object with no \`name:\` (or \`id:\`) — a phase needs a label`);
|
|
164
|
+
return { id: `PHASE${i + 1}` };
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
id: String(name).toUpperCase(),
|
|
168
|
+
description: o.description !== undefined ? String(o.description) : undefined,
|
|
169
|
+
scenarios: Array.isArray(o.scenarios) ? (o.scenarios as unknown[]).map((v) => String(v)) : undefined,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return { id: String(x).toUpperCase() };
|
|
173
|
+
})
|
|
174
|
+
: [];
|
|
175
|
+
const phases = phaseDetails.length > 0 ? phaseDetails.map((p) => p.id) : DEFAULT_PHASES;
|
|
136
176
|
return {
|
|
137
177
|
contract: {
|
|
138
178
|
goal: String(raw.goal),
|
|
@@ -144,6 +184,7 @@ export function loadFlowContract(unitDir: string): { contract: FlowContract | nu
|
|
|
144
184
|
successGuarantee: raw.successGuarantee !== undefined ? String(raw.successGuarantee) : (raw.success_guarantee !== undefined ? String(raw.success_guarantee) : undefined),
|
|
145
185
|
minimalGuarantee: raw.minimalGuarantee !== undefined ? String(raw.minimalGuarantee) : (raw.minimal_guarantee !== undefined ? String(raw.minimal_guarantee) : undefined),
|
|
146
186
|
phases,
|
|
187
|
+
...(phaseDetails.some((p) => p.scenarios || p.description) ? { phaseDetails } : {}),
|
|
147
188
|
flows: Array.isArray(raw.flows)
|
|
148
189
|
? (raw.flows as Array<Record<string, unknown>>)
|
|
149
190
|
.filter((f) => f && typeof f === 'object' && f.id)
|
|
@@ -209,8 +250,17 @@ function reachesOutcome(s: ScenarioInfo, outcomeScreen: string): boolean {
|
|
|
209
250
|
|
|
210
251
|
/** Phase of a scenario: its declared phase token (FL-HP-001 / VP-FLOW-ER-02 / MS-EH-005)
|
|
211
252
|
* when present, else vocabulary detection. */
|
|
212
|
-
export function phaseOf(s: ScenarioInfo, declared: string[]): string | null {
|
|
253
|
+
export function phaseOf(s: ScenarioInfo, declared: string[], details?: FlowPhaseDecl[]): string | null {
|
|
213
254
|
const id = (s.vpId ?? '').toUpperCase();
|
|
255
|
+
// When a phase NAMES its scenarios, that list is the mapping — the author has already done
|
|
256
|
+
// the assignment, and inferring it from id segments instead would ignore them. A phase called
|
|
257
|
+
// "Cross-screen data" shares no token with `VP-LOGIC-002`; only the declaration connects them.
|
|
258
|
+
for (const ph of details ?? []) {
|
|
259
|
+
if (ph.scenarios?.some((ref) => {
|
|
260
|
+
const r = ref.trim().toUpperCase();
|
|
261
|
+
return r === id || (r.length > 0 && s.name.toUpperCase().includes(r));
|
|
262
|
+
})) return ph.id;
|
|
263
|
+
}
|
|
214
264
|
// A DECLARED phase wins, matched on the id's segments with the branch number removed —
|
|
215
265
|
// so the use-case vocabulary (VP-EF01-01, VP-AF02-01: one id per Exception/Alternate
|
|
216
266
|
// Flow) resolves to its phase exactly like the flat HP/ER/EH form does.
|
|
@@ -350,7 +400,7 @@ export function flowQuality(unitDir: string, scenarios: ScenarioInfo[]): FlowQua
|
|
|
350
400
|
const neutral: FlowQualityResult = {
|
|
351
401
|
hasContract: false, errors, outcomeProven: false, outcomeManualOnly: false,
|
|
352
402
|
offGoal: [], offGoalRatio: 0, offGoalCategories: [],
|
|
353
|
-
phases: [], phaseRatio: 1, handoffs: { total: 0, asserted: 0, ratio: 1 }, sit: [],
|
|
403
|
+
phases: [], phaseRatio: 1, danglingPhaseRefs: [], handoffs: { total: 0, asserted: 0, ratio: 1 }, sit: [],
|
|
354
404
|
};
|
|
355
405
|
if (!contract) return neutral;
|
|
356
406
|
|
|
@@ -367,7 +417,7 @@ export function flowQuality(unitDir: string, scenarios: ScenarioInfo[]): FlowQua
|
|
|
367
417
|
const basic = declaredPhases.filter((p) => p !== 'UI')[0];
|
|
368
418
|
const offGoalScenarios = scenarios.filter((s) => {
|
|
369
419
|
if (touchesOutcome(s, outcomeScreen)) return false;
|
|
370
|
-
const ph = phaseOf(s, declaredPhases);
|
|
420
|
+
const ph = phaseOf(s, declaredPhases, contract.phaseDetails);
|
|
371
421
|
// Any NON-basic declared phase (guards, error recovery, alternate branches) legitimately
|
|
372
422
|
// stops before the outcome — that is what a branch IS. Only an unclassified scenario that
|
|
373
423
|
// never reaches the outcome is evidence of a second business goal.
|
|
@@ -383,7 +433,7 @@ export function flowQuality(unitDir: string, scenarios: ScenarioInfo[]): FlowQua
|
|
|
383
433
|
// phase that must reach the declared outcome, whether the project spells it HP or BF.
|
|
384
434
|
const basicPhase = demanded[0];
|
|
385
435
|
const phases = demanded.map((phase) => {
|
|
386
|
-
const inPhase = scenarios.filter((s) => phaseOf(s, declaredPhases) === phase);
|
|
436
|
+
const inPhase = scenarios.filter((s) => phaseOf(s, declaredPhases, contract.phaseDetails) === phase);
|
|
387
437
|
// The basic flow must additionally prove the outcome — a data assertion elsewhere is not the goal.
|
|
388
438
|
const relevant = phase === basicPhase ? inPhase.filter((s) => touchesOutcome(s, outcomeScreen)) : inPhase;
|
|
389
439
|
return {
|
|
@@ -396,6 +446,17 @@ export function flowQuality(unitDir: string, scenarios: ScenarioInfo[]): FlowQua
|
|
|
396
446
|
? phases.filter((p) => p.covered && p.automated).length / demanded.length
|
|
397
447
|
: 1;
|
|
398
448
|
|
|
449
|
+
// A phase that names a scenario which does not exist is a declaration pointing at nothing:
|
|
450
|
+
// the phase reads covered in the contract and is empty in the suite. Same class as a dangling
|
|
451
|
+
// traceability ref — the map, not the territory.
|
|
452
|
+
const known = new Set(scenarios.flatMap((s) => [s.vpId?.toUpperCase(), s.name.toUpperCase()].filter(Boolean) as string[]));
|
|
453
|
+
const danglingPhaseRefs = (contract.phaseDetails ?? []).flatMap((ph) => (ph.scenarios ?? [])
|
|
454
|
+
.filter((ref) => {
|
|
455
|
+
const r = ref.trim().toUpperCase();
|
|
456
|
+
return !known.has(r) && !scenarios.some((s) => s.name.toUpperCase().includes(r));
|
|
457
|
+
})
|
|
458
|
+
.map((ref) => ({ phase: ph.id, ref })));
|
|
459
|
+
|
|
399
460
|
// --- Handoff integrity: no blind tail after a cross-namespace transition. ---
|
|
400
461
|
// A transition counts as asserted when ANY assertion follows it — in the entered
|
|
401
462
|
// namespace or later. Demanding the assertion in the entered namespace itself
|
|
@@ -439,7 +500,7 @@ export function flowQuality(unitDir: string, scenarios: ScenarioInfo[]): FlowQua
|
|
|
439
500
|
outcomeProven, outcomeManualOnly,
|
|
440
501
|
offGoal: offGoalScenarios.map((s) => s.name.slice(0, 80)),
|
|
441
502
|
offGoalRatio, offGoalCategories,
|
|
442
|
-
phases, phaseRatio, handoffs, sit,
|
|
503
|
+
phases, phaseRatio, danglingPhaseRefs, handoffs, sit,
|
|
443
504
|
};
|
|
444
505
|
}
|
|
445
506
|
|
|
@@ -460,7 +521,24 @@ export interface ContinuityGap { claim: string; missing: 'loss' | 'persistence'
|
|
|
460
521
|
|
|
461
522
|
const CONTINUITY_CLAIM = /\b(buffer(?:s|ed|ing)?|persist(?:s|ed|ence)?|round[- ]?trips?|restor(?:e|es|ed|ation)|carr(?:y|ied|ies)|retain(?:s|ed)?|session[- ]?storage|local[- ]?storage)\b/i;
|
|
462
523
|
const SIDE_PERSIST = /\b(restor(?:e|es|ed)|persist(?:s|ed)?|unchanged|same value|round[- ]?trips?|re-?hydrat\w*|still (?:shows|holds)|carr(?:y|ied|ies)|retain(?:s|ed)?|prefilled|pre-?filled)\b/i;
|
|
463
|
-
|
|
524
|
+
// A LOSS CONSTRUCTION, not a word list. `reset` and `expire` are domain NOUNS — every claim in a
|
|
525
|
+
// password-*reset* flow contains "reset", and every token flow contains "expire", so a bare-word
|
|
526
|
+
// list marked all of them as promising that state is lost and then demanded the missing half.
|
|
527
|
+
// A field report hit exactly that: the two sides of its mechanism were both written (in two
|
|
528
|
+
// different scenarios, which this already handles) and CONTINUITY-ONE-SIDED fired anyway (#630).
|
|
529
|
+
// The state has to be the thing that goes: `is cleared`, `loses the values`, `no longer holds`.
|
|
530
|
+
const SIDE_LOSS = new RegExp([
|
|
531
|
+
'\\b(?:lose|loses|losing|lost)\\b',
|
|
532
|
+
'\\b(?:is|are|gets?|got|becomes?|will be|was|were)\\s+(?:lost|cleared|emptied|discarded|wiped|reset|blanked)\\b',
|
|
533
|
+
'\\b(?:clears|clearing|discards|discarding|wipes|wiping)\\b',
|
|
534
|
+
'\\b(?:does|do|did)\\s+not\\s+(?:survive|persist|remain|carry)\\b',
|
|
535
|
+
'\\bnot\\s+(?:restored|retained|preserved|kept)\\b',
|
|
536
|
+
'\\bno longer\\b',
|
|
537
|
+
'\\bcomes? up (?:empty|blank)\\b',
|
|
538
|
+
// The triggers that CAUSE the loss still count when the claim names them as such.
|
|
539
|
+
'\\b(?:on|after) (?:a )?(?:reload|refresh)\\b',
|
|
540
|
+
'\\breload(?:ing|s|ed)? (?:the )?(?:page|screen|form|tab)\\b',
|
|
541
|
+
].join('|'), 'i');
|
|
464
542
|
|
|
465
543
|
/**
|
|
466
544
|
* Continuity claims in the viewpoint file whose feature proves only ONE side.
|
package/src/harness/parse.ts
CHANGED
|
@@ -230,7 +230,11 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
|
|
|
230
230
|
// assert persisted state — without this they'd be scored `shallow`, penalizing
|
|
231
231
|
// exactly the checks the review encourages. `tab order` is a STRUCTURAL
|
|
232
232
|
// assertion (a data table of focus stops) with the same no-{{ref}} property.
|
|
233
|
-
|
|
233
|
+
// `matches "<format>"` belongs here for the same reason: it carries no {{ref}} because
|
|
234
|
+
// the contract IS the shape ("Format: yyyy/mm/dd ~ yyyy/mm/dd"), and it is a stricter
|
|
235
|
+
// check than the value assert it replaces — scoring it shallow would penalize the
|
|
236
|
+
// stronger assertion.
|
|
237
|
+
if (/\{\{|contains|match data|matches\s+"|toHaveText|in (local|session) storage|tab order/i.test(step.text)) hasData = true;
|
|
234
238
|
}
|
|
235
239
|
}
|
|
236
240
|
|
|
@@ -14,24 +14,46 @@ import { readTextFile } from './read-text';
|
|
|
14
14
|
export interface DownstreamResult {
|
|
15
15
|
downstreamRoutes: string[]; // success/navigation targets ≠ own route
|
|
16
16
|
underCovered: { route: string; slug: string }[]; // referenced only by a bare page-nav
|
|
17
|
+
/**
|
|
18
|
+
* Declared navigation targets NO scenario mentions at all. These used to be skipped as "out of
|
|
19
|
+
* this screen's scope", which is right for a screen and wrong for a FLOW: a field report found a
|
|
20
|
+
* dashboard whose spec declared four click-through destinations and whose suite tested only the
|
|
21
|
+
* render conditions of the regions containing them — the navigation itself had no case, and
|
|
22
|
+
* nothing said so (#630).
|
|
23
|
+
*/
|
|
24
|
+
absent: { route: string; slug: string }[];
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
/** Routes the spec hands off to (Navigation Flow / success), other than the screen's own route. */
|
|
20
28
|
function downstreamRoutes(specText: string): string[] {
|
|
21
29
|
const ownRoute = (specText.match(/\*\*Route\*\*\s*:\s*`?(\/[^\s`]+)/) || [])[1] || '';
|
|
22
30
|
const routes = new Set<string>();
|
|
31
|
+
const negated = new Set<string>();
|
|
23
32
|
for (const line of specText.split('\n')) {
|
|
24
33
|
if (!/success|navigat|to \(|→/i.test(line)) continue;
|
|
25
34
|
// A real route's leading `/` sits at a path boundary (start, whitespace, backtick, quote, paren),
|
|
26
35
|
// NOT after a letter/digit. The lookbehind rejects prose slashes like "text/icon" or
|
|
27
36
|
// "category/brand" that aren't routes at all (H2 — they produced /icon, /button, /brand).
|
|
37
|
+
let negRun = false; // inside a "…, not `/a`/`/b`" list
|
|
38
|
+
let prevEnd = 0;
|
|
28
39
|
for (const m of line.matchAll(/(?<![A-Za-z0-9])(\/[a-z][a-z0-9/_-]+)`?/gi)) {
|
|
29
40
|
const r = m[1];
|
|
41
|
+
// "the destination is `/a/b` (singular, not `/a/bs` as this spec assumed)" declares ONE
|
|
42
|
+
// target and CORRECTS another. A spec that says where a route is NOT is doing the reader a
|
|
43
|
+
// service; extracting the disowned one and demanding a test for it punishes the correction.
|
|
44
|
+
// The negation carries across a LIST — "not `/x`/`/y`" disowns both, not just the first.
|
|
45
|
+
const before = line.slice(Math.max(0, m.index - 24), m.index);
|
|
46
|
+
const gap = line.slice(prevEnd, m.index);
|
|
47
|
+
prevEnd = m.index + m[0].length;
|
|
48
|
+
if (/\b(?:not|instead of|rather than|no longer|never|không)\s*[`'"(\[]*\s*$/i.test(before)) negRun = true;
|
|
49
|
+
else if (negRun && !/^[`'"\s)\]]*[/,]?\s*(?:or|hoặc)?\s*[`'"(\[]*$/i.test(gap)) negRun = false;
|
|
50
|
+
if (negRun) { negated.add(r); continue; }
|
|
30
51
|
if (r !== ownRoute && r.split('/').length > ownRoute.split('/').length - 0) routes.add(r);
|
|
31
52
|
}
|
|
32
53
|
}
|
|
54
|
+
for (const n of negated) if (!routes.has(n)) routes.delete(n);
|
|
33
55
|
// keep only routes that extend beyond the own route (a distinct downstream surface)
|
|
34
|
-
return [...routes].filter((r) => r !== ownRoute && (!ownRoute || r.startsWith(ownRoute + '/') || r.split('/').length >= 3));
|
|
56
|
+
return [...routes].filter((r) => !negated.has(r) && r !== ownRoute && (!ownRoute || r.startsWith(ownRoute + '/') || r.split('/').length >= 3));
|
|
35
57
|
}
|
|
36
58
|
|
|
37
59
|
/**
|
|
@@ -100,9 +122,27 @@ export function sameScreenName(slug: string, label: string): boolean {
|
|
|
100
122
|
return i >= 5 && i / Math.min(a.length, b.length) >= 0.6;
|
|
101
123
|
}
|
|
102
124
|
|
|
103
|
-
export function downstreamScope(
|
|
104
|
-
|
|
125
|
+
export function downstreamScope(
|
|
126
|
+
specText: string, scenarios: ScenarioInfo[], featureText = '',
|
|
127
|
+
): DownstreamResult {
|
|
128
|
+
const all = downstreamRoutes(specText);
|
|
129
|
+
// A one-segment route that other declared routes are BUILT ON is a prefix, not a destination:
|
|
130
|
+
// a spec noting that "every route is served under a `/xx/` locale prefix (e.g. `/xx/register`)"
|
|
131
|
+
// was read as declaring `/xx/` a navigation target, and no suite will ever "navigate to" it.
|
|
132
|
+
// Structural, so it catches any mount point — locale, tenant, api version — without a list.
|
|
133
|
+
const mounts = all.filter((r) => {
|
|
134
|
+
const segs = r.split('/').filter(Boolean);
|
|
135
|
+
return segs.length === 1 && all.some((o) => o !== r && o.startsWith(r.endsWith('/') ? r : `${r}/`));
|
|
136
|
+
});
|
|
137
|
+
// The same destination written twice — once bare, once under the mount the spec documents —
|
|
138
|
+
// is one navigation target, and reporting both doubles the work it asks for.
|
|
139
|
+
const routes = all.filter((r) => !mounts.includes(r)
|
|
140
|
+
&& !mounts.some((m) => {
|
|
141
|
+
const bare = r.slice(m.replace(/\/$/, '').length);
|
|
142
|
+
return bare.startsWith('/') && all.includes(bare);
|
|
143
|
+
}));
|
|
105
144
|
const underCovered: { route: string; slug: string }[] = [];
|
|
145
|
+
const absent: { route: string; slug: string }[] = [];
|
|
106
146
|
for (const route of routes) {
|
|
107
147
|
const slug = (route.split('/').filter(Boolean).pop() || route).toLowerCase();
|
|
108
148
|
// Referenced when the route/slug appears literally, OR when any `[Ref]` in the suite
|
|
@@ -112,16 +152,34 @@ export function downstreamScope(specText: string, scenarios: ScenarioInfo[]): Do
|
|
|
112
152
|
const refs = scenarios.filter((s) =>
|
|
113
153
|
s.haystack.includes(slug) || s.haystack.includes(route.toLowerCase()) ||
|
|
114
154
|
labelsOf(s).some((l) => sameScreenName(slug, l)));
|
|
115
|
-
if (!refs.length)
|
|
155
|
+
if (!refs.length) {
|
|
156
|
+
// A scenario's haystack is its name + steps, so a route documented in a @manual scenario's
|
|
157
|
+
// tester procedure — a COMMENT — looks like nothing at all. That route is accounted for: a
|
|
158
|
+
// person has been told to check it. Reporting "no scenario mentions it, add one" over a
|
|
159
|
+
// deliberate manual deferral is the false positive this whole round is about, so absence is
|
|
160
|
+
// measured against the feature TEXT, comments included (#651 follow-up).
|
|
161
|
+
if (!featureText.toLowerCase().includes(route.toLowerCase())) absent.push({ route, slug });
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
116
164
|
// Substantively covered only if some scenario OPERATES on the downstream — i.e. it
|
|
117
165
|
// starts there (`is on [<downstream>]`) — not merely navigates to it as a terminal
|
|
118
166
|
// `see [<downstream>] page` assertion. The latter just proves the transition.
|
|
167
|
+
// Two shapes prove the screen's CONTENT was checked, not just its route:
|
|
168
|
+
// - the suite OPENS ON it (`is on [Registration] page`), and
|
|
169
|
+
// - it asserts an element NAMESPACED to it (`see [Registration:Submit] button`) — the
|
|
170
|
+
// namespace names the screen, so the assertion is about that screen by construction.
|
|
171
|
+
// Only the first counted, so a scenario that navigated there and then asserted the screen's
|
|
172
|
+
// own elements was still reported "covered only by a page-nav assertion" (#630). The field
|
|
173
|
+
// report's hypothesis — that the parser reads one line after the nav and stops — is still not
|
|
174
|
+
// what happens: there is no line window, and both shapes are matched over the whole scenario.
|
|
119
175
|
const contentCovered = refs.some((s) =>
|
|
120
176
|
[...s.haystack.matchAll(/\bis on \[([^\]]+)\]/g)]
|
|
121
|
-
.some((m) => sameScreenName(slug, m[1].split(':')[0]))
|
|
177
|
+
.some((m) => sameScreenName(slug, m[1].split(':')[0]))
|
|
178
|
+
|| [...s.haystack.matchAll(/\bsee \[([^\]:]+):[^\]]+\]/g)]
|
|
179
|
+
.some((m) => sameScreenName(slug, m[1])));
|
|
122
180
|
if (!contentCovered) underCovered.push({ route, slug });
|
|
123
181
|
}
|
|
124
|
-
return { downstreamRoutes: routes, underCovered };
|
|
182
|
+
return { downstreamRoutes: routes, underCovered, absent };
|
|
125
183
|
}
|
|
126
184
|
|
|
127
185
|
// ---------- #4 Manual-oracle ----------
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run-manifest — which scenarios does a QA edit actually oblige us to re-run?
|
|
3
|
+
*
|
|
4
|
+
* The problem it serves: the JSON reporter writes ONE file per target, named only for the unit +
|
|
5
|
+
* platform + env, so a subset re-run overwrites the full run's results. `sungen results merge`
|
|
6
|
+
* restores the lost half, but only if the caller knew to back the file up first — and only after
|
|
7
|
+
* the caller decided WHICH cases to run. This module answers the second question deterministically
|
|
8
|
+
* so `run-test --change-only` is a computation, not a guess.
|
|
9
|
+
*
|
|
10
|
+
* Sibling of `manifest.ts`, deliberately not an extension of it: that one hashes spec.md sections
|
|
11
|
+
* to decide what to REGENERATE, this one hashes the compiled inputs to decide what to RE-RUN. Same
|
|
12
|
+
* conventions (deterministic hashing, `.sungen/` storage, a plan object), different question.
|
|
13
|
+
*
|
|
14
|
+
* The bias is fixed and one-directional: when an edit cannot be attributed to specific scenarios,
|
|
15
|
+
* EVERY scenario is selected. Over-running costs minutes; under-running reports a scenario green
|
|
16
|
+
* that never re-ran after the change that broke it, which is the failure this whole family of
|
|
17
|
+
* guards exists to prevent.
|
|
18
|
+
*
|
|
19
|
+
* Storage: `.sungen/run-manifest/<slug>.json`.
|
|
20
|
+
*/
|
|
21
|
+
import * as fs from 'fs';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
import { createHash } from 'crypto';
|
|
24
|
+
import yaml from 'yaml';
|
|
25
|
+
import { featureFilesFor, reportSlug } from './unit-paths';
|
|
26
|
+
import { readTextFile } from './read-text';
|
|
27
|
+
import { GherkinParser } from '../generators/gherkin-parser';
|
|
28
|
+
|
|
29
|
+
const sha = (s: string): string => createHash('sha256').update(s).digest('hex').slice(0, 16);
|
|
30
|
+
|
|
31
|
+
/** Mirrors SelectorResolver.generateKey — a `[Ref]` becomes this before the YAML lookup. */
|
|
32
|
+
const refToKey = (label: string): string => label.normalize('NFC').toLowerCase().trim().replace(/\s+/g, ' ');
|
|
33
|
+
|
|
34
|
+
export interface RunManifest {
|
|
35
|
+
unit: string;
|
|
36
|
+
recordedAt: string;
|
|
37
|
+
/** scenario title → hash of its whole block (tags + steps) */
|
|
38
|
+
scenarios: Record<string, string>;
|
|
39
|
+
/** selectors.yaml key → hash of that entry */
|
|
40
|
+
selectorKeys: Record<string, string>;
|
|
41
|
+
/** test-data key → hash of that value */
|
|
42
|
+
dataKeys: Record<string, string>;
|
|
43
|
+
/** scenario title → the selector keys and data keys its steps touch */
|
|
44
|
+
deps: Record<string, { selectors: string[]; data: string[] }>;
|
|
45
|
+
/** hash of the Background block: a change there reaches every scenario */
|
|
46
|
+
background: string;
|
|
47
|
+
/**
|
|
48
|
+
* PROJECT-level inputs, `qa/<file>` → hash. These reach every scenario of every unit — app.yaml
|
|
49
|
+
* carries widget recipes and the behaviour profile, datasources.yaml the db/api endpoints,
|
|
50
|
+
* capabilities.yaml which drivers even load. Per-key attribution is not possible (a recipe change
|
|
51
|
+
* can alter any step's compilation), so a move here widens to the whole unit. They were the last
|
|
52
|
+
* known under-selection hole: edit app.yaml and the selection used to report "nothing changed".
|
|
53
|
+
*/
|
|
54
|
+
globals: Record<string, string>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ChangeSelection {
|
|
58
|
+
unit: string;
|
|
59
|
+
/** scenario titles to re-run, in feature order */
|
|
60
|
+
scenarios: string[];
|
|
61
|
+
/** why each was selected — one short clause per title, for the operator */
|
|
62
|
+
reasons: Record<string, string>;
|
|
63
|
+
/** true when the whole unit was selected because the change could not be attributed */
|
|
64
|
+
all: boolean;
|
|
65
|
+
summary: { total: number; selected: number; firstRun: boolean };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const manifestPath = (cwd: string, unit: string): string =>
|
|
69
|
+
path.join(cwd, '.sungen', 'run-manifest', `${reportSlug(unit)}.json`);
|
|
70
|
+
|
|
71
|
+
/** Flatten a YAML mapping to `key → hash(value)`; a non-mapping file yields one whole-file entry. */
|
|
72
|
+
function hashTopLevel(file: string): Record<string, string> {
|
|
73
|
+
if (!fs.existsSync(file)) return {};
|
|
74
|
+
const text = readTextFile(file);
|
|
75
|
+
let doc: unknown;
|
|
76
|
+
try {
|
|
77
|
+
doc = yaml.parse(text);
|
|
78
|
+
} catch {
|
|
79
|
+
// Unparseable: attribute nothing, and let the caller widen to the whole unit rather than
|
|
80
|
+
// silently treating a broken file as "no change".
|
|
81
|
+
return { __unparseable__: sha(text) };
|
|
82
|
+
}
|
|
83
|
+
// An empty or comment-only file parses to null. That is an EMPTY mapping — nothing to attribute
|
|
84
|
+
// and nothing to widen on — not an unrecognised shape. (A test-data file holding only a comment
|
|
85
|
+
// made the first probe widen to all 18 scenarios.)
|
|
86
|
+
if (doc === null || doc === undefined) return {};
|
|
87
|
+
if (typeof doc !== 'object' || Array.isArray(doc)) return { __whole__: sha(text) };
|
|
88
|
+
const out: Record<string, string> = {};
|
|
89
|
+
for (const [k, v] of Object.entries(doc as Record<string, unknown>)) {
|
|
90
|
+
out[refToKey(k)] = sha(JSON.stringify(v ?? null));
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Read the unit's compiled inputs and fingerprint them. */
|
|
96
|
+
export function buildRunManifest(cwd: string, unitDir: string, unit: string): RunManifest {
|
|
97
|
+
const parser = new GherkinParser();
|
|
98
|
+
const scenarios: Record<string, string> = {};
|
|
99
|
+
const deps: RunManifest['deps'] = {};
|
|
100
|
+
const bgParts: string[] = [];
|
|
101
|
+
|
|
102
|
+
for (const file of featureFilesFor(unitDir, unit)) {
|
|
103
|
+
const parsed = parser.parseFeatureFile(file);
|
|
104
|
+
if (parsed.background?.steps?.length) {
|
|
105
|
+
bgParts.push(parsed.background.steps.map((s) => `${s.keyword} ${s.text}`).join('\n'));
|
|
106
|
+
}
|
|
107
|
+
for (const sc of parsed.scenarios) {
|
|
108
|
+
const block = [...(sc.tags || []), ...sc.steps.map((s) => `${s.keyword} ${s.text}`)].join('\n');
|
|
109
|
+
scenarios[sc.name] = sha(block);
|
|
110
|
+
const sel = new Set<string>();
|
|
111
|
+
const dat = new Set<string>();
|
|
112
|
+
for (const st of sc.steps) {
|
|
113
|
+
if (st.selectorRef) sel.add(refToKey(st.selectorRef));
|
|
114
|
+
if (st.parentRef) sel.add(refToKey(st.parentRef));
|
|
115
|
+
if (st.childRef) sel.add(refToKey(st.childRef));
|
|
116
|
+
if (st.dataRef) dat.add(refToKey(st.dataRef));
|
|
117
|
+
}
|
|
118
|
+
deps[sc.name] = { selectors: [...sel].sort(), data: [...dat].sort() };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const selDir = path.join(unitDir, 'selectors');
|
|
123
|
+
const dataDir = path.join(unitDir, 'test-data');
|
|
124
|
+
const collect = (dir: string): Record<string, string> => {
|
|
125
|
+
if (!fs.existsSync(dir)) return {};
|
|
126
|
+
const merged: Record<string, string> = {};
|
|
127
|
+
for (const f of fs.readdirSync(dir).filter((n) => /\.ya?ml$/.test(n)).sort()) {
|
|
128
|
+
Object.assign(merged, hashTopLevel(path.join(dir, f)));
|
|
129
|
+
}
|
|
130
|
+
return merged;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Project-level config lives beside the units, not inside them.
|
|
134
|
+
const qaDir = path.join(cwd, 'qa');
|
|
135
|
+
const globals: Record<string, string> = {};
|
|
136
|
+
for (const name of ['app.yaml', 'datasources.yaml', 'capabilities.yaml']) {
|
|
137
|
+
const f = path.join(qaDir, name);
|
|
138
|
+
if (fs.existsSync(f)) globals[`qa/${name}`] = sha(readTextFile(f));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
unit,
|
|
143
|
+
recordedAt: new Date().toISOString(),
|
|
144
|
+
scenarios,
|
|
145
|
+
selectorKeys: collect(selDir),
|
|
146
|
+
dataKeys: collect(dataDir),
|
|
147
|
+
deps,
|
|
148
|
+
background: sha(bgParts.join('\n---\n')),
|
|
149
|
+
globals,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function readRunManifest(cwd: string, unit: string): RunManifest | null {
|
|
154
|
+
const p = manifestPath(cwd, unit);
|
|
155
|
+
// Absent baseline is the FIRST-RUN case, not an error — selectChanged widens to the whole unit.
|
|
156
|
+
if (!fs.existsSync(p)) return null;
|
|
157
|
+
try { return JSON.parse(readTextFile(p)) as RunManifest; } catch { return null; }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function writeRunManifest(cwd: string, unit: string, m: RunManifest): string {
|
|
161
|
+
const p = manifestPath(cwd, unit);
|
|
162
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
163
|
+
fs.writeFileSync(p, JSON.stringify(m, null, 2));
|
|
164
|
+
return p;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Keys whose hash differs, was added, or disappeared. */
|
|
168
|
+
function movedKeys(before: Record<string, string>, after: Record<string, string>): Set<string> {
|
|
169
|
+
const moved = new Set<string>();
|
|
170
|
+
for (const k of new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
171
|
+
if (before[k] !== after[k]) moved.add(k);
|
|
172
|
+
}
|
|
173
|
+
return moved;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Which scenarios must re-run, given the recorded manifest and the current inputs.
|
|
178
|
+
*
|
|
179
|
+
* A selector key matches a step's `[Ref]` either exactly or as one of the resolver's suffixed
|
|
180
|
+
* forms (`key--button`, `key--3`), because that is how the lookup itself resolves.
|
|
181
|
+
*/
|
|
182
|
+
export function selectChanged(
|
|
183
|
+
before: RunManifest | null,
|
|
184
|
+
after: RunManifest,
|
|
185
|
+
/**
|
|
186
|
+
* Run-affecting files an outside source (git) reports as changed. Passed in rather than read here
|
|
187
|
+
* so this function stays pure and testable; the CLI gathers the evidence. Non-empty + an
|
|
188
|
+
* otherwise-empty selection means something changed that per-key hashing cannot see (a rename, a
|
|
189
|
+
* new file shape) — unattributable, so it widens like the other unattributable cases.
|
|
190
|
+
*/
|
|
191
|
+
externalChanged: string[] = []
|
|
192
|
+
): ChangeSelection {
|
|
193
|
+
const titles = Object.keys(after.scenarios);
|
|
194
|
+
const selectAll = (reason: string): ChangeSelection => ({
|
|
195
|
+
unit: after.unit,
|
|
196
|
+
scenarios: titles,
|
|
197
|
+
reasons: Object.fromEntries(titles.map((t) => [t, reason])),
|
|
198
|
+
all: true,
|
|
199
|
+
summary: { total: titles.length, selected: titles.length, firstRun: !before },
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (!before) return selectAll('no previous run recorded');
|
|
203
|
+
const movedGlobals = movedKeys(before.globals || {}, after.globals || {});
|
|
204
|
+
if (movedGlobals.size) {
|
|
205
|
+
return selectAll(`project config changed (${[...movedGlobals].join(', ')}) — it reaches every scenario`);
|
|
206
|
+
}
|
|
207
|
+
if (before.background !== after.background) return selectAll('the Background changed — it runs before every scenario');
|
|
208
|
+
if (after.selectorKeys.__unparseable__ || after.dataKeys.__unparseable__) {
|
|
209
|
+
return selectAll('a selectors/test-data file could not be parsed, so the edit cannot be attributed');
|
|
210
|
+
}
|
|
211
|
+
if (after.selectorKeys.__whole__ || after.dataKeys.__whole__) {
|
|
212
|
+
const movedWhole = movedKeys(before.selectorKeys, after.selectorKeys).size
|
|
213
|
+
|| movedKeys(before.dataKeys, after.dataKeys).size;
|
|
214
|
+
if (movedWhole) return selectAll('a selectors/test-data file is not a key mapping, so the edit cannot be attributed');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const movedSel = movedKeys(before.selectorKeys, after.selectorKeys);
|
|
218
|
+
const movedDat = movedKeys(before.dataKeys, after.dataKeys);
|
|
219
|
+
const touches = (key: string, dep: string): boolean => key === dep || key.startsWith(`${dep}--`);
|
|
220
|
+
|
|
221
|
+
const reasons: Record<string, string> = {};
|
|
222
|
+
for (const title of titles) {
|
|
223
|
+
if (before.scenarios[title] !== after.scenarios[title]) {
|
|
224
|
+
reasons[title] = before.scenarios[title] ? 'its steps or tags changed' : 'new scenario';
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const dep = after.deps[title] || { selectors: [], data: [] };
|
|
228
|
+
const sel = [...movedSel].filter((k) => dep.selectors.some((d) => touches(k, d)));
|
|
229
|
+
if (sel.length) { reasons[title] = `selector changed: ${sel.slice(0, 3).join(', ')}`; continue; }
|
|
230
|
+
const dat = [...movedDat].filter((k) => dep.data.some((d) => touches(k, d)));
|
|
231
|
+
if (dat.length) { reasons[title] = `test-data changed: ${dat.slice(0, 3).join(', ')}`; continue; }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let selected = titles.filter((t) => reasons[t]);
|
|
235
|
+
|
|
236
|
+
// Git cross-check. Per-key hashing is blind to structural moves — a renamed selectors file, a new
|
|
237
|
+
// file shape, an encoding change — because the keys simply appear or vanish. When an outside
|
|
238
|
+
// source reports run-affecting files changed and we attributed NOTHING, that is the same
|
|
239
|
+
// unattributable case as an unparseable YAML, so it widens rather than reporting "all current".
|
|
240
|
+
if (!selected.length && externalChanged.length) {
|
|
241
|
+
return selectAll(
|
|
242
|
+
`git reports ${externalChanged.length} run-affecting file(s) changed (${externalChanged.slice(0, 3).join(', ')}) `
|
|
243
|
+
+ 'that per-key hashing could not attribute'
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
unit: after.unit,
|
|
249
|
+
scenarios: selected,
|
|
250
|
+
reasons,
|
|
251
|
+
all: false,
|
|
252
|
+
summary: { total: titles.length, selected: selected.length, firstRun: false },
|
|
253
|
+
};
|
|
254
|
+
}
|