@sun-asterisk/sungen 3.2.32 → 3.2.34
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/cli/commands/audit.d.ts +5 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +26 -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 +35 -17
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/changed.d.ts.map +1 -1
- package/dist/cli/commands/changed.js +2 -3
- package/dist/cli/commands/changed.js.map +1 -1
- package/dist/cli/commands/dashboard.d.ts.map +1 -1
- package/dist/cli/commands/dashboard.js +2 -28
- package/dist/cli/commands/dashboard.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +79 -53
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +14 -7
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/results-merge.d.ts +9 -0
- package/dist/cli/commands/results-merge.d.ts.map +1 -1
- package/dist/cli/commands/results-merge.js +9 -12
- package/dist/cli/commands/results-merge.js.map +1 -1
- package/dist/cli/commands/results-sanitize.d.ts.map +1 -1
- package/dist/cli/commands/results-sanitize.js +2 -13
- package/dist/cli/commands/results-sanitize.js.map +1 -1
- package/dist/cli/commands/select.d.ts +12 -0
- package/dist/cli/commands/select.d.ts.map +1 -0
- package/dist/cli/commands/select.js +122 -0
- package/dist/cli/commands/select.js.map +1 -0
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +3 -0
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/index.js +7 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
- package/dist/dashboard/snapshot-builder.js +6 -10
- package/dist/dashboard/snapshot-builder.js.map +1 -1
- package/dist/dashboard/templates/index.html +1 -1
- package/dist/dashboard/types.d.ts +1 -0
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +11 -10
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/feature-parser.d.ts +21 -0
- package/dist/exporters/feature-parser.d.ts.map +1 -1
- package/dist/exporters/feature-parser.js +32 -1
- package/dist/exporters/feature-parser.js.map +1 -1
- package/dist/exporters/json-exporter.d.ts.map +1 -1
- package/dist/exporters/json-exporter.js +7 -0
- package/dist/exporters/json-exporter.js.map +1 -1
- package/dist/exporters/locale-variants.d.ts +7 -1
- package/dist/exporters/locale-variants.d.ts.map +1 -1
- package/dist/exporters/locale-variants.js +23 -16
- package/dist/exporters/locale-variants.js.map +1 -1
- package/dist/exporters/manual-hybrid-oracle.d.ts +54 -0
- package/dist/exporters/manual-hybrid-oracle.d.ts.map +1 -0
- package/dist/exporters/manual-hybrid-oracle.js +114 -0
- package/dist/exporters/manual-hybrid-oracle.js.map +1 -0
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +19 -9
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/carry-manual-results.d.ts +70 -0
- package/dist/exporters/matrix/carry-manual-results.d.ts.map +1 -0
- package/dist/exporters/matrix/carry-manual-results.js +232 -0
- package/dist/exporters/matrix/carry-manual-results.js.map +1 -0
- package/dist/exporters/matrix/export.d.ts +5 -0
- package/dist/exporters/matrix/export.d.ts.map +1 -1
- package/dist/exporters/matrix/export.js +8 -1
- package/dist/exporters/matrix/export.js.map +1 -1
- package/dist/exporters/matrix/gates.js +6 -0
- package/dist/exporters/matrix/gates.js.map +1 -1
- package/dist/exporters/matrix/render-testcase-sheet.d.ts.map +1 -1
- package/dist/exporters/matrix/render-testcase-sheet.js +10 -4
- package/dist/exporters/matrix/render-testcase-sheet.js.map +1 -1
- package/dist/exporters/matrix/types.d.ts +8 -1
- package/dist/exporters/matrix/types.d.ts.map +1 -1
- package/dist/exporters/matrix/types.js.map +1 -1
- package/dist/exporters/matrix/wording.d.ts +8 -1
- package/dist/exporters/matrix/wording.d.ts.map +1 -1
- package/dist/exporters/matrix/wording.js +37 -2
- package/dist/exporters/matrix/wording.js.map +1 -1
- package/dist/exporters/run-environment.d.ts +32 -0
- package/dist/exporters/run-environment.d.ts.map +1 -0
- package/dist/exporters/run-environment.js +125 -0
- package/dist/exporters/run-environment.js.map +1 -0
- package/dist/exporters/scenario-merger.d.ts +18 -0
- package/dist/exporters/scenario-merger.d.ts.map +1 -1
- package/dist/exporters/scenario-merger.js +47 -3
- package/dist/exporters/scenario-merger.js.map +1 -1
- package/dist/exporters/test-data-resolver.d.ts +0 -4
- package/dist/exporters/test-data-resolver.d.ts.map +1 -1
- package/dist/exporters/test-data-resolver.js +0 -7
- package/dist/exporters/test-data-resolver.js.map +1 -1
- package/dist/exporters/xlsx-exporter.d.ts +2 -0
- package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
- package/dist/exporters/xlsx-exporter.js +7 -2
- package/dist/exporters/xlsx-exporter.js.map +1 -1
- package/dist/generators/gherkin-parser/index.d.ts +11 -4
- package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
- package/dist/generators/gherkin-parser/index.js +49 -19
- package/dist/generators/gherkin-parser/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +4 -0
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
- package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +68 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-select-action.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/pick-option.hbs +13 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +5 -1
- package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +16 -0
- package/dist/generators/test-generator/code-generator.d.ts +2 -1
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +90 -5
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +36 -5
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +35 -4
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts +12 -2
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +15 -9
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/patterns/types.d.ts +15 -0
- package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/with-parent-scope.d.ts +5 -0
- package/dist/generators/test-generator/patterns/with-parent-scope.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/with-parent-scope.js +6 -0
- package/dist/generators/test-generator/patterns/with-parent-scope.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +46 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +84 -9
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.d.ts +25 -0
- package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.js +39 -8
- package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-entry-fields.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-entry-fields.js +1 -0
- package/dist/generators/test-generator/utils/selector-entry-fields.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts +37 -4
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +212 -79
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/harness/audit.d.ts +3 -0
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +29 -12
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability.d.ts +7 -1
- package/dist/harness/capability.d.ts.map +1 -1
- package/dist/harness/capability.js +15 -12
- package/dist/harness/capability.js.map +1 -1
- package/dist/harness/catalog/universal-viewpoints.yaml +5 -5
- package/dist/harness/config-file.d.ts +17 -0
- package/dist/harness/config-file.d.ts.map +1 -0
- package/dist/harness/config-file.js +100 -0
- package/dist/harness/config-file.js.map +1 -0
- package/dist/harness/data-driven-lint.d.ts.map +1 -1
- package/dist/harness/data-driven-lint.js +69 -27
- package/dist/harness/data-driven-lint.js.map +1 -1
- package/dist/harness/depth-lint.d.ts.map +1 -1
- package/dist/harness/depth-lint.js +2 -1
- package/dist/harness/depth-lint.js.map +1 -1
- package/dist/harness/flow-check.d.ts.map +1 -1
- package/dist/harness/flow-check.js +6 -2
- package/dist/harness/flow-check.js.map +1 -1
- package/dist/harness/keyword-match.d.ts +59 -0
- package/dist/harness/keyword-match.d.ts.map +1 -0
- package/dist/harness/keyword-match.js +86 -0
- package/dist/harness/keyword-match.js.map +1 -0
- package/dist/harness/next-step.d.ts.map +1 -1
- package/dist/harness/next-step.js +1 -0
- package/dist/harness/next-step.js.map +1 -1
- package/dist/harness/parse.d.ts +24 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +117 -13
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/perf.d.ts +7 -1
- package/dist/harness/perf.d.ts.map +1 -1
- package/dist/harness/perf.js +13 -10
- package/dist/harness/perf.js.map +1 -1
- package/dist/harness/quality-gates.d.ts +9 -2
- package/dist/harness/quality-gates.d.ts.map +1 -1
- package/dist/harness/quality-gates.js +22 -8
- package/dist/harness/quality-gates.js.map +1 -1
- package/dist/harness/query-catalog.js +0 -0
- package/dist/harness/query-catalog.js.map +1 -1
- package/dist/harness/read-text.d.ts +21 -0
- package/dist/harness/read-text.d.ts.map +1 -1
- package/dist/harness/read-text.js +32 -1
- package/dist/harness/read-text.js.map +1 -1
- package/dist/harness/secret-scan.d.ts +27 -1
- package/dist/harness/secret-scan.d.ts.map +1 -1
- package/dist/harness/secret-scan.js +57 -4
- package/dist/harness/secret-scan.js.map +1 -1
- package/dist/harness/select-by-ids.d.ts +31 -0
- package/dist/harness/select-by-ids.d.ts.map +1 -0
- package/dist/harness/select-by-ids.js +106 -0
- package/dist/harness/select-by-ids.js.map +1 -0
- package/dist/harness/sensors.d.ts +20 -1
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +102 -27
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/spec-branches.d.ts +0 -18
- package/dist/harness/spec-branches.d.ts.map +1 -1
- package/dist/harness/spec-branches.js +24 -3
- package/dist/harness/spec-branches.js.map +1 -1
- package/dist/harness/spec-coverage.d.ts.map +1 -1
- package/dist/harness/spec-coverage.js +14 -4
- package/dist/harness/spec-coverage.js.map +1 -1
- package/dist/harness/ui-inventory.d.ts +40 -0
- package/dist/harness/ui-inventory.d.ts.map +1 -0
- package/dist/harness/ui-inventory.js +191 -0
- package/dist/harness/ui-inventory.js.map +1 -0
- package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
- package/dist/harness/viewpoint-ledger.js +66 -10
- package/dist/harness/viewpoint-ledger.js.map +1 -1
- package/dist/harness/write-atomic.d.ts +2 -0
- package/dist/harness/write-atomic.d.ts.map +1 -0
- package/dist/harness/write-atomic.js +62 -0
- package/dist/harness/write-atomic.js.map +1 -0
- package/dist/orchestrator/ai-skills/writer.d.ts.map +1 -1
- package/dist/orchestrator/ai-skills/writer.js +15 -7
- package/dist/orchestrator/ai-skills/writer.js.map +1 -1
- package/dist/orchestrator/assets-drift.d.ts.map +1 -1
- package/dist/orchestrator/assets-drift.js +11 -6
- package/dist/orchestrator/assets-drift.js.map +1 -1
- package/dist/orchestrator/project-deps-sync.d.ts.map +1 -1
- package/dist/orchestrator/project-deps-sync.js +22 -1
- package/dist/orchestrator/project-deps-sync.js.map +1 -1
- package/dist/orchestrator/project-initializer.d.ts +8 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +49 -7
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/screen-manager.d.ts.map +1 -1
- package/dist/orchestrator/screen-manager.js +3 -11
- package/dist/orchestrator/screen-manager.js.map +1 -1
- package/dist/orchestrator/security-overrides-catalog.d.ts +39 -0
- package/dist/orchestrator/security-overrides-catalog.d.ts.map +1 -0
- package/dist/orchestrator/security-overrides-catalog.js +63 -0
- package/dist/orchestrator/security-overrides-catalog.js.map +1 -0
- package/dist/orchestrator/security-overrides.d.ts +51 -0
- package/dist/orchestrator/security-overrides.d.ts.map +1 -0
- package/dist/orchestrator/security-overrides.js +165 -0
- package/dist/orchestrator/security-overrides.js.map +1 -0
- package/dist/orchestrator/templates/ai-src/agents/discovery.md +11 -3
- package/dist/orchestrator/templates/ai-src/agents/reviewer.md +8 -0
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +2 -2
- package/dist/orchestrator/templates/ai-src/commands/review.md +2 -2
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +70 -13
- package/dist/orchestrator/templates/ai-src/skills/sungen-capture/mode-local.md +2 -2
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +23 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +26 -8
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +58 -14
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +41 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +7 -0
- package/dist/orchestrator/templates/playwright.config.ts +4 -2
- package/dist/orchestrator/templates/specs-locale-fixture.ts +21 -9
- package/dist/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
- package/dist/orchestrator/templates/specs-url-assert.d.ts +17 -1
- package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-url-assert.js +38 -4
- package/dist/orchestrator/templates/specs-url-assert.js.map +1 -1
- package/dist/orchestrator/templates/specs-url-assert.ts +47 -4
- package/dist/orchestrator/templates/wdio.conf.ts +24 -1
- package/dist/orchestrator/trace-mode-install.d.ts +14 -0
- package/dist/orchestrator/trace-mode-install.d.ts.map +1 -0
- package/dist/orchestrator/trace-mode-install.js +132 -0
- package/dist/orchestrator/trace-mode-install.js.map +1 -0
- package/dist/orchestrator/viewpoint-pattern-catalog.d.ts +20 -0
- package/dist/orchestrator/viewpoint-pattern-catalog.d.ts.map +1 -0
- package/dist/orchestrator/viewpoint-pattern-catalog.js +45 -0
- package/dist/orchestrator/viewpoint-pattern-catalog.js.map +1 -0
- package/package.json +3 -3
- package/src/cli/commands/audit.ts +24 -5
- package/src/cli/commands/capability.ts +51 -18
- package/src/cli/commands/changed.ts +2 -4
- package/src/cli/commands/dashboard.ts +1 -26
- package/src/cli/commands/delivery.ts +75 -48
- package/src/cli/commands/generate.ts +15 -7
- package/src/cli/commands/results-merge.ts +9 -9
- package/src/cli/commands/results-sanitize.ts +2 -9
- package/src/cli/commands/select.ts +88 -0
- package/src/cli/commands/update.ts +3 -0
- package/src/cli/index.ts +7 -2
- package/src/dashboard/snapshot-builder.ts +6 -8
- package/src/dashboard/templates/index.html +1 -1
- package/src/dashboard/types.ts +1 -0
- package/src/exporters/csv-exporter.ts +7 -10
- package/src/exporters/feature-parser.ts +32 -2
- package/src/exporters/json-exporter.ts +6 -1
- package/src/exporters/locale-variants.ts +23 -17
- package/src/exporters/manual-hybrid-oracle.ts +112 -0
- package/src/exporters/matrix/build.ts +20 -10
- package/src/exporters/matrix/carry-manual-results.ts +229 -0
- package/src/exporters/matrix/export.ts +14 -4
- package/src/exporters/matrix/gates.ts +6 -0
- package/src/exporters/matrix/render-testcase-sheet.ts +10 -4
- package/src/exporters/matrix/types.ts +8 -1
- package/src/exporters/matrix/wording.ts +36 -2
- package/src/exporters/run-environment.ts +89 -0
- package/src/exporters/scenario-merger.ts +49 -4
- package/src/exporters/test-data-resolver.ts +0 -6
- package/src/exporters/xlsx-exporter.ts +7 -2
- package/src/generators/gherkin-parser/index.ts +50 -18
- package/src/generators/test-generator/adapters/adapter-interface.ts +6 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
- package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +68 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-select-action.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/pick-option.hbs +13 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +5 -1
- package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +16 -0
- package/src/generators/test-generator/code-generator.ts +77 -5
- package/src/generators/test-generator/diagnostics.ts +36 -5
- package/src/generators/test-generator/patterns/index.ts +15 -8
- package/src/generators/test-generator/patterns/types.ts +15 -0
- package/src/generators/test-generator/patterns/with-parent-scope.ts +6 -0
- package/src/generators/test-generator/step-mapper.ts +90 -8
- package/src/generators/test-generator/utils/data-resolver.ts +41 -12
- package/src/generators/test-generator/utils/selector-entry-fields.ts +1 -0
- package/src/generators/test-generator/utils/selector-resolver.ts +238 -82
- package/src/harness/audit.ts +37 -16
- package/src/harness/capability.ts +23 -12
- package/src/harness/catalog/universal-viewpoints.yaml +5 -5
- package/src/harness/config-file.ts +66 -0
- package/src/harness/data-driven-lint.ts +82 -24
- package/src/harness/depth-lint.ts +2 -1
- package/src/harness/flow-check.ts +6 -2
- package/src/harness/keyword-match.ts +85 -0
- package/src/harness/next-step.ts +1 -0
- package/src/harness/parse.ts +123 -16
- package/src/harness/perf.ts +15 -7
- package/src/harness/quality-gates.ts +22 -8
- package/src/harness/query-catalog.ts +0 -0
- package/src/harness/read-text.ts +32 -1
- package/src/harness/secret-scan.ts +60 -4
- package/src/harness/select-by-ids.ts +83 -0
- package/src/harness/sensors.ts +109 -28
- package/src/harness/spec-branches.ts +6 -3
- package/src/harness/spec-coverage.ts +14 -4
- package/src/harness/ui-inventory.ts +164 -0
- package/src/harness/viewpoint-ledger.ts +59 -12
- package/src/harness/write-atomic.ts +22 -0
- package/src/orchestrator/ai-skills/writer.ts +18 -3
- package/src/orchestrator/assets-drift.ts +10 -5
- package/src/orchestrator/project-deps-sync.ts +23 -1
- package/src/orchestrator/project-initializer.ts +54 -6
- package/src/orchestrator/screen-manager.ts +3 -11
- package/src/orchestrator/security-overrides-catalog.ts +69 -0
- package/src/orchestrator/security-overrides.ts +149 -0
- package/src/orchestrator/templates/ai-src/agents/discovery.md +11 -3
- package/src/orchestrator/templates/ai-src/agents/reviewer.md +8 -0
- package/src/orchestrator/templates/ai-src/commands/create-test.md +2 -2
- package/src/orchestrator/templates/ai-src/commands/review.md +2 -2
- package/src/orchestrator/templates/ai-src/commands/run-test.md +70 -13
- package/src/orchestrator/templates/ai-src/skills/sungen-capture/mode-local.md +2 -2
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +23 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +26 -8
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +58 -14
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +41 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +7 -0
- package/src/orchestrator/templates/playwright.config.ts +4 -2
- package/src/orchestrator/templates/specs-locale-fixture.ts +21 -9
- package/src/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
- package/src/orchestrator/templates/specs-url-assert.ts +47 -4
- package/src/orchestrator/templates/wdio.conf.ts +24 -1
- package/src/orchestrator/trace-mode-install.ts +94 -0
- package/src/orchestrator/viewpoint-pattern-catalog.ts +58 -0
|
@@ -6,7 +6,7 @@ import { readYaml, readYamlIfExists } from '../../../utils/yaml-io';
|
|
|
6
6
|
|
|
7
7
|
import { SelectorType } from '../../../utils/selector-types';
|
|
8
8
|
import { DataResolver } from './data-resolver';
|
|
9
|
-
import { DiagnosticsCollector } from '../diagnostics';
|
|
9
|
+
import { DiagnosticsCollector, type DiagnosticCode } from '../diagnostics';
|
|
10
10
|
import { DEFAULT_CONTENT_NAMED_ROLES } from '../app-profile';
|
|
11
11
|
import { reportUnknownEntryFields } from './selector-entry-fields';
|
|
12
12
|
import { reportUnknownSelectorTypes } from './selector-type-validation';
|
|
@@ -33,6 +33,25 @@ interface SelectorEntry {
|
|
|
33
33
|
// that bounces the user after the URL has already matched turns the step red instead of green.
|
|
34
34
|
anchor?: SelectorEntry;
|
|
35
35
|
|
|
36
|
+
// === Page query identity (#748) ===
|
|
37
|
+
// For a `type: page` entry: `any` means the query string is NOT part of this page's identity.
|
|
38
|
+
//
|
|
39
|
+
// A bare `see [X] page` asserts that the URL carries NO query at all (#464 — a filter/search
|
|
40
|
+
// regression read as green while the query was wrong or gone). That is right for a page whose
|
|
41
|
+
// identity IS its path, and unsatisfiable for one keyed by a query — `/admins/detail?id=5`,
|
|
42
|
+
// where the id comes from the row the previous step clicked and is unknowable when the scenario
|
|
43
|
+
// is written. `with {{v}}` cannot rescue it: that asserts a LITERAL value.
|
|
44
|
+
//
|
|
45
|
+
// Declared on the ENTRY rather than added to the step grammar because it is a fact about the
|
|
46
|
+
// PAGE — every step targeting it needs the same reading — and because `wait for [X] page`
|
|
47
|
+
// already ignores the query. A step-form escape hatch would serve only the Then side and widen
|
|
48
|
+
// the very drift the shared url-assertion partial was written to end.
|
|
49
|
+
//
|
|
50
|
+
// It does NOT weaken a step that declares its own query: `with {{v}}` still asserts every
|
|
51
|
+
// declared param. This changes only what SILENCE means — from "there must be no query" to
|
|
52
|
+
// "the query is not mine to judge".
|
|
53
|
+
query?: 'any';
|
|
54
|
+
|
|
36
55
|
// === Mobile per-platform variant (issue #392) ===
|
|
37
56
|
// `android:`/`ios:` sub-selectors for one logical element whose locator differs per OS (composite
|
|
38
57
|
// content-desc, partial/dynamic text, native predicate). Each is a full SelectorEntry.
|
|
@@ -166,12 +185,114 @@ export interface ResolvedSelector {
|
|
|
166
185
|
checkedClass?: string;
|
|
167
186
|
checkedAttribute?: string;
|
|
168
187
|
anchor?: ResolvedSelector; // page entries only — the rendered-content proof behind `see [X] page` (#662)
|
|
188
|
+
query?: 'any'; // page entries only — the query is not part of this page's identity (#748)
|
|
169
189
|
}
|
|
170
190
|
|
|
171
191
|
/**
|
|
172
192
|
* SelectorResolver - Resolves selector references to direct Playwright locator code
|
|
173
193
|
* Supports natural language references with auto-generated keys
|
|
174
194
|
*/
|
|
195
|
+
/**
|
|
196
|
+
* Read an OWN property of a lookup table, or undefined.
|
|
197
|
+
*
|
|
198
|
+
* Every type table here — the alias map, the strategy map, the reverse index, and the parsed
|
|
199
|
+
* selectors.yaml itself — is a bare object literal, so it inherits Object.prototype and answers for
|
|
200
|
+
* keys it does not have. `elementType` is whatever word the author wrote after `]`, and the three
|
|
201
|
+
* readers failed three different ways on an inherited one: normalizeElementType is declared
|
|
202
|
+
* `: string` and returned a FUNCTION to be interpolated into a selector key; inferFromElementType
|
|
203
|
+
* CALLED that function with no receiver and crashed the compiler with "Cannot convert undefined or
|
|
204
|
+
* null to object"; and `ALIAS_SPELLINGS[t] ?? []` sailed past its own `??` guard, because a
|
|
205
|
+
* function is neither null nor undefined, and died on `.map`.
|
|
206
|
+
*
|
|
207
|
+
* No real feature file says `[X] valueOf`. The rule is the point: a lookup table must not answer
|
|
208
|
+
* for a key it does not have, and one helper is cheaper to keep right than four call sites.
|
|
209
|
+
*/
|
|
210
|
+
function own<T>(table: Record<string, T>, key: string): T | undefined {
|
|
211
|
+
return Object.prototype.hasOwnProperty.call(table, key) ? table[key] : undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Gherkin element-type spelling → the canonical suffix used in selectors.yaml keys.
|
|
216
|
+
*
|
|
217
|
+
* Module-scope so the table has exactly ONE reader-facing definition and the reverse index below
|
|
218
|
+
* can be DERIVED from it. A second, hand-written reverse table is the failure mode issue #728
|
|
219
|
+
* recorded: a hand-maintained mirror of something the source already states drifts, quietly, for
|
|
220
|
+
* months. Declaration order is meaningful — it decides which alias wins when a selectors.yaml
|
|
221
|
+
* declares two spellings of one canonical type (see ALIAS_SPELLINGS).
|
|
222
|
+
*/
|
|
223
|
+
const ELEMENT_TYPE_ALIASES: Record<string, string> = {
|
|
224
|
+
// Text-like aliases → "text"
|
|
225
|
+
'title': 'text',
|
|
226
|
+
'label': 'text',
|
|
227
|
+
'caption': 'text',
|
|
228
|
+
// Heading aliases → "heading" (role-based)
|
|
229
|
+
'heading': 'heading',
|
|
230
|
+
'header': 'heading',
|
|
231
|
+
'message': 'text',
|
|
232
|
+
// Table-related aliases
|
|
233
|
+
'column': 'columnheader',
|
|
234
|
+
// List-related aliases
|
|
235
|
+
'list-item': 'listitem',
|
|
236
|
+
// Image aliases
|
|
237
|
+
'logo': 'img',
|
|
238
|
+
'image': 'img',
|
|
239
|
+
'icon': 'img',
|
|
240
|
+
// Button aliases
|
|
241
|
+
'btn': 'button',
|
|
242
|
+
// Field aliases
|
|
243
|
+
'input': 'field',
|
|
244
|
+
'textbox': 'field',
|
|
245
|
+
'textarea': 'field',
|
|
246
|
+
'editor': 'field',
|
|
247
|
+
// Search alias → searchbox role
|
|
248
|
+
'search': 'searchbox',
|
|
249
|
+
// Toggle alias → switch role
|
|
250
|
+
'toggle': 'switch',
|
|
251
|
+
// Alert alias → alertdialog role.
|
|
252
|
+
// UNREACHABLE in practice, kept for the same reason it is not changed: in sungen's grammar
|
|
253
|
+
// `alert` means the BROWSER system dialog, so `see [X] alert` matches a dedicated step pattern
|
|
254
|
+
// and compiles to `page.on('dialog', …)` without ever entering the selector resolver
|
|
255
|
+
// (verified by compiling the step; target-dictionary.mdx documents it as `page.on('dialog')`).
|
|
256
|
+
// This entry only answers a step form that reaches the resolver with the type `alert`, and
|
|
257
|
+
// none does. Removing it would be a behaviour change with no caller to benefit, so it stays.
|
|
258
|
+
'alert': 'alertdialog',
|
|
259
|
+
// Modal/drawer alias → dialog role
|
|
260
|
+
'modal': 'dialog',
|
|
261
|
+
'drawer': 'dialog',
|
|
262
|
+
// Dropdown/select alias -> combobox role
|
|
263
|
+
'dropdown': 'combobox',
|
|
264
|
+
'select': 'combobox',
|
|
265
|
+
// Data aliases
|
|
266
|
+
'item': 'listitem',
|
|
267
|
+
'card': 'article',
|
|
268
|
+
'cell': 'cell',
|
|
269
|
+
'section': 'region',
|
|
270
|
+
// Feedback aliases
|
|
271
|
+
'badge': 'text',
|
|
272
|
+
'tooltip': 'text',
|
|
273
|
+
'tag': 'text',
|
|
274
|
+
// Trigger aliases
|
|
275
|
+
'menuitem': 'menuitem',
|
|
276
|
+
// System aliases
|
|
277
|
+
'progressbar': 'progressbar',
|
|
278
|
+
'spinner': 'status',
|
|
279
|
+
'breadcrumb': 'navigation',
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Canonical type → every alias spelling that normalizes to it, in ELEMENT_TYPE_ALIASES declaration
|
|
284
|
+
* order. Derived, never authored. Identity entries ('heading': 'heading') are excluded: they are
|
|
285
|
+
* already covered by the canonical candidate and would only duplicate a lookup.
|
|
286
|
+
*/
|
|
287
|
+
const ALIAS_SPELLINGS: Record<string, string[]> = (() => {
|
|
288
|
+
const out: Record<string, string[]> = {};
|
|
289
|
+
for (const [alias, canonical] of Object.entries(ELEMENT_TYPE_ALIASES)) {
|
|
290
|
+
if (alias === canonical) continue;
|
|
291
|
+
(out[canonical] ??= []).push(alias);
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
})();
|
|
295
|
+
|
|
175
296
|
export class SelectorResolver {
|
|
176
297
|
private selectorCache = new Map<string, SelectorFile | LegacySelectorFile>();
|
|
177
298
|
private selectorsDir: string;
|
|
@@ -393,69 +514,54 @@ export class SelectorResolver {
|
|
|
393
514
|
'navigation': () => ({ strategy: 'role', role: 'navigation', name: label, value: 'navigation' }),
|
|
394
515
|
};
|
|
395
516
|
|
|
396
|
-
const factory = strategyMap
|
|
517
|
+
const factory = own(strategyMap, normalized);
|
|
397
518
|
return factory ? factory() : null;
|
|
398
519
|
}
|
|
399
520
|
|
|
521
|
+
/**
|
|
522
|
+
* Does the compiler have an OPINION about this element type — is it an alias it normalizes, or a
|
|
523
|
+
* type auto-infer can serve?
|
|
524
|
+
*
|
|
525
|
+
* The distinction decides whether the last-resort sweep may run, and it exists because
|
|
526
|
+
* `step.elementType` is not always an element type. The parser takes the first word after `]`
|
|
527
|
+
* (gherkin-parser/index.ts), so `see [Quick Panel] is hidden` arrives here with the "type" `is`
|
|
528
|
+
* and `drag [Item] to [Drop Zone]` with `to`. Nine such function words appear across the shipped
|
|
529
|
+
* features. A word the compiler cannot reason about carries no type information at all, so it must
|
|
530
|
+
* not be treated as a narrowing signal — a step like that has to keep the same open fallback a
|
|
531
|
+
* bare `[Ref]` gets, or a project whose label needs suffixed keys loses every state-clause step.
|
|
532
|
+
*
|
|
533
|
+
* Derived from the two maps rather than listed, so a new alias or strategy is known here the day
|
|
534
|
+
* it is added. tests/docs/element-type-vocabulary-parity.run.ts pins the maps against the parser's
|
|
535
|
+
* own list and the published table.
|
|
536
|
+
*/
|
|
537
|
+
private static isKnownElementType(elementType: string): boolean {
|
|
538
|
+
if (own(ELEMENT_TYPE_ALIASES, elementType) !== undefined) return true;
|
|
539
|
+
return SelectorResolver.inferFromElementType(elementType, '') !== null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Every selectors.yaml key that could legitimately hold the entry for `[label] <elementType>`.
|
|
544
|
+
*
|
|
545
|
+
* Shared by the lookup and by the not-found message, so the error can never advertise a key the
|
|
546
|
+
* resolver did not actually try — the two drifting apart is how #752 stayed invisible: the throw
|
|
547
|
+
* that would have named the missing key was unreachable, and when it did fire it listed two
|
|
548
|
+
* candidates while the resolver had its own idea of which to check.
|
|
549
|
+
*/
|
|
550
|
+
private static typedKeyCandidates(key: string, elementType: string, normalizedType: string): string[] {
|
|
551
|
+
return [...new Set([
|
|
552
|
+
`${key}--${normalizedType}`,
|
|
553
|
+
`${key}--${elementType}`,
|
|
554
|
+
...(own(ALIAS_SPELLINGS, normalizedType) ?? []).map((alias) => `${key}--${alias}`),
|
|
555
|
+
])];
|
|
556
|
+
}
|
|
557
|
+
|
|
400
558
|
/**
|
|
401
559
|
* Normalize elementType aliases to canonical YAML suffix
|
|
402
560
|
* Gherkin writers may use: title, label, heading, text → all map to "text"
|
|
403
561
|
* Similarly: btn → button, link → link, input/field → field
|
|
404
562
|
*/
|
|
405
563
|
private static normalizeElementType(elementType: string): string {
|
|
406
|
-
|
|
407
|
-
// Text-like aliases → "text"
|
|
408
|
-
'title': 'text',
|
|
409
|
-
'label': 'text',
|
|
410
|
-
'caption': 'text',
|
|
411
|
-
// Heading aliases → "heading" (role-based)
|
|
412
|
-
'heading': 'heading',
|
|
413
|
-
'header': 'heading',
|
|
414
|
-
'message': 'text',
|
|
415
|
-
// Table-related aliases
|
|
416
|
-
'column': 'columnheader',
|
|
417
|
-
// List-related aliases
|
|
418
|
-
'list-item': 'listitem',
|
|
419
|
-
// Image aliases
|
|
420
|
-
'logo': 'img',
|
|
421
|
-
'image': 'img',
|
|
422
|
-
'icon': 'img',
|
|
423
|
-
// Button aliases
|
|
424
|
-
'btn': 'button',
|
|
425
|
-
// Field aliases
|
|
426
|
-
'input': 'field',
|
|
427
|
-
'textbox': 'field',
|
|
428
|
-
'textarea': 'field',
|
|
429
|
-
'editor': 'field',
|
|
430
|
-
// Search alias → searchbox role
|
|
431
|
-
'search': 'searchbox',
|
|
432
|
-
// Toggle alias → switch role
|
|
433
|
-
'toggle': 'switch',
|
|
434
|
-
// Alert alias → alertdialog role
|
|
435
|
-
'alert': 'alertdialog',
|
|
436
|
-
// Modal/drawer alias → dialog role
|
|
437
|
-
'modal': 'dialog',
|
|
438
|
-
'drawer': 'dialog',
|
|
439
|
-
// Dropdown/select alias -> combobox role
|
|
440
|
-
'dropdown': 'combobox',
|
|
441
|
-
'select': 'combobox',
|
|
442
|
-
// Data aliases
|
|
443
|
-
'item': 'listitem',
|
|
444
|
-
'card': 'article',
|
|
445
|
-
'cell': 'cell',
|
|
446
|
-
'section': 'region',
|
|
447
|
-
// Feedback aliases
|
|
448
|
-
'badge': 'text',
|
|
449
|
-
'tooltip': 'text',
|
|
450
|
-
'tag': 'text',
|
|
451
|
-
// Trigger aliases
|
|
452
|
-
'menuitem': 'menuitem',
|
|
453
|
-
// System aliases
|
|
454
|
-
'progressbar': 'progressbar',
|
|
455
|
-
'spinner': 'status',
|
|
456
|
-
'breadcrumb': 'navigation',
|
|
457
|
-
};
|
|
458
|
-
return aliasMap[elementType] || elementType;
|
|
564
|
+
return own(ELEMENT_TYPE_ALIASES, elementType) ?? elementType;
|
|
459
565
|
}
|
|
460
566
|
|
|
461
567
|
/**
|
|
@@ -475,33 +581,37 @@ export class SelectorResolver {
|
|
|
475
581
|
// Try nth-suffixed key first: hay.gui.loi.cam.on--3
|
|
476
582
|
if (nth && nth > 0) {
|
|
477
583
|
const nthKey = `${key}--${nth}`;
|
|
478
|
-
const nthEntry = selectorFile
|
|
584
|
+
const nthEntry = own(selectorFile, nthKey);
|
|
479
585
|
if (nthEntry) {
|
|
480
586
|
return this.resolveFromEntry(nthEntry, displayLabel);
|
|
481
587
|
}
|
|
482
588
|
}
|
|
483
589
|
|
|
484
|
-
// Try type-suffixed
|
|
590
|
+
// Try type-suffixed keys: add.campaign--button, add.campaign--text
|
|
591
|
+
//
|
|
592
|
+
// EVERY spelling of the step's type is tried, not just the two the step itself could have been
|
|
593
|
+
// written with (#752). The alias table maps a Gherkin spelling onto a canonical one, but it was
|
|
594
|
+
// only ever applied to the STEP; the file's keys were compared raw. So `confirm--modal` was
|
|
595
|
+
// invisible to `[Confirm] dialog` while `confirm--dialog` resolved for `[Confirm] modal` —
|
|
596
|
+
// the mapping is one-directional and the lookup inherited that. The hole was not specific to
|
|
597
|
+
// modal/drawer: all 30 non-identity aliases had it.
|
|
598
|
+
//
|
|
599
|
+
// Order is deterministic and NOT file order: canonical first (the spelling the compiler thinks
|
|
600
|
+
// in), then whatever the author literally typed, then the remaining aliases in alias-table
|
|
601
|
+
// declaration order. A file declaring both `--modal` and `--drawer` therefore resolves the same
|
|
602
|
+
// way on every machine.
|
|
485
603
|
if (elementType) {
|
|
486
604
|
const normalizedType = SelectorResolver.normalizeElementType(elementType);
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// If normalized differs from original, also try original as-is
|
|
494
|
-
if (normalizedType !== elementType) {
|
|
495
|
-
const originalKey = `${key}--${elementType}`;
|
|
496
|
-
const originalEntry = selectorFile[originalKey];
|
|
497
|
-
if (originalEntry) {
|
|
498
|
-
return this.resolveFromEntry(originalEntry, displayLabel);
|
|
605
|
+
for (const candidate of SelectorResolver.typedKeyCandidates(key, elementType, normalizedType)) {
|
|
606
|
+
const typedEntry = own(selectorFile, candidate);
|
|
607
|
+
if (typedEntry) {
|
|
608
|
+
return this.resolveFromEntry(typedEntry, displayLabel);
|
|
499
609
|
}
|
|
500
610
|
}
|
|
501
611
|
}
|
|
502
612
|
|
|
503
613
|
// Fallback to base key
|
|
504
|
-
const entry = selectorFile
|
|
614
|
+
const entry = own(selectorFile, key);
|
|
505
615
|
if (entry) {
|
|
506
616
|
return this.resolveFromEntry(entry, displayLabel);
|
|
507
617
|
}
|
|
@@ -545,20 +655,30 @@ export class SelectorResolver {
|
|
|
545
655
|
}
|
|
546
656
|
}
|
|
547
657
|
|
|
548
|
-
// Last resort:
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
658
|
+
// Last resort: any suffixed key matching key--*, for a [Ref] the compiler could not type.
|
|
659
|
+
//
|
|
660
|
+
// Two cases reach here and they are the same case: a bare [Ref] with no word after it, and a
|
|
661
|
+
// [Ref] followed by a word that is not an element type. The parser cannot tell the two apart —
|
|
662
|
+
// it takes the FIRST word after `]`, so `see [Quick Panel] is hidden` arrives typed `is`. In
|
|
663
|
+
// both, nothing is known about what kind of element is wanted, so any suffixed key is as good a
|
|
664
|
+
// guess as the next, and a project whose label needs suffixed keys keeps its state-clause steps.
|
|
665
|
+
//
|
|
666
|
+
// A step the compiler CAN type gets no such guess. The candidate scan above already tried every
|
|
667
|
+
// spelling that normalizes to that type, so whatever is still sitting under `key--` is a
|
|
668
|
+
// DIFFERENT element, and handing it back is the silent-wrong-answer shape of #752 itself.
|
|
669
|
+
// Falling through to the throw is better: it names the keys that were tried, and "add an entry"
|
|
670
|
+
// is an instruction the author can act on.
|
|
671
|
+
if (!elementType || !SelectorResolver.isKnownElementType(elementType)) {
|
|
672
|
+
const suffixedKeys = Object.keys(selectorFile).filter(k => k.startsWith(`${key}--`));
|
|
673
|
+
if (suffixedKeys.length > 0) {
|
|
674
|
+
return this.resolveFromEntry(selectorFile[suffixedKeys[0]], displayLabel);
|
|
560
675
|
}
|
|
561
676
|
}
|
|
677
|
+
|
|
678
|
+
const tried: string[] = elementType
|
|
679
|
+
? SelectorResolver.typedKeyCandidates(
|
|
680
|
+
key, elementType, SelectorResolver.normalizeElementType(elementType))
|
|
681
|
+
: [];
|
|
562
682
|
const suffix = tried.length > 0 ? ` (also tried ${tried.join(', ')})` : '';
|
|
563
683
|
throw new Error(
|
|
564
684
|
`Selector "${label}" (key: ${key}) not found in ${featureName}.yaml${suffix}. ` +
|
|
@@ -751,6 +871,20 @@ export class SelectorResolver {
|
|
|
751
871
|
return out.class || out.attribute ? out : undefined;
|
|
752
872
|
}
|
|
753
873
|
|
|
874
|
+
/**
|
|
875
|
+
* Report a selectors.yaml problem ONCE per entry+field, not once per step that resolves it.
|
|
876
|
+
*
|
|
877
|
+
* An entry is resolved again for every step that names it, so an unguarded `diagnostics.add`
|
|
878
|
+
* here repeats the same sentence a dozen times for one typo and buries everything else in the
|
|
879
|
+
* channel. `warnedContentNamed` is the existing de-dupe set; `key` namespaces the callers so two
|
|
880
|
+
* diagnostics about one entry cannot silence each other.
|
|
881
|
+
*/
|
|
882
|
+
private warnOnce(key: string, code: DiagnosticCode, message: string): void {
|
|
883
|
+
if (this.warnedContentNamed.has(key)) return;
|
|
884
|
+
this.warnedContentNamed.add(key);
|
|
885
|
+
this.diagnostics?.add(code, message, { feature: this.featureName });
|
|
886
|
+
}
|
|
887
|
+
|
|
754
888
|
/**
|
|
755
889
|
* Resolve a selectors.yaml entry that is NOT reachable by key — a `columns.<col>.elements.cell`
|
|
756
890
|
* declaration, which lives nested under a table entry. Public for that one caller; everything
|
|
@@ -824,6 +958,28 @@ export class SelectorResolver {
|
|
|
824
958
|
// The anchor is a selector in its own right; it must not inherit the PAGE's label as its
|
|
825
959
|
// accessible name, so it resolves against its own name (or nothing).
|
|
826
960
|
if (entry.anchor) v2Fields.anchor = this.resolveFromEntry(entry.anchor, entry.anchor.name ?? '');
|
|
961
|
+
// Gated on the entry KIND, like the value check below: `query:` means nothing on a non-page
|
|
962
|
+
// entry, and both the field and its diagnostic are worded purely in page terms. Today's two
|
|
963
|
+
// consumers re-check `selectorType === 'page'` at the read site (the `anchor:` precedent), so
|
|
964
|
+
// this is belt-and-braces — but a third consumer that forgets the guard would otherwise inherit
|
|
965
|
+
// a field that was never meant for it.
|
|
966
|
+
if (type === 'page' && entry.query !== undefined) {
|
|
967
|
+
const declared = String(entry.query).trim().toLowerCase();
|
|
968
|
+
// Exactly one value is legal today. A typo must NOT read as the permissive form: it would
|
|
969
|
+
// silently turn off the bare-page query assertion (#464) on a page the author meant to keep
|
|
970
|
+
// strict, which is the class of quiet weakening this whole diagnostic channel exists for.
|
|
971
|
+
if (declared === 'any') v2Fields.query = 'any';
|
|
972
|
+
else this.warnOnce(`w037:${originalLabel}:query`, 'SG-W037',
|
|
973
|
+
`selector "${originalLabel}": query: ${JSON.stringify(String(entry.query))} is not a query rule — the only value is \`any\` ("the query is not part of this page's identity"). The declaration is ignored, so a bare \`see [${originalLabel}] page\` still requires the URL to carry NO query at all.`);
|
|
974
|
+
}
|
|
975
|
+
// A page `value` is the PATH only — pathToRegexSource strips a `?`/`#` before building the
|
|
976
|
+
// pattern. Silently, until now: an author who hit the bare-page query rule (#464) and reached
|
|
977
|
+
// for the obvious workaround got byte-identical output and no hint why, which is exactly how
|
|
978
|
+
// #748 was misdiagnosed in the field as a regex-escaping bug.
|
|
979
|
+
if (type === 'page' && typeof entry.value === 'string' && /[?#]/.test(entry.value)) {
|
|
980
|
+
this.warnOnce(`w037:${originalLabel}:value`, 'SG-W037',
|
|
981
|
+
`selector "${originalLabel}": the page value ${JSON.stringify(entry.value)} carries a query/fragment, which is DROPPED — a page value is the path only, and the pattern is matched against \`u.pathname\`, which never holds one. Writing it here changes nothing. To assert the query, declare it on the step (\`see [${originalLabel}] page with {{v}}\`); to say the query is not part of this page's identity, add \`query: any\` to the entry.`);
|
|
982
|
+
}
|
|
827
983
|
|
|
828
984
|
// Helper to attach v2 fields and inputMethod
|
|
829
985
|
const withExtras = (resolved: ResolvedSelector): ResolvedSelector => {
|
package/src/harness/audit.ts
CHANGED
|
@@ -32,7 +32,8 @@ const MULTI_SCREEN_RISK_COUNT = MULTI_SCREEN_RISKS.length;
|
|
|
32
32
|
import { capabilityRegistry } from '../capabilities/registry';
|
|
33
33
|
import { discoverAndRegisterCapabilities } from '../capabilities/discover';
|
|
34
34
|
import { contextRouter } from '../capabilities/context-router';
|
|
35
|
-
import { readTextFile } from './read-text';
|
|
35
|
+
import { readTextFile, readViewpointText } from './read-text';
|
|
36
|
+
import { uiInventoryCoverage, uiInventoryFindings, UiInventoryResult } from './ui-inventory';
|
|
36
37
|
|
|
37
38
|
export interface AuditReport {
|
|
38
39
|
screen: string;
|
|
@@ -57,6 +58,8 @@ export interface AuditReport {
|
|
|
57
58
|
flowQuality?: FlowQualityResult; // #569 — flow contract verification (flows only)
|
|
58
59
|
/** #595 — coverage per DECLARED FLOW; absent when the contract declares no `flows:`. */
|
|
59
60
|
flowInventory?: InventoryResult;
|
|
61
|
+
/** #811 — design items vs the suite; present only when the unit holds design sources. */
|
|
62
|
+
uiInventory?: UiInventoryResult;
|
|
60
63
|
calibration: { // #8 — multi-axis score so a high overall can't hide a weak axis
|
|
61
64
|
axes: Record<string, number>;
|
|
62
65
|
weakest: { axis: string; value: number };
|
|
@@ -185,6 +188,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
185
188
|
const scoringCap = scoringCapId ? capabilityRegistry.get(scoringCapId) : undefined;
|
|
186
189
|
const catalog = (scoringCap?.viewpoints?.() as Catalog | undefined) || loadCatalog();
|
|
187
190
|
const spec = specCoverage(specPath, scenarios, featureText);
|
|
191
|
+
const uiInv = uiInventoryCoverage(path.join(screenDir, 'requirements'), featureText);
|
|
188
192
|
|
|
189
193
|
// P3 — intent profile from qa/context.md drives the depth threshold (focus).
|
|
190
194
|
const intent = readIntent(projectRootFromScreenDir(screenDir));
|
|
@@ -196,7 +200,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
196
200
|
// Is the declaration this suite is measured against still the one a human accepted?
|
|
197
201
|
const viewpointBaseline = checkViewpointBaseline(
|
|
198
202
|
projectRootFromScreenDir(screenDir), catalogScreenName, viewpointPath);
|
|
199
|
-
const viewpointText =
|
|
203
|
+
const viewpointText = readViewpointText(viewpointPath);
|
|
200
204
|
// #569 — a flow WITH a contract is measured against ITS declaration, not a screen's.
|
|
201
205
|
// Keyword page-type detection force-fit flows into screen checklists (a registration
|
|
202
206
|
// flow judged as a `form` screen scored coverage 0%), so for contract-holding flows the
|
|
@@ -227,7 +231,12 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
227
231
|
if (flowScenarios.length) {
|
|
228
232
|
for (const c of flowCoveredThemes(gate.gaps, flowScenarios)) {
|
|
229
233
|
const i = gate.gaps.findIndex((g) => g.theme === c.theme);
|
|
230
|
-
if (i >= 0) {
|
|
234
|
+
if (i >= 0) {
|
|
235
|
+
gate.gaps.splice(i, 1); gate.themesCovered++; flowCredits.push(c);
|
|
236
|
+
// Keep the claim-proof list in step with the count (#776) — a theme credited to a flow
|
|
237
|
+
// must SAY it came from a flow, or the evidence lines silently under-report the total.
|
|
238
|
+
gate.coveredBy.push({ theme: c.theme, by: `(flow) ${c.flow}`, keyword: '—' });
|
|
239
|
+
}
|
|
231
240
|
}
|
|
232
241
|
gate.coverageRatio = gate.themesTotal ? gate.themesCovered / gate.themesTotal : 1;
|
|
233
242
|
}
|
|
@@ -292,12 +301,16 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
292
301
|
// #569 — handoff integrity: a flow's depth is also whether each cross-screen transition
|
|
293
302
|
// is followed by an assertion on the carried state (same floor discipline as the other caps).
|
|
294
303
|
const HANDOFF_FLOOR = 0.5;
|
|
295
|
-
|
|
304
|
+
// Each FLOOR is a floor on the CAP, not on the result — an out-of-range input ratio walks
|
|
305
|
+
// straight through it (a -1.5 oracle ratio yielded businessDepth -0.25 and crashed the CLI's
|
|
306
|
+
// bar renderer, #762). The axis is declared `0..1` above and weighted `critical`, so a negative
|
|
307
|
+
// value would also drag `score.overall` below zero. Clamp once, here, where the axis is formed.
|
|
308
|
+
const businessDepth = Math.max(0, Math.min(1, Math.min(
|
|
296
309
|
depth.bcDepthRatio,
|
|
297
310
|
flowDepth.stateful ? FLOW_DEPTH_FLOOR + (1 - FLOW_DEPTH_FLOOR) * flowDepth.ratio : 1,
|
|
298
|
-
oracle.
|
|
311
|
+
oracle.weakFacet ? ORACLE_FLOOR + (1 - ORACLE_FLOOR) * oracle.ratio : 1,
|
|
299
312
|
flowQ.hasContract && flowQ.handoffs.total > 0 ? HANDOFF_FLOOR + (1 - HANDOFF_FLOOR) * flowQ.handoffs.ratio : 1,
|
|
300
|
-
);
|
|
313
|
+
)));
|
|
301
314
|
// When the taxonomy drifted (most scenarios unclassified), the balance axis is unreliable — cap it
|
|
302
315
|
// at 0.5 instead of awarding a vacuous 1.0 so a stale taxonomy fails loudly, not silently (H1).
|
|
303
316
|
const balanceScore = balance.unclassifiedRatio > 0.4
|
|
@@ -364,7 +377,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
364
377
|
{ key: 'atomicLedger', value: ledger.ratio, weight: 0.13, applicable: ledger.hasViewpoint && ledger.total > 0 && !viewpointMoved, critical: true, withheld: viewpointMoved && ledger.hasViewpoint && ledger.total > 0 },
|
|
365
378
|
{ key: 'businessDepth', value: businessDepth, weight: 0.20, applicable: true, critical: true },
|
|
366
379
|
{ key: 'claimProof', value: claim.ratio, weight: 0.15, applicable: claim.withClaims > 0, critical: true },
|
|
367
|
-
{ key: 'traceability', value: traceScore, weight: 0.09, applicable: viewpoints.length > 0 && !viewpointMoved, critical: false, withheld: viewpointMoved && viewpoints.length > 0 },
|
|
380
|
+
{ key: 'traceability', value: traceScore, weight: 0.09, applicable: viewpoints.length > 0 && scenarios.length > 0 && !viewpointMoved, critical: false, withheld: viewpointMoved && viewpoints.length > 0 && scenarios.length > 0 },
|
|
368
381
|
// A use-case decomposition is EF-heavy by construction (1 basic flow, N alternates, N
|
|
369
382
|
// exceptions) — that is the shape of a well-decomposed use case, not neglected business
|
|
370
383
|
// core. `flowCoverage` already measures whether the journey's phases are covered, so
|
|
@@ -576,7 +589,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
576
589
|
findings.push(`COVERED-VIA-FLOW: critical theme "${c.theme}" is not covered on this screen but is deeply covered by flow "${c.flow}" — cross-screen depth correctly owned by the flow, so it is credited (not a screen gap). Verify with \`sungen flow-check\`.`);
|
|
577
590
|
}
|
|
578
591
|
// Verification-scope (advisory): API/DB verification the project's recorded test type never asked for.
|
|
579
|
-
for (const v of verificationScopeFindings(featureText,
|
|
592
|
+
for (const v of verificationScopeFindings(featureText, viewpointText, capProfile)) {
|
|
580
593
|
findings.push(`VERIFICATION-OUT-OF-SCOPE (${v.layer}): this unit ${v.message}`);
|
|
581
594
|
}
|
|
582
595
|
for (const g of gate.gaps) {
|
|
@@ -594,12 +607,13 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
594
607
|
`add data assertions (\`... with {{value}}\`, \`see all ... contain {{v}}\`) or, if cross-screen, defer to a flow with @manual + reason.`,
|
|
595
608
|
);
|
|
596
609
|
}
|
|
597
|
-
// H5 — state isolation: a
|
|
598
|
-
//
|
|
610
|
+
// H5 — state isolation: a stateful flow that mutates the cart needs per-scenario isolation, else
|
|
611
|
+
// count/quantity asserts go flaky. Warn (advisory) when no mitigation is present
|
|
599
612
|
// (@cleanup:cart / @isolate / a "Given … empty cart" background / fresh context).
|
|
613
|
+
// #759 — scoped by the ABSENCE of @serial, not the presence of @parallel: parallel is the default.
|
|
600
614
|
if (isoRisk) {
|
|
601
615
|
const what = flowQ.contract?.stateful ?? 'cart';
|
|
602
|
-
findings.push(`ISOLATION-RISK: this
|
|
616
|
+
findings.push(`ISOLATION-RISK: this flow mutates ${what} but has no per-scenario isolation → count/quantity/state asserts can go flaky when scenarios share it. Add \`@cleanup:${what}\` (or \`@isolate\`, a fresh context, or \`{{$timestamp}}\` data) so each scenario starts clean.`);
|
|
603
617
|
}
|
|
604
618
|
// #414 — serial-cascade masking: an @serial suite of independent (re-navigating) scenarios hides
|
|
605
619
|
// failures, because Playwright's describe.serial SKIPS everything after the first red. Isolation is
|
|
@@ -647,7 +661,8 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
647
661
|
...(unsourced.length ? [`${unsourced.length} scenario(s) could not be traced to a source`] : []),
|
|
648
662
|
];
|
|
649
663
|
findings.push(`VIEWPOINT-MISSING: \`requirements/test-viewpoint.md\` ${ledger.hasViewpoint ? 'declares no viewpoints' : 'does not exist'}, so this unit has no yardstick. Two scored axes are excluded (atomicLedger, traceability) and four sensors cannot run: the atomic ledger, the declaration-integrity checks (reused ids, dangling refs, uniform priority), the browser-gesture check and the two-way continuity check. Nothing here is a pass — it is unmeasured [${lost.join(', ')}]. A flow contract does NOT substitute: the contract declares the use case, the viewpoint declares the CLAIMS the suite is scored against, and only the second is independent of what was generated. Author it (\`sungen add-flow\` scaffolds a template), then \`sungen audit --screen ${screenName} --accept-viewpoint\`.`);
|
|
650
|
-
} else if (trace.mappedRatio < 0.5) {
|
|
664
|
+
} else if (scenarios.length > 0 && trace.mappedRatio < 0.5) {
|
|
665
|
+
// No scenario yet is not a traceability gap — the axis is n/a, like coverage and claimProof (#852).
|
|
651
666
|
findings.push(`TRACE: ${trace.note}`);
|
|
652
667
|
}
|
|
653
668
|
// (UNIVERSAL viewpoint-gap finding now emitted by the `ui` gate sensor — see the gate block below.)
|
|
@@ -688,8 +703,10 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
688
703
|
for (const d of downstream.underCovered) {
|
|
689
704
|
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}\`).`);
|
|
690
705
|
}
|
|
706
|
+
// #811 — design inventory (advisory): never moves the score or the gate.
|
|
707
|
+
findings.push(...uiInventoryFindings(uiInv));
|
|
691
708
|
for (const m of manualOracleResult.insufficient.slice(0, 8)) {
|
|
692
|
-
findings.push(`MANUAL-STEPS-INSUFFICIENT: "${m}" — @manual is incomplete: either Gherkin steps without a Then (invalid Gherkin; comments can't substitute), or a documentation manual whose comments lack numbered action steps + a labeled Observable:/Oracle: line. Write \`# Tester verifies:\` as "1. <action>" … "N. Observable: <what>" / "Oracle: <tool>" — that is what the delivery parser extracts into Steps/Expected.`);
|
|
709
|
+
findings.push(`MANUAL-STEPS-INSUFFICIENT: "${m}" — @manual is incomplete: either Gherkin steps without a Then (invalid Gherkin; comments can't substitute), a Then that only asserts presence (\`see [X] table\`) while the real oracle sits in a \`# Tester verifies:\` comment (the deliverable's Expected would not say what is checked), or a documentation manual whose comments lack numbered action steps + a labeled Observable:/Oracle: line. Write \`# Tester verifies:\` as "1. <action>" … "N. Observable: <what>" / "Oracle: <tool>" — that is what the delivery parser extracts into Steps/Expected.`);
|
|
693
710
|
}
|
|
694
711
|
// TQ-2 — automatable @manual: deferred (usually cross-screen) but fully DSL-expressible.
|
|
695
712
|
for (const m of autoManual.scenarios.slice(0, 8)) {
|
|
@@ -787,7 +804,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
787
804
|
// #622 — the priority table is where release selection comes from; a tag that contradicts it
|
|
788
805
|
// means nobody can tell which of the two was the decision.
|
|
789
806
|
{
|
|
790
|
-
const pm = priorityMismatches(hasYardstick ?
|
|
807
|
+
const pm = priorityMismatches(hasYardstick ? viewpoints : [], scenarios);
|
|
791
808
|
const byId = new Map<string, { declared: string; tagged: string; n: number }>();
|
|
792
809
|
for (const m of pm) {
|
|
793
810
|
const cur = byId.get(m.id) ?? { declared: m.declared, tagged: m.tagged, n: 0 };
|
|
@@ -860,8 +877,11 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
860
877
|
axes.manualOracle = Math.round(manualCompleteness * 100) / 100;
|
|
861
878
|
// Only a real mismatch scores 0. Without a yardstick `trace.mappedRatio` is 0 because there is
|
|
862
879
|
// nothing to map to, and reporting that as the weakest axis sent authors to "raise taxonomy" —
|
|
863
|
-
// an axis that was never measured.
|
|
864
|
-
|
|
880
|
+
// an axis that was never measured. The same holds for a yardstick the ledger can read but that
|
|
881
|
+
// declares no viewpoint ID traceability can read (per-axis tables only, #832): the ledger has
|
|
882
|
+
// items, so `hasYardstick` is true, yet there is still nothing to map to. Same condition as the
|
|
883
|
+
// `traceability` score axis (`applicable: viewpoints.length > 0`).
|
|
884
|
+
if (hasYardstick && viewpoints.length > 0 && scenarios.length > 0) axes.taxonomy = taxonomyMismatch ? 0 : Math.round(trace.mappedRatio * 100) / 100;
|
|
865
885
|
const notApplicable = axisDefs.filter((a) => !a.applicable).map((a) => a.key);
|
|
866
886
|
const weakestEntry = Object.entries(axes).sort((a, b) => a[1] - b[1])[0];
|
|
867
887
|
const weakest = { axis: weakestEntry[0], value: weakestEntry[1] };
|
|
@@ -889,6 +909,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
889
909
|
gate, depth, claim, taxonomy, balance, duplicates, trace, spec,
|
|
890
910
|
taxonomyMismatch, reusedScenarioIds: reusedIds, downstream, manualOracle: manualOracleResult, automatableManual: autoManual, flowDepth, oracle, ledger, viewpointBaseline, calibration,
|
|
891
911
|
...(isUiFlow ? { flowQuality: flowQ, flowInventory: inventory ?? undefined } : {}),
|
|
912
|
+
...(uiInv.designSources.length ? { uiInventory: uiInv } : {}),
|
|
892
913
|
score: {
|
|
893
914
|
overall: Math.round(overall * 10) / 10,
|
|
894
915
|
coverage: Math.round(coverage * 100) / 100,
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import * as path from 'path';
|
|
13
13
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
14
|
+
import { readYamlConfig, warnUnreadableConfig } from './config-file';
|
|
14
15
|
|
|
15
16
|
export interface CapabilityProfile {
|
|
16
17
|
platform?: string; // web | mobile | … (the runtime adapter)
|
|
@@ -23,7 +24,13 @@ export interface CapabilityProfile {
|
|
|
23
24
|
* never asked for.
|
|
24
25
|
*/
|
|
25
26
|
verification?: string[];
|
|
26
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Where the profile came from. `'unreadable'` is NOT `'absent'`: the file exists and says
|
|
29
|
+
* something, we just cannot read it. Callers that WRITE capabilities.yaml must branch on this —
|
|
30
|
+
* scaffolding a default over a file whose contents we failed to parse destroys the user's
|
|
31
|
+
* declaration (see `cli/commands/generate.ts`).
|
|
32
|
+
*/
|
|
33
|
+
source: 'capabilities.yaml' | 'absent' | 'unreadable';
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
export type VerifyLayer = 'ui' | 'api' | 'db';
|
|
@@ -119,18 +126,22 @@ export function capabilitiesPath(cwd: string): string {
|
|
|
119
126
|
|
|
120
127
|
export function readCapabilities(cwd: string): CapabilityProfile {
|
|
121
128
|
const p = capabilitiesPath(cwd);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
};
|
|
131
|
-
} catch {
|
|
132
|
-
return { platform: undefined, enabled: [], source: 'absent' };
|
|
129
|
+
const read = readYamlConfig<Record<string, unknown> | null>(p);
|
|
130
|
+
if (read.state === 'absent') return { platform: undefined, enabled: [], source: 'absent' };
|
|
131
|
+
if (read.state === 'unreadable') {
|
|
132
|
+
// The profile decides the runtime adapter, the driver set and the verification scope. Reporting
|
|
133
|
+
// it once here covers all fifteen call sites; what each does about it is still its own choice.
|
|
134
|
+
warnUnreadableConfig(p, read.error,
|
|
135
|
+
'this run cannot tell which platform or drivers the project declares, and falls back to none');
|
|
136
|
+
return { platform: undefined, enabled: [], source: 'unreadable' };
|
|
133
137
|
}
|
|
138
|
+
const y = (read.value || {}) as Record<string, unknown>;
|
|
139
|
+
return {
|
|
140
|
+
platform: typeof y.platform === 'string' ? y.platform : undefined,
|
|
141
|
+
enabled: Array.isArray(y.enabled) ? y.enabled.map(String) : [],
|
|
142
|
+
verification: Array.isArray(y.verification) ? y.verification.map(String) : undefined,
|
|
143
|
+
source: 'capabilities.yaml',
|
|
144
|
+
};
|
|
134
145
|
}
|
|
135
146
|
|
|
136
147
|
export function writeCapabilities(cwd: string, profile: { platform?: string; enabled: string[]; verification?: string[] }): string {
|