@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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { scanSegments, type Segment } from './js-literal-scanner';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Wire format of a runtime test-data marker: `__SUNGEN_TD$<key>$__`.
|
|
3
5
|
*
|
|
@@ -14,12 +16,12 @@
|
|
|
14
16
|
* swallowed one `_` of the `___` run and decoding produced `get('code_')` plus
|
|
15
17
|
* a detached `ACCESS_TOKEN`. `$` is also identifier-safe — the whole marker
|
|
16
18
|
* still parses as ONE bare JS token, so it survives the bare-identifier
|
|
17
|
-
* (numeric) positions
|
|
19
|
+
* (numeric) positions the code context handles.
|
|
18
20
|
*
|
|
19
|
-
* Single source of truth: every
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* Single source of truth: every context below is built from these constants.
|
|
22
|
+
* Invariant: every marker producer goes through DataResolver.encodeMarker and
|
|
23
|
+
* every consumer through this module — a drifting hand-copied char class is what
|
|
24
|
+
* let the suffix bug hide.
|
|
23
25
|
*/
|
|
24
26
|
const MARKER_KEY = '[A-Za-z0-9_-]+';
|
|
25
27
|
// Templates that embed a value into a regex literal run it through the escapeRegex
|
|
@@ -27,100 +29,148 @@ const MARKER_KEY = '[A-Za-z0-9_-]+';
|
|
|
27
29
|
// arrive here spelled `__SUNGEN_TD\$key\$__`. `\\\\?` accepts that optional backslash
|
|
28
30
|
// before each delimiter; the replacement consumes it, so no escape leaks to output.
|
|
29
31
|
const DELIM = '\\\\?\\$';
|
|
30
|
-
/** Marker with no capture group — for "contains a marker"
|
|
32
|
+
/** Marker with no capture group — for "contains a marker" probes. */
|
|
31
33
|
const MARKER = `__SUNGEN_TD${DELIM}${MARKER_KEY}${DELIM}__`;
|
|
32
34
|
/** Marker capturing the encoded key. */
|
|
33
35
|
const MARKER_CAP = `__SUNGEN_TD${DELIM}(${MARKER_KEY})${DELIM}__`;
|
|
34
36
|
|
|
35
37
|
const MARKER_PATTERN = new RegExp(MARKER);
|
|
36
|
-
|
|
37
|
-
const RE_COMMENT_LINE = new RegExp(`^(\\s*)\\/\\/(.*)${MARKER_CAP}(.*)$`, 'gm');
|
|
38
|
-
// Pass 1: a quoted string literal containing at least one marker. Covers ', " and
|
|
39
|
-
// backtick containers; `\\.` lets an ESCAPED quote (`'it\'s {{x}}'`) sit inside the
|
|
40
|
-
// body instead of terminating the literal early. The single-char branch excludes `\`
|
|
41
|
-
// so every backslash is consumed by exactly ONE branch — an ambiguous `(?!\1).`
|
|
42
|
-
// alternative enumerates 2^n segmentations when a marker-less escape-heavy literal
|
|
43
|
-
// (escapeQuotes output: multi-line Flutter content-desc) fails to match, hanging
|
|
44
|
-
// the generate run. `\n` exclusion just makes the dot's no-newline behavior explicit.
|
|
45
|
-
const RE_STRING_LITERAL = new RegExp('([\'"`])((?:\\\\.|(?!\\1)[^\\\\\\n])*?' + MARKER + '(?:\\\\.|(?!\\1)[^\\\\\\n])*)\\1', 'g');
|
|
46
|
-
// Pass 2: a single-line regex literal containing at least one marker.
|
|
47
|
-
const RE_REGEX_LITERAL = new RegExp(`\\/((?:[^/\\\\\\n]|\\\\.)*?${MARKER}(?:[^/\\\\\\n]|\\\\.)*)\\/([gimsuy]*)`, 'g');
|
|
48
|
-
// Whole-literal probe: the body is EXACTLY one marker (bare accessor call, no template).
|
|
38
|
+
/** Whole-literal probe: the body is EXACTLY one marker (bare accessor call, no template). */
|
|
49
39
|
const RE_WHOLE_MARKER = new RegExp(`^${MARKER_CAP}$`);
|
|
50
|
-
|
|
40
|
+
/** Every marker occurrence, capturing the key. */
|
|
51
41
|
const RE_MARKER_G = new RegExp(MARKER_CAP, 'g');
|
|
52
42
|
|
|
53
43
|
/**
|
|
54
44
|
* Replace __SUNGEN_TD$…$__ markers with testData.get() calls in generated code.
|
|
55
|
-
*
|
|
45
|
+
*
|
|
46
|
+
* The code is scanned ONCE into literal/regex/comment/code segments and each
|
|
47
|
+
* segment is rewritten for the context it is actually in. This replaced four
|
|
48
|
+
* independent global-regex passes whose ordering was the defect: the string pass
|
|
49
|
+
* ran first and, finding no marker inside a genuine literal, re-matched from that
|
|
50
|
+
* literal's CLOSING quote — pairing it with the OPENING quote of the next literal
|
|
51
|
+
* and rewriting the regex literal between them into a broken backtick template
|
|
52
|
+
* (`getByText('Tìm thấy`)` … `state: `visible' })`, a .spec.ts that would not
|
|
53
|
+
* parse at all). See js-literal-scanner.ts for the full field report.
|
|
56
54
|
*/
|
|
57
55
|
export function transformToRuntimeData(code: string, accessor: string = 'testData'): string {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// — e.g. a URL `https://careerforum.aki-inc.jp/` in a selector name — is NOT mistaken
|
|
62
|
-
// for a comment. Before this anchor, such a URL preceding a marker on the same line
|
|
63
|
-
// made Pass 0 strip the marker to its bare key, so Pass 1 never wrapped it in
|
|
64
|
-
// testData.get() (silent: `toHaveAttribute('href', 'official_site_url')`). Generated
|
|
65
|
-
// comments are always full-line, so anchoring loses nothing.
|
|
66
|
-
code = code.replace(
|
|
67
|
-
RE_COMMENT_LINE,
|
|
68
|
-
(_, indent, before, enc, after) => `${indent}//${before}${decodeKey(enc)}${after}`
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
// Pass 1: String literal context — a literal may carry SEVERAL markers
|
|
72
|
-
// ('/{{code}}/car-search?{{prefill}}' compiles to two markers in ONE literal).
|
|
73
|
-
// Match the whole quoted literal that contains at least one marker, then
|
|
74
|
-
// interpolate EVERY marker inside it. The old shape (one marker + lazy
|
|
75
|
-
// prefix/suffix) swallowed trailing markers into its suffix verbatim; they
|
|
76
|
-
// escaped the quote context (the literal had become a backtick template) and
|
|
77
|
-
// Pass 3's numeric catch-all emitted them as literal
|
|
78
|
-
// `Number(testData.get(...))` TEXT inside the template — a compiling spec
|
|
79
|
-
// navigating to a garbage URL.
|
|
80
|
-
// 'marker' → testData.get('key')
|
|
81
|
-
// 'a__m1__b__m2__c' → `a${testData.get('k1')}b${testData.get('k2')}c`
|
|
82
|
-
code = code.replace(
|
|
83
|
-
RE_STRING_LITERAL,
|
|
84
|
-
(_, _quote, body) => {
|
|
85
|
-
const whole = RE_WHOLE_MARKER.exec(body);
|
|
86
|
-
if (whole) return `${accessor}.get('${decodeKey(whole[1])}')`;
|
|
87
|
-
return `\`${interpolateMarkers(body, accessor)}\``;
|
|
88
|
-
}
|
|
89
|
-
);
|
|
56
|
+
if (!MARKER_PATTERN.test(code)) return code;
|
|
57
|
+
return scanSegments(code).map((seg) => transformSegment(seg, accessor)).join('');
|
|
58
|
+
}
|
|
90
59
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
code = code.replace(
|
|
95
|
-
RE_REGEX_LITERAL,
|
|
96
|
-
(_, body, flags) => {
|
|
97
|
-
const flagStr = flags ? `, '${flags}'` : '';
|
|
98
|
-
const whole = RE_WHOLE_MARKER.exec(body);
|
|
99
|
-
if (whole) return `new RegExp(${accessor}.get('${decodeKey(whole[1])}')${flagStr})`;
|
|
100
|
-
return `new RegExp(\`${interpolateMarkers(body, accessor)}\`${flagStr})`;
|
|
101
|
-
}
|
|
102
|
-
);
|
|
60
|
+
export function hasRuntimeDataMarkers(code: string): boolean {
|
|
61
|
+
return MARKER_PATTERN.test(code);
|
|
62
|
+
}
|
|
103
63
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
64
|
+
function transformSegment(seg: Segment, accessor: string): string {
|
|
65
|
+
if (!MARKER_PATTERN.test(seg.text)) return seg.text;
|
|
66
|
+
switch (seg.kind) {
|
|
67
|
+
// Decode to the plain key: a marker in a comment documents the ref, it is not code.
|
|
68
|
+
case 'comment':
|
|
69
|
+
return seg.text.replace(RE_MARKER_G, (_, enc) => decodeKey(enc));
|
|
70
|
+
case 'string':
|
|
71
|
+
return transformStringLiteral(seg.text, accessor);
|
|
72
|
+
case 'regex':
|
|
73
|
+
return transformRegexLiteral(seg.text, accessor);
|
|
74
|
+
// Bare-token position (e.g. `toHaveCount(__marker__)` from the table/list count
|
|
75
|
+
// templates). testData.get() returns a string, so coerce with Number().
|
|
76
|
+
case 'code':
|
|
77
|
+
return seg.text.replace(RE_MARKER_G, (_, enc) => `Number(${accessor}.get('${decodeKey(enc)}'))`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
111
80
|
|
|
112
|
-
|
|
81
|
+
/**
|
|
82
|
+
* 'marker' → testData.get('key')
|
|
83
|
+
* 'a__m1__b__m2__c' → `a${testData.get('k1')}b${testData.get('k2')}c`
|
|
84
|
+
*
|
|
85
|
+
* A literal may carry SEVERAL markers ('/{{code}}/car-search?{{prefill}}' compiles
|
|
86
|
+
* to two markers in ONE literal), so every marker inside it is interpolated.
|
|
87
|
+
*/
|
|
88
|
+
function transformStringLiteral(text: string, accessor: string): string {
|
|
89
|
+
const quote = text[0];
|
|
90
|
+
const body = text.slice(1, -1);
|
|
91
|
+
const whole = RE_WHOLE_MARKER.exec(body);
|
|
92
|
+
if (whole) return `${accessor}.get('${decodeKey(whole[1])}')`;
|
|
93
|
+
return `\`${interpolateMarkers(escapeForTemplate(body, quote === '`'), accessor)}\``;
|
|
113
94
|
}
|
|
114
95
|
|
|
115
|
-
|
|
116
|
-
|
|
96
|
+
/**
|
|
97
|
+
* /…$marker$…/flags → new RegExp(`…`, 'flags'). Same several-markers rule as above.
|
|
98
|
+
*
|
|
99
|
+
* Two regex-only twists on top of the string path:
|
|
100
|
+
* - The runtime VALUE is literal text (test-data holds data, not patterns), so every
|
|
101
|
+
* accessor is wrapped in the runtime escapeRegExp helper — a value containing
|
|
102
|
+
* `( [ $ . * +` must not throw a SyntaxError or silently widen the match. The helper
|
|
103
|
+
* lives in the synced specs/regex-escape.ts; the generator imports it when the
|
|
104
|
+
* transformed body references it. Templates that intend PATTERN semantics (attribute
|
|
105
|
+
* `pattern:`) emit a string container instead of a regex literal, so they skip this.
|
|
106
|
+
* - Marker-less SEGMENTS get their backslashes doubled for the template context: regex
|
|
107
|
+
* source text like a hand-written `\s*` (or escapeRegex output `\)`) means one thing
|
|
108
|
+
* in a /…/ literal but loses its backslash when the template cooks one escape level —
|
|
109
|
+
* the constructed RegExp would read `s*` / a bare `)`. Segments are escaped AFTER the
|
|
110
|
+
* markers are cut out, so an escapeRegex-spelled marker delimiter (`\$`) is never
|
|
111
|
+
* double-escaped into something the marker regex can no longer see.
|
|
112
|
+
*/
|
|
113
|
+
function transformRegexLiteral(text: string, accessor: string): string {
|
|
114
|
+
// Flags never contain `/`, so the last slash always closes the pattern.
|
|
115
|
+
const close = text.lastIndexOf('/');
|
|
116
|
+
const body = text.slice(1, close);
|
|
117
|
+
const flags = text.slice(close + 1);
|
|
118
|
+
const flagStr = flags ? `, '${flags}'` : '';
|
|
119
|
+
const whole = RE_WHOLE_MARKER.exec(body);
|
|
120
|
+
if (whole) return `new RegExp(escapeRegExp(${accessor}.get('${decodeKey(whole[1])}'))${flagStr})`;
|
|
121
|
+
return `new RegExp(\`${interpolateMarkersInRegexBody(body, accessor)}\`${flagStr})`;
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
function decodeKey(encoded: string): string {
|
|
120
125
|
return encoded.replace(/_D_/g, '.');
|
|
121
126
|
}
|
|
122
127
|
|
|
123
|
-
/** Replace every marker in a
|
|
128
|
+
/** Replace every marker in a body with a `${accessor.get('key')}` interpolation. */
|
|
124
129
|
function interpolateMarkers(body: string, accessor: string): string {
|
|
125
130
|
return body.replace(RE_MARKER_G, (_, enc) => `\${${accessor}.get('${decodeKey(enc)}')}`);
|
|
126
131
|
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Regex-body variant: markers are cut out FIRST, each accessor is wrapped in the
|
|
135
|
+
* runtime escapeRegExp helper, and only the marker-less segments between them are
|
|
136
|
+
* re-escaped for the template context (backslashes doubled, backtick/`${` neutralized).
|
|
137
|
+
*/
|
|
138
|
+
function interpolateMarkersInRegexBody(body: string, accessor: string): string {
|
|
139
|
+
let out = '';
|
|
140
|
+
let last = 0;
|
|
141
|
+
for (const m of body.matchAll(RE_MARKER_G)) {
|
|
142
|
+
out += escapeRegexSegmentForTemplate(body.slice(last, m.index!)) +
|
|
143
|
+
`\${escapeRegExp(${accessor}.get('${decodeKey(m[1])}'))}`;
|
|
144
|
+
last = m.index! + m[0].length;
|
|
145
|
+
}
|
|
146
|
+
return out + escapeRegexSegmentForTemplate(body.slice(last));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Regex pattern text moving into a template literal: the template layer cooks one
|
|
151
|
+
* level of escapes BEFORE RegExp ever sees the string, so `\)` arrives as a bare `)`
|
|
152
|
+
* (SyntaxError: unmatched paren) and `\.` as a match-anything dot. Double the
|
|
153
|
+
* backslashes and neutralize the template's own metacharacters so the constructed
|
|
154
|
+
* RegExp source equals the original pattern text.
|
|
155
|
+
*/
|
|
156
|
+
const escapeRegexSegmentForTemplate = (segment: string): string =>
|
|
157
|
+
segment.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Both rewrites move a body into a backtick template, where a raw backtick or `${`
|
|
161
|
+
* would be read as syntax rather than text — a `'…'` selector value or a regex
|
|
162
|
+
* pattern is allowed to contain either (escapeQuotes/escapeRegex escape neither).
|
|
163
|
+
* Escape before interpolating so the `${…}` this module inserts is the only live one.
|
|
164
|
+
* An already-backtick container needs no pass: its body was valid template text.
|
|
165
|
+
*
|
|
166
|
+
* PRECONDITION: the body must already have its backslashes DOUBLED — which every caller
|
|
167
|
+
* satisfies, because every dynamic value reaches a literal through escapeQuotes or
|
|
168
|
+
* escapeRegex (template-engine.ts) and both double `\` before anything else. An unpaired
|
|
169
|
+
* backslash immediately before a backtick would make the pass below emit `\\\``, i.e. an
|
|
170
|
+
* escaped backslash followed by a LIVE backtick, ending the template early. If you
|
|
171
|
+
* hand-write a backslash into a .hbs template, keep it paired.
|
|
172
|
+
*/
|
|
173
|
+
function escapeForTemplate(body: string, alreadyTemplate: boolean): string {
|
|
174
|
+
if (alreadyTemplate) return body;
|
|
175
|
+
return body.replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
176
|
+
}
|
|
@@ -229,6 +229,24 @@ export class SelectorResolver {
|
|
|
229
229
|
* - Legacy format: "login:email_field" or "login.email_field" (with screen prefix)
|
|
230
230
|
*/
|
|
231
231
|
resolveSelector(selectorRef: string, featureName?: string, elementType?: string, nth?: number): ResolvedSelector {
|
|
232
|
+
// A [Reference] is a selectors.yaml KEY, looked up verbatim (generateKey only lowercases and
|
|
233
|
+
// normalizes whitespace) — it is never interpolated. Embedding a {{var}} in it is therefore
|
|
234
|
+
// unsupported, and silence was the harmful part: with no matching entry the ref fell through
|
|
235
|
+
// to auto-infer and produced getByRole(..., { name: '{{var}}' }) — an accessible name that can
|
|
236
|
+
// never match — so the run failed far from the cause. Fail here instead, naming the two
|
|
237
|
+
// supported ways to target dynamic text. Browser-storage keys legitimately embed vars but are
|
|
238
|
+
// not selectors and never reach this method (see driver-ui storage-patterns).
|
|
239
|
+
const templateVarInRef = /\{\{\s*([^}]+?)\s*\}\}/.exec(selectorRef);
|
|
240
|
+
if (templateVarInRef) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`Selector reference "[${selectorRef}]" embeds a template variable ({{${templateVarInRef[1]}}}). ` +
|
|
243
|
+
`A [Reference] is a selectors.yaml key and is matched verbatim, not interpolated. ` +
|
|
244
|
+
`To target text that varies (locale, @cases column), keep the reference static and put the ` +
|
|
245
|
+
`variable in the entry instead — e.g. "confirm answer": { type: role, value: button, name: "{{${templateVarInRef[1]}}}" } ` +
|
|
246
|
+
`used as [Confirm Answer] button. To pick an element by its text inside a container, use ` +
|
|
247
|
+
`\`with {{${templateVarInRef[1]}}}\` (e.g. User click [Answer List] item with {{${templateVarInRef[1]}}}).`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
232
250
|
const contextName = featureName || this.featureName;
|
|
233
251
|
|
|
234
252
|
// Check if it's legacy format (contains : or has dot with screen prefix)
|
|
@@ -590,7 +608,9 @@ export class SelectorResolver {
|
|
|
590
608
|
});
|
|
591
609
|
}
|
|
592
610
|
|
|
593
|
-
// If type is 'locator', use locator field (or value as fallback) as the CSS locator directly
|
|
611
|
+
// If type is 'locator', use locator field (or value as fallback) as the CSS locator directly.
|
|
612
|
+
// `exact` travels along: a locator LIST filtered by a test-data value (hasText) matches by
|
|
613
|
+
// substring by default, and `exact: true` in the YAML opts into the anchored full-text match.
|
|
594
614
|
if (type === 'locator') {
|
|
595
615
|
const locatorValue = locator && locator.trim() ? locator : value;
|
|
596
616
|
return withExtras({
|
|
@@ -598,6 +618,7 @@ export class SelectorResolver {
|
|
|
598
618
|
value: locatorValue,
|
|
599
619
|
locator: locatorValue,
|
|
600
620
|
nth,
|
|
621
|
+
exact,
|
|
601
622
|
});
|
|
602
623
|
}
|
|
603
624
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { parse } from '@babel/parser';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compiler self-check for emitted spec files.
|
|
5
|
+
*
|
|
6
|
+
* Every template and post-processor in the generator assembles TEXT, so a delimiter
|
|
7
|
+
* bug in any one of them produces a file Playwright cannot parse AT ALL: one
|
|
8
|
+
* SyntaxError, zero scenarios run, and nothing points at the compiler. That is how the
|
|
9
|
+
* runtime-data quote-pairing defect reached a user (3.2.23) — `sungen generate` printed
|
|
10
|
+
* success and the breakage only surfaced a step later, at `npx playwright test`.
|
|
11
|
+
*
|
|
12
|
+
* Parsing what we are about to write costs one parse per spec file and makes the
|
|
13
|
+
* compiler name its own bad output at the moment it makes it (SG-E022).
|
|
14
|
+
*/
|
|
15
|
+
export function findSyntaxError(code: string): string | null {
|
|
16
|
+
try {
|
|
17
|
+
parse(code, { sourceType: 'module', plugins: ['typescript'] });
|
|
18
|
+
return null;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
return error instanceof Error ? error.message : String(error);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -3,13 +3,18 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Parses `name(a={{x}},b="lit",c=3)` overrides into a map of JS expressions, e.g.
|
|
5
5
|
* `{ a: "testData.raw('x')", b: "\"lit\"", c: "3" }`. Used by the DB and API capability drivers'
|
|
6
|
-
* precondition codegen; lives in core so both drivers can share it. Gherkin tags carry no
|
|
7
|
-
*
|
|
6
|
+
* precondition codegen; lives in core so both drivers can share it. Gherkin tags carry no
|
|
7
|
+
* whitespace, but a QUOTED value can still embed `{{ref}}` next to static text
|
|
8
|
+
* (`user_email="pre-{{id}}-x"`) — those interpolate through literalExpr; shipping the braces
|
|
9
|
+
* verbatim would bind `{{...}}` text as a SQL parameter or POST it in a live request body.
|
|
10
|
+
* Flows thread a prior response via a whole-value ref, e.g.
|
|
8
11
|
* `@api:get_profile(token={{login.body.token}})`, with the auth scheme declared in the catalog header.
|
|
9
|
-
* A `{{ref}}` override binds into a typed sink (DB query param, JSON request body), so it
|
|
10
|
-
* via `testData.raw()` (native type, still interpolates embedded refs) rather than `get()`
|
|
12
|
+
* A bare `{{ref}}` override binds into a typed sink (DB query param, JSON request body), so it
|
|
13
|
+
* resolves via `testData.raw()` (native type, still interpolates embedded refs) rather than `get()`
|
|
11
14
|
* (Gherkin-text stringify) — a numeric override otherwise reaches the DB/API as a JSON string.
|
|
12
15
|
*/
|
|
16
|
+
import { literalExpr } from '../generators/test-generator/patterns/expect-patterns';
|
|
17
|
+
|
|
13
18
|
export function parseQueryOverrides(raw?: string): Record<string, string> {
|
|
14
19
|
const out: Record<string, string> = {};
|
|
15
20
|
if (!raw) return out;
|
|
@@ -22,7 +27,7 @@ export function parseQueryOverrides(raw?: string): Record<string, string> {
|
|
|
22
27
|
const v = val.match(/^\{\{\s*([^}]+?)\s*\}\}$/);
|
|
23
28
|
const q = val.match(/^["'](.*)["']$/);
|
|
24
29
|
if (v) out[key] = `testData.raw(${JSON.stringify(v[1])})`;
|
|
25
|
-
else if (q) out[key] =
|
|
30
|
+
else if (q) out[key] = literalExpr(q[1]);
|
|
26
31
|
else if (/^-?\d+(?:\.\d+)?$/.test(val)) out[key] = val;
|
|
27
32
|
else out[key] = JSON.stringify(val);
|
|
28
33
|
}
|
package/src/harness/audit.ts
CHANGED
|
@@ -27,6 +27,8 @@ import { specCoverage, SpecCoverageResult, parseSpecClauses, restatedRequirement
|
|
|
27
27
|
import { downstreamScope, manualOracle, readText, DownstreamResult, ManualOracleResult,
|
|
28
28
|
negativeSideEffect, sourceBacked, crossArtifactOwnership, isolationRisk, serialCascadeRisk } from './quality-gates';
|
|
29
29
|
import { viewpointLedger, parseViewpointItems, browserGestureSubstitutions, viewpointIntegrity, LedgerResult } from './viewpoint-ledger';
|
|
30
|
+
import { specBranches, unenumeratedBranches, sameOracleClusters, permissionPairGaps, awaitingSeedData, unconsideredRisks, MULTI_SCREEN_RISKS } from './spec-branches';
|
|
31
|
+
const MULTI_SCREEN_RISK_COUNT = MULTI_SCREEN_RISKS.length;
|
|
30
32
|
import { capabilityRegistry } from '../capabilities/registry';
|
|
31
33
|
import { discoverAndRegisterCapabilities } from '../capabilities/discover';
|
|
32
34
|
import { contextRouter } from '../capabilities/context-router';
|
|
@@ -65,6 +67,12 @@ export interface AuditReport {
|
|
|
65
67
|
score: {
|
|
66
68
|
overall: number; // 0..10, business-weighted over the APPLICABLE axes
|
|
67
69
|
notApplicable?: string[]; // axes with no evidence — excluded, not scored as 1.0
|
|
70
|
+
/**
|
|
71
|
+
* Why each excluded axis is excluded. "No evidence" and "evidence WITHHELD pending review"
|
|
72
|
+
* are different states with different remedies, and reporting both as "no evidence" told the
|
|
73
|
+
* author of a 93%-covered ledger to go write the viewpoint they had just written (#657).
|
|
74
|
+
*/
|
|
75
|
+
naReasons?: Record<string, string>;
|
|
68
76
|
cappedAt?: number; // set when a weak critical axis holds the score down
|
|
69
77
|
coverage: number; // 0..1
|
|
70
78
|
businessDepth: number; // 0..1
|
|
@@ -76,7 +84,7 @@ export interface AuditReport {
|
|
|
76
84
|
* `specFR` and `atomicLedger` entirely — so a flow's real coverage axis was never printed
|
|
77
85
|
* while `balance`, which carried no weight, was.
|
|
78
86
|
*/
|
|
79
|
-
axes: Array<{ key: string; value: number; weight: number; applicable: boolean; critical: boolean; byDesign?: boolean }>;
|
|
87
|
+
axes: Array<{ key: string; value: number; weight: number; applicable: boolean; critical: boolean; byDesign?: boolean; withheld?: boolean }>;
|
|
80
88
|
formula: string;
|
|
81
89
|
};
|
|
82
90
|
gateStatus: 'PASS' | 'FAIL';
|
|
@@ -196,7 +204,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
196
204
|
const isUiFlowUnit = /^flows\//.test(catalogScreenName);
|
|
197
205
|
const flowQ: FlowQualityResult = isUiFlowUnit
|
|
198
206
|
? flowQuality(screenDir, scenarios)
|
|
199
|
-
: { hasContract: false, errors: [], outcomeProven: false, outcomeManualOnly: false, offGoal: [], offGoalRatio: 0, offGoalCategories: [], phases: [], phaseRatio: 1, handoffs: { total: 0, asserted: 0, ratio: 1 }, sit: [] };
|
|
207
|
+
: { hasContract: false, errors: [], outcomeProven: false, outcomeManualOnly: false, offGoal: [], offGoalRatio: 0, offGoalCategories: [], phases: [], phaseRatio: 1, danglingPhaseRefs: [], handoffs: { total: 0, asserted: 0, ratio: 1 }, sit: [] };
|
|
200
208
|
const gateCatalog = (flowQ.hasContract && !declaredPageType(viewpointText, catalog))
|
|
201
209
|
? { ...catalog, page_types: {} } as Catalog
|
|
202
210
|
: catalog;
|
|
@@ -230,7 +238,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
230
238
|
// #1 taxonomy-match: when the project defines a viewpoint taxonomy, scenarios must use it.
|
|
231
239
|
const taxonomyMismatch = viewpoints.length > 0 && trace.withVpCode > 0 && trace.mappedRatio < 0.6;
|
|
232
240
|
// #2 downstream-scope + #4 manual-oracle
|
|
233
|
-
const downstream = downstreamScope(readText(specPath), scenarios);
|
|
241
|
+
const downstream = downstreamScope(readText(specPath), scenarios, featureText);
|
|
234
242
|
const manualOracleResult = manualOracle(featureText);
|
|
235
243
|
const autoManual = automatableManual(scenarios); // TQ-2 — @manual that is really automatable
|
|
236
244
|
const ledger = viewpointLedger(viewpointPath, scenarios, featureText);
|
|
@@ -331,7 +339,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
331
339
|
// this KIND of unit". Only the first is a reason to cap the score: a contract flow can never
|
|
332
340
|
// supply balance evidence, so capping for it held every flow at 8.9 forever and printed
|
|
333
341
|
// "weakest: flowCoverage 100% — fix it to lift the number", which is not fixable advice (#595).
|
|
334
|
-
const axisDefs: Array<{ key: string; value: number; weight: number; applicable: boolean; critical: boolean; byDesign?: boolean }> = [
|
|
342
|
+
const axisDefs: Array<{ key: string; value: number; weight: number; applicable: boolean; critical: boolean; byDesign?: boolean; withheld?: boolean }> = [
|
|
335
343
|
flowScored
|
|
336
344
|
// A declared flow claimed only by @manual scenarios is ACCOUNTED FOR but nothing runs, so
|
|
337
345
|
// it cannot count the same as an automated one — `covered` was collapsing three very
|
|
@@ -340,10 +348,10 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
340
348
|
? { key: 'flowCoverage', value: inventory ? Math.min((inventory.ratio + inventory.automatedRatio) / 2, flowQ.phaseRatio) : flowQ.phaseRatio, weight: 0.22, applicable: true, critical: true }
|
|
341
349
|
: { key: 'coverage', value: coverage, weight: 0.22, applicable: !!gate.pageType && gate.themesTotal > 0, critical: true },
|
|
342
350
|
{ key: 'specFR', value: specRatio, weight: 0.15, applicable: spec.hasSpec && spec.frTotal > 0, critical: true },
|
|
343
|
-
{ key: 'atomicLedger', value: ledger.ratio, weight: 0.13, applicable: ledger.hasViewpoint && ledger.total > 0 && !viewpointMoved, critical: true },
|
|
351
|
+
{ key: 'atomicLedger', value: ledger.ratio, weight: 0.13, applicable: ledger.hasViewpoint && ledger.total > 0 && !viewpointMoved, critical: true, withheld: viewpointMoved && ledger.hasViewpoint && ledger.total > 0 },
|
|
344
352
|
{ key: 'businessDepth', value: businessDepth, weight: 0.20, applicable: true, critical: true },
|
|
345
353
|
{ key: 'claimProof', value: claim.ratio, weight: 0.15, applicable: claim.withClaims > 0, critical: true },
|
|
346
|
-
{ key: 'traceability', value: traceScore, weight: 0.09, applicable: viewpoints.length > 0 && !viewpointMoved, critical: false },
|
|
354
|
+
{ key: 'traceability', value: traceScore, weight: 0.09, applicable: viewpoints.length > 0 && !viewpointMoved, critical: false, withheld: viewpointMoved && viewpoints.length > 0 },
|
|
347
355
|
// A use-case decomposition is EF-heavy by construction (1 basic flow, N alternates, N
|
|
348
356
|
// exceptions) — that is the shape of a well-decomposed use case, not neglected business
|
|
349
357
|
// core. `flowCoverage` already measures whether the journey's phases are covered, so
|
|
@@ -394,10 +402,24 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
394
402
|
const overall = seniorBandedOverall(rawOverall, { flowStateful: flowDepth.stateful, flowRatio: flowDepth.ratio, oracleWeak: oracle.weak.length, isolationRisk: isoRisk });
|
|
395
403
|
|
|
396
404
|
const findings: string[] = [];
|
|
405
|
+
// A baseline that could not be compared was re-recorded rather than reported as an edit.
|
|
406
|
+
// Migrating in silence would be the same sin one level down, so it is stated.
|
|
407
|
+
if (viewpointBaseline.reparsed) {
|
|
408
|
+
findings.push(`VIEWPOINT-BASELINE-REPARSED: the recorded baseline for this unit was written by an earlier version whose viewpoint parser had a different granularity, so its ids cannot be compared with today's. \`test-viewpoint.md\` itself is unchanged — the file is now fingerprinted by its own bytes, which no parser upgrade can move, and the baseline has been re-recorded against it. \`atomicLedger\` and \`traceability\` count normally. If you DID edit the viewpoint since ${viewpointBaseline.recordedAt?.slice(0, 10) ?? 'it was accepted'}, review that diff yourself — this one migration could not tell an edit from the parser change.`);
|
|
409
|
+
}
|
|
397
410
|
if (capped) {
|
|
398
411
|
const why = weakestCritical && weakestCritical.value < 0.7
|
|
399
412
|
? `"${weakestCritical.key}" is ${(weakestCritical.value * 100).toFixed(0)}% — a weak axis is a hole, not something the other axes can average away`
|
|
400
|
-
:
|
|
413
|
+
: (() => {
|
|
414
|
+
const held = axisDefs.filter((a) => a.withheld).map((a) => a.key);
|
|
415
|
+
const absent = missingEvidence.filter((k) => !held.includes(k.replace(/ .*$/, '')));
|
|
416
|
+
const parts = [];
|
|
417
|
+
if (absent.length) parts.push(`supplies no evidence for [${absent.join(', ')}]`);
|
|
418
|
+
// An axis WAS measured and is being held — saying "no evidence" here sends the author
|
|
419
|
+
// to write a declaration that already exists, when the actual next move is to review a diff.
|
|
420
|
+
if (held.length) parts.push(`has [${held.join(', ')}] measured but WITHHELD until the changed viewpoint baseline is reviewed and accepted`);
|
|
421
|
+
return `this unit ${parts.join(', and ')}, and a top mark has to rest on complete evidence`;
|
|
422
|
+
})();
|
|
401
423
|
findings.push(`SCORE-CAPPED: overall held at ${cap.toFixed(1)} because ${why}.`);
|
|
402
424
|
}
|
|
403
425
|
if (viewpointBaseline.status === 'changed') {
|
|
@@ -460,7 +482,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
460
482
|
}
|
|
461
483
|
}
|
|
462
484
|
// #595 — a viewpoint item naming a browser-level gesture, answered with an in-app control.
|
|
463
|
-
for (const g of browserGestureSubstitutions(viewpointPath, scenarios).slice(0, 4)) {
|
|
485
|
+
for (const g of browserGestureSubstitutions(viewpointPath, scenarios, platform).slice(0, 4)) {
|
|
464
486
|
findings.push(`VIEWPOINT-GESTURE-SUBSTITUTED: the viewpoint asks for "${g.gesture}" ("${g.item.slice(0, 80)}…") but the suite exercises an in-app control instead. They are different mechanisms — an app button runs the screen's own re-hydrate logic, browser history does not, and a defect that only shows on history navigation lives in exactly that gap. The step vocabulary has no reload/back step (#593), so defer it with \`@manual\` naming the gesture rather than substituting silently.`);
|
|
465
487
|
}
|
|
466
488
|
// #595 — a flow whose requirement list is a hand restatement of the screen specs it
|
|
@@ -622,6 +644,9 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
622
644
|
if (taxonomyMismatch) {
|
|
623
645
|
findings.push(`VP-TAXONOMY-MISMATCH: only ${(trace.mappedRatio * 100).toFixed(0)}% of scenarios use the viewpoint IDs declared in test-viewpoint.md — scenarios invented a generic VP-<CAT> scheme. Re-tag to the project's viewpoint IDs so the coverage matrix is accurate.`);
|
|
624
646
|
}
|
|
647
|
+
for (const a of downstream.absent.slice(0, 5)) {
|
|
648
|
+
findings.push(`NAV-TARGET-ABSENT: the spec declares "${a.route}" as a navigation target and NO scenario mentions it — not even a page assertion. Testing the render conditions of the region that links there does not test the link: a click-through case is what proves the destination. Add one, or record why this flow does not own it.`);
|
|
649
|
+
}
|
|
625
650
|
for (const d of downstream.underCovered) {
|
|
626
651
|
findings.push(`DOWNSTREAM-SCOPE-MISSING: "${d.route}" is a navigation target but is covered only by a page-nav assertion — cover its content/guards, or scaffold it (\`sungen add --screen ${d.slug}\`).`);
|
|
627
652
|
}
|
|
@@ -643,7 +668,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
643
668
|
if (intent.capabilitySuggestions) {
|
|
644
669
|
// The catalog unit id (`flows/<f>`, `api/<a>`) carries the unit KIND, which gates which
|
|
645
670
|
// drivers can serve it at all.
|
|
646
|
-
const plan = buildPlan(screenDir, screenName, catalogScreenName);
|
|
671
|
+
const plan = buildPlan(screenDir, screenName, catalogScreenName, platform);
|
|
647
672
|
if (plan.recommendations.length) {
|
|
648
673
|
const recs = plan.recommendations.map((r) => `\`sungen capability add ${r.driver}\` (automates ${r.count})`).join(' · ');
|
|
649
674
|
findings.push(`CAPABILITY-SUGGESTION: ${plan.capabilityManual} @manual scenario(s) are capability-manual (a driver could automate them) — ${recs}. Recommend-only: nothing is installed automatically; the ${plan.judgmentManual} judgment-manual (M6/M8/M9) correctly stay manual.`);
|
|
@@ -651,8 +676,8 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
651
676
|
// Named, not dropped: without this the scenarios read as "nothing could help", and the
|
|
652
677
|
// operator's only clue was an install that dead-ends (#597).
|
|
653
678
|
if (plan.unavailable.length) {
|
|
654
|
-
const un = plan.unavailable.map((r) => `${r.driver} (would automate ${r.count})`).join(' · ');
|
|
655
|
-
findings.push(`CAPABILITY-UNAVAILABLE: ${un}
|
|
679
|
+
const un = plan.unavailable.map((r) => `${r.driver} (would automate ${r.count}) — ${r.why}`).join(' · ');
|
|
680
|
+
findings.push(`CAPABILITY-UNAVAILABLE: ${un}. Do NOT install: the scenarios stay @manual until the driver gains that support, and installing it buys an install, a dead end and a manual rollback. ${plan.unavailable.some((r) => /no .* support/.test(r.why)) ? 'On this platform, verify the same behaviour through `@api`/`@query` against the real dependency instead.' : 'Automating the same behaviour on the owning SCREEN unit is the available route today.'}`);
|
|
656
681
|
}
|
|
657
682
|
}
|
|
658
683
|
// TQ-11b — automation-ready (pending capability): @requires:<cap> scenarios whose cap isn't enabled.
|
|
@@ -670,6 +695,46 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
670
695
|
if (depth.deferredBusinessCritical > 0 && depth.deferredBusinessCritical >= depth.businessCriticalTotal) {
|
|
671
696
|
findings.push(`DEPTH-DEFERRED: businessDepth ${businessDepth.toFixed(2)} is computed over only ${depth.businessCriticalTotal} on-screen scenario(s); ${depth.deferredBusinessCritical} business-critical scenario(s) are deferred to @manual (excluded from the ratio). Automate them in a flow and verify with \`sungen flow-check\` — this ratio is NOT "all business depth covered".`);
|
|
672
697
|
}
|
|
698
|
+
// #630 — the branches the spec ENUMERATES must each have a scenario. Narrative guidance for
|
|
699
|
+
// this already existed and was missed three times in three flows, so the check is an enforced
|
|
700
|
+
// enumeration rather than another sentence.
|
|
701
|
+
{
|
|
702
|
+
const branches = specBranches(readText(specPath) ?? '');
|
|
703
|
+
for (const b of unenumeratedBranches(branches, featureText).slice(0, 6)) {
|
|
704
|
+
findings.push(`SPEC-BRANCH-UNCOVERED: the spec enumerates "${b.label}" (${b.source}) and no scenario cites it. A mutually-exclusive branch is only covered when its OWN arm is verified — covering a sibling arm proves nothing about this one. Write it, or record the exclusion in the feature so the omission is a decision.`);
|
|
705
|
+
}
|
|
706
|
+
for (const c of sameOracleClusters(scenarios).slice(0, 4)) {
|
|
707
|
+
findings.push(`BRANCH-OVERCLAIMED: ${c.scenarios.length} scenarios assert an identical oracle (${c.scenarios.slice(0, 3).join(' | ')}${c.scenarios.length > 3 ? ' …' : ''}) — "${c.oracle}". They verify one branch several times while the coverage sheet counts them as ${c.scenarios.length}. Give each the assertion that distinguishes ITS branch, or merge them.`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
// #651 F1-7 — the fixed risk catalog, reconciled. SPEC-BRANCH-UNCOVERED enforces the branches a
|
|
711
|
+
// spec DECLARES; this is the half a spec is often silent about, which a reviewer catches and a
|
|
712
|
+
// generator does not. Consideration counts from anywhere the author reasons — a declared flow's
|
|
713
|
+
// branch point or reason, a scenario, the viewpoint — so "thought about, does not apply" is a
|
|
714
|
+
// one-line answer rather than a scenario nobody needs.
|
|
715
|
+
if (flowQ.hasContract) {
|
|
716
|
+
const unconsidered = unconsideredRisks(flowQ.contract!, scenarios, viewpointText);
|
|
717
|
+
if (unconsidered.length > 0) {
|
|
718
|
+
const list = unconsidered.map((r) => `${r.label} (${r.why})`).join(' · ');
|
|
719
|
+
findings.push(`RISK-FAMILY-UNCONSIDERED: ${unconsidered.length} of ${MULTI_SCREEN_RISK_COUNT} multi-screen risk families are not mentioned anywhere in this flow's contract, viewpoint or scenarios — ${list}. Each is a question, not a demand: if it does not apply here, record that in the \`flows:\` inventory as \`out-of-scope\` with the reason and it stops being asked. A flow is "enough" when every family has an answer, and a silent family is the one nobody weighed.`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
// #630 — a deny-side security case with no allow-side counterpart cannot tell a working guard
|
|
723
|
+
// from a feature that is broken for everyone.
|
|
724
|
+
for (const g of permissionPairGaps(scenarios).slice(0, 4)) {
|
|
725
|
+
findings.push(`PERMISSION-PAIR-MISSING: "${g.scenario}" proves the content is hidden when permission is denied, but no scenario proves it is SHOWN when permission is granted. If the code hid it unconditionally this would still pass — the pair is the test, the way min-1/min/max/max+1 is the test for a boundary. Add the allow-side case${g.subject.length ? ` for [${g.subject.join(', ')}]` : ''}.`);
|
|
726
|
+
}
|
|
727
|
+
// #630 — a scenario blocked on unseeded data is a DATA task, not unfinished test design.
|
|
728
|
+
{
|
|
729
|
+
const tdDir = path.join(screenDir, 'test-data');
|
|
730
|
+
let tdText = '';
|
|
731
|
+
try {
|
|
732
|
+
for (const f of fs.readdirSync(tdDir).filter((x) => x.endsWith('.yaml'))) tdText += `${readText(path.join(tdDir, f)) ?? ''}\n`;
|
|
733
|
+
} catch { /* no test-data yet */ }
|
|
734
|
+
for (const a of awaitingSeedData(tdText, scenarios).slice(0, 4)) {
|
|
735
|
+
findings.push(`AWAITING-SEED-DATA: \`${a.key}\` is still a placeholder and ${a.scenarios.length} scenario(s) reference it (${a.scenarios.slice(0, 2).join(' | ')}${a.scenarios.length > 2 ? ' …' : ''}). These are Pending for a DATA reason, not an unresolved design — seed the value (or move it to \`<unit>.<env>.yaml\`) and they become runnable as written.`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
673
738
|
// #622 — a claim that names a count, proved by fewer assertions than it names. The count sits
|
|
674
739
|
// in the viewpoint CLAIM, so both the title-level and the claim-level shape are checked.
|
|
675
740
|
for (const g of ledger.partial.slice(0, 5)) {
|
|
@@ -791,7 +856,12 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
791
856
|
// flow) that carried no weight at all.
|
|
792
857
|
axes: axisDefs.map((a) => ({ ...a, weight: a.applicable ? a.weight / weightSum : 0 })),
|
|
793
858
|
formula: `overall = (${scored.map((a) => `${(a.weight / weightSum).toFixed(2)}*${a.key}`).join(' + ')}) * 10`
|
|
794
|
-
+ (
|
|
859
|
+
+ (() => {
|
|
860
|
+
const held = new Set(axisDefs.filter((a) => a.withheld).map((a) => a.key));
|
|
861
|
+
const absent = missingEvidence.filter((k) => !held.has(k.replace(/ .*$/, '')));
|
|
862
|
+
return (absent.length ? ` — n/a (no evidence): ${absent.join(', ')}` : '')
|
|
863
|
+
+ (held.size ? ` — measured but held: ${[...held].join(', ')}` : '');
|
|
864
|
+
})()
|
|
795
865
|
+ (notApplicableByDesign.length ? ` — n/a for this unit kind: ${notApplicableByDesign.join(', ')}` : ''),
|
|
796
866
|
},
|
|
797
867
|
gateStatus,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
|
-
import { loadDriverCatalog, unitKindOf, driverServesUnit } from './capability';
|
|
11
|
+
import { loadDriverCatalog, driverMeta, unitKindOf, driverServesUnit, driverServesPlatform } from './capability';
|
|
12
12
|
import { readTextFile } from './read-text';
|
|
13
13
|
import { featureFilesFor } from './unit-paths';
|
|
14
14
|
|
|
@@ -197,11 +197,11 @@ export interface CapabilityPlan {
|
|
|
197
197
|
* dropped: the scenarios stay manual for a reason the operator needs to know, and a silent
|
|
198
198
|
* omission would read as "nothing could help here" (#597).
|
|
199
199
|
*/
|
|
200
|
-
unavailable: { driver: string; reason: string; count: number; unitKind: string }[];
|
|
200
|
+
unavailable: { driver: string; reason: string; count: number; unitKind: string; why: string }[];
|
|
201
201
|
keep: { code: string; count: number }[];
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
export function buildPlan(screenDir: string, screenName: string, unitId = screenName): CapabilityPlan {
|
|
204
|
+
export function buildPlan(screenDir: string, screenName: string, unitId = screenName, platform?: string): CapabilityPlan {
|
|
205
205
|
const scenarios = featureFilesFor(screenDir, screenName).flatMap(parseScenarios);
|
|
206
206
|
const catalog = loadDriverCatalog();
|
|
207
207
|
// Recommending a driver that cannot serve this unit KIND costs an install, a dead end and a
|
|
@@ -241,14 +241,19 @@ export function buildPlan(screenDir: string, screenName: string, unitId = screen
|
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
|
|
244
|
+
// Two axes, same guarantee: a driver must be able to serve this unit KIND and this PLATFORM.
|
|
245
|
+
const serves = (d: string): boolean => driverServesUnit(d, unitKind) && driverServesPlatform(d, platform);
|
|
246
|
+
const servable = [...recByDriver.entries()].filter(([d]) => serves(d));
|
|
245
247
|
const unavailable = [...recByDriver.entries()]
|
|
246
|
-
.filter(([d]) => !
|
|
248
|
+
.filter(([d]) => !serves(d))
|
|
247
249
|
.map(([driver, v]) => ({
|
|
248
250
|
driver,
|
|
249
251
|
reason: v.reason,
|
|
250
252
|
count: v.scenarios.length,
|
|
251
253
|
unitKind,
|
|
254
|
+
why: !driverServesUnit(driver, unitKind)
|
|
255
|
+
? `does not support a ${unitKind} unit`
|
|
256
|
+
: `has no ${platform} support (its step patterns are ${(driverMeta(driver)?.platforms ?? []).join('/')}-only)`,
|
|
252
257
|
}))
|
|
253
258
|
.sort((a, b) => b.count - a.count);
|
|
254
259
|
const recommendations = servable
|
|
@@ -81,6 +81,15 @@ export interface DriverMeta {
|
|
|
81
81
|
* driver on a flow: install, dead end, manual rollback (#597).
|
|
82
82
|
*/
|
|
83
83
|
units?: Array<'screen' | 'flow' | 'api'>;
|
|
84
|
+
/**
|
|
85
|
+
* Platforms this driver can serve. Absent = all.
|
|
86
|
+
*
|
|
87
|
+
* The same guarantee as `units`, on the other axis. Step patterns already gate themselves
|
|
88
|
+
* (`platforms: ['web']` → SG-W020 at compile time), but the PLANNER did not know, so a mobile
|
|
89
|
+
* project was still told to install a web-only driver — install, dead end, manual rollback,
|
|
90
|
+
* exactly as in #597 (#626).
|
|
91
|
+
*/
|
|
92
|
+
platforms?: string[];
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
/** The unit kind of a catalog unit id: `flows/<f>` → flow, `api/<a>` → api, else screen. */
|
|
@@ -97,6 +106,13 @@ export function driverServesUnit(driverId: string, kind: 'screen' | 'flow' | 'ap
|
|
|
97
106
|
return !units || units.includes(kind);
|
|
98
107
|
}
|
|
99
108
|
|
|
109
|
+
/** Can this driver serve that platform? No declared platform on either side → assume yes. */
|
|
110
|
+
export function driverServesPlatform(driverId: string, platform?: string): boolean {
|
|
111
|
+
const platforms = driverMeta(driverId)?.platforms;
|
|
112
|
+
if (!platforms || !platform) return true;
|
|
113
|
+
return platforms.includes(platform.toLowerCase());
|
|
114
|
+
}
|
|
115
|
+
|
|
100
116
|
export function capabilitiesPath(cwd: string): string {
|
|
101
117
|
return path.join(cwd, 'qa', 'capabilities.yaml');
|
|
102
118
|
}
|
|
@@ -66,6 +66,11 @@ drivers:
|
|
|
66
66
|
# (mockCatalogPath is hardcoded to that shape), so it cannot serve a flow. Recommending
|
|
67
67
|
# it for one sent an operator through an install, a dead end and a manual rollback (#597).
|
|
68
68
|
units: [screen]
|
|
69
|
+
# `platforms:` is the same guarantee on the other axis. Absent → every platform.
|
|
70
|
+
# `mock.install` is Playwright route interception (page.route) and its step patterns already
|
|
71
|
+
# declare `platforms: ['web']`, so on mobile they compile to SG-W020. The PLANNER did not know
|
|
72
|
+
# that, so a mobile project was still told to install it — the #597 dead end, one axis over.
|
|
73
|
+
platforms: [web]
|
|
69
74
|
mail-file:
|
|
70
75
|
kind: capability
|
|
71
76
|
package: "@sungen/driver-mail-file"
|