@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
|
@@ -24,7 +24,7 @@ page_types:
|
|
|
24
24
|
detect_keywords: [cart, product, checkout, catalog, brand, category, "add to cart"]
|
|
25
25
|
must_cover:
|
|
26
26
|
- theme: list-data
|
|
27
|
-
keywords: [list, displayed, card, "product card", grid]
|
|
27
|
+
keywords: ["product list", displayed, card, "product card", grid]
|
|
28
28
|
depth:
|
|
29
29
|
requires: data-assertion
|
|
30
30
|
cross_screen: false
|
|
@@ -38,7 +38,7 @@ page_types:
|
|
|
38
38
|
keywords: ["product detail", "detail page", "same product name", "matching name", "consistency", "same price"]
|
|
39
39
|
template: "User remember [Product Name] text as {{v}} … User see [Detail Product Name] header with {{v}}"
|
|
40
40
|
- theme: cart-correctness
|
|
41
|
-
keywords: ["cart contains", quantity, "price", subtotal, "in cart", "cart content", "cart product"]
|
|
41
|
+
keywords: ["cart contains", "cart quantity", "cart price", subtotal, "in cart", "cart content", "cart product"]
|
|
42
42
|
depth:
|
|
43
43
|
requires: data-assertion
|
|
44
44
|
cross_screen: true
|
|
@@ -77,7 +77,7 @@ page_types:
|
|
|
77
77
|
keywords: ["required", "must be", "error message", "validation message"]
|
|
78
78
|
template: "User see [Field Error] message with {{error_text}}"
|
|
79
79
|
- theme: format-boundary
|
|
80
|
-
keywords: [format, invalid, boundary, length, range]
|
|
80
|
+
keywords: [format, invalid, boundary, "max length", "min length", range]
|
|
81
81
|
depth:
|
|
82
82
|
requires: data-assertion
|
|
83
83
|
cross_screen: false
|
|
@@ -125,10 +125,10 @@ page_types:
|
|
|
125
125
|
# suite, which is a scoring decision for an owner, not a drive-by. `viewpoint-ledger` already
|
|
126
126
|
# covers the dangerous half by refusing `tap [Back]` as an answer to an OS-back viewpoint item.
|
|
127
127
|
mobile-list:
|
|
128
|
-
detect_keywords: [feed, "pull to refresh", "infinite scroll", "load more", "list item", "list row"]
|
|
128
|
+
detect_keywords: ["feed item", "pull to refresh", "infinite scroll", "load more", "list item", "list row"]
|
|
129
129
|
must_cover:
|
|
130
130
|
- theme: list-data
|
|
131
|
-
keywords: [
|
|
131
|
+
keywords: ["feed item", row, "list item", displayed]
|
|
132
132
|
depth:
|
|
133
133
|
requires: data-assertion
|
|
134
134
|
cross_screen: false
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a config file has THREE outcomes, not two: absent, parsed, and present-but-unreadable.
|
|
3
|
+
*
|
|
4
|
+
* Collapsing the third into the first is the defect class #782 catalogues. The shape recurs because
|
|
5
|
+
* it looks harmless: an `fs.existsSync` guard already answers absence, so the `catch` below it only
|
|
6
|
+
* ever fires for CORRUPTION — and returning the absent value there is the one case that must not be
|
|
7
|
+
* silent. A file that is absent was never written; a file that is broken WAS written, by someone who
|
|
8
|
+
* believes it is in effect.
|
|
9
|
+
*
|
|
10
|
+
* `run-manifest.ts:hashTopLevel` already draws the distinction for its own reader (`__unparseable__`
|
|
11
|
+
* rather than `{}`); this is that idea as a seam every config reader can share, so the next one does
|
|
12
|
+
* not have to rediscover it.
|
|
13
|
+
*
|
|
14
|
+
* Nothing here throws: the caller owns the consequence, because only the caller knows it. What the
|
|
15
|
+
* type removes is the option of not deciding.
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from 'fs';
|
|
18
|
+
import { parse as parseYaml } from 'yaml';
|
|
19
|
+
|
|
20
|
+
export type ConfigRead<T> =
|
|
21
|
+
| { state: 'absent' }
|
|
22
|
+
| { state: 'ok'; value: T }
|
|
23
|
+
| { state: 'unreadable'; error: string };
|
|
24
|
+
|
|
25
|
+
const message = (e: unknown): string => (e instanceof Error ? e.message.split('\n')[0] : String(e));
|
|
26
|
+
|
|
27
|
+
/** Read + parse a YAML config. An empty or comment-only file parses to null — that is `ok`, not broken. */
|
|
28
|
+
export function readYamlConfig<T>(file: string): ConfigRead<T> {
|
|
29
|
+
if (!fs.existsSync(file)) return { state: 'absent' };
|
|
30
|
+
try {
|
|
31
|
+
return { state: 'ok', value: (parseYaml(fs.readFileSync(file, 'utf-8')) ?? null) as T };
|
|
32
|
+
} catch (e) {
|
|
33
|
+
return { state: 'unreadable', error: message(e) };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Read + parse a JSON config. Unlike YAML, an empty file is NOT valid JSON and reads as unreadable. */
|
|
38
|
+
export function readJsonConfig<T>(file: string): ConfigRead<T> {
|
|
39
|
+
if (!fs.existsSync(file)) return { state: 'absent' };
|
|
40
|
+
try {
|
|
41
|
+
return { state: 'ok', value: JSON.parse(fs.readFileSync(file, 'utf-8')) as T };
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return { state: 'unreadable', error: message(e) };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* One line to stderr naming the file, the parse error, and WHAT THE RUN WILL DO INSTEAD — the last
|
|
49
|
+
* part is the whole point: "capabilities.yaml is broken" leaves the reader guessing; "…so this run
|
|
50
|
+
* treats the project as web" tells them why their mobile suite just compiled wrong.
|
|
51
|
+
*
|
|
52
|
+
* At most once per file per process. `readCapabilities` alone has fifteen call sites, and a warning
|
|
53
|
+
* repeated fifteen times reads as noise — which is its own way of being ignored.
|
|
54
|
+
*/
|
|
55
|
+
const warned = new Set<string>();
|
|
56
|
+
|
|
57
|
+
export function warnUnreadableConfig(file: string, error: string, consequence: string): void {
|
|
58
|
+
if (warned.has(file)) return;
|
|
59
|
+
warned.add(file);
|
|
60
|
+
console.error(`⚠️ ${file}: unreadable (${error}) — ${consequence}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Test seam: the dedup set is process-global, so a suite exercising several cases must clear it. */
|
|
64
|
+
export function resetUnreadableConfigWarnings(): void {
|
|
65
|
+
warned.clear();
|
|
66
|
+
}
|
|
@@ -155,29 +155,100 @@ function overrideKeys(raw?: string): Set<string> {
|
|
|
155
155
|
return out;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
interface FeatureUnderLint {
|
|
159
|
+
/** Feature file basename — names the test-data file the runtime loads for it. */
|
|
160
|
+
name: string;
|
|
161
|
+
scenarios: ParsedScenario[];
|
|
162
|
+
tags: string[];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** What every feature of a unit shares: unit-level refs and the @api catalog. */
|
|
166
|
+
interface UnitLintContext {
|
|
167
|
+
screenDir: string;
|
|
168
|
+
screenName: string;
|
|
169
|
+
cwd: string;
|
|
170
|
+
fileRefs: Set<string>;
|
|
171
|
+
apiParams: Map<string, string[]>;
|
|
172
|
+
}
|
|
173
|
+
|
|
158
174
|
/** Lint the @cases/@query usage of one screen/flow directory. Returns advisory warnings. */
|
|
159
175
|
export function lintDataDriven(screenDir: string, cwd: string = process.cwd()): DataDrivenWarning[] {
|
|
160
176
|
const base = path.basename(screenDir);
|
|
161
177
|
const screenName = unitIdOf(screenDir);
|
|
162
178
|
// Every `.feature` of the unit — `add --feature` sub-features lint too. A file that
|
|
163
179
|
// fails to parse contributes nothing rather than voiding the whole unit's lint.
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
for (const p of
|
|
180
|
+
const features: FeatureUnderLint[] = [];
|
|
181
|
+
const featureFiles = featureFilesFor(screenDir, base).filter((f) => fs.existsSync(f));
|
|
182
|
+
for (const p of featureFiles) {
|
|
167
183
|
try {
|
|
168
184
|
const feature = new GherkinParser().parseFeatureFile(p);
|
|
169
|
-
|
|
170
|
-
featureTags.push(...(feature.tags || []));
|
|
185
|
+
features.push({ name: path.basename(p, '.feature'), scenarios: feature.scenarios || [], tags: feature.tags || [] });
|
|
171
186
|
} catch {
|
|
172
187
|
/* a file that fails to parse contributes nothing */
|
|
173
188
|
}
|
|
174
189
|
}
|
|
175
|
-
if (scenarios.length
|
|
190
|
+
if (!features.some((f) => f.scenarios.length > 0)) return [];
|
|
191
|
+
|
|
192
|
+
const ctx: UnitLintContext = {
|
|
193
|
+
screenDir, screenName, cwd,
|
|
194
|
+
// `{{key}}` refs carried by the unit's selectors (i18n) and mock catalog are live too.
|
|
195
|
+
fileRefs: refsInUnitFiles(screenDir),
|
|
196
|
+
apiParams: apiParamsByName(screenName, cwd),
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// #839 — each feature against ITS OWN test-data file. The runtime loads
|
|
200
|
+
// `test-data/<feature file basename>.yaml` (TestDataLoader.load(unit, featureFileName)), with no
|
|
201
|
+
// fallback and no merging; reading `<unit dir>.yaml` instead found nothing whenever the names
|
|
202
|
+
// differ — the default for `add --screen x --path /a/b` (→ b.feature) and for `--feature` — so
|
|
203
|
+
// every key read as missing and no orphan was ever reported. The tables are deliberately NOT
|
|
204
|
+
// merged: a key in another feature's file compiles to the literal `${key}` here.
|
|
205
|
+
const warns: DataDrivenWarning[] = [];
|
|
206
|
+
for (const f of features) if (f.scenarios.length > 0) warns.push(...lintFeature(f, ctx));
|
|
207
|
+
warns.push(...unloadedTestDataFiles(screenDir, featureFiles.map((f) => path.basename(f, '.feature'))));
|
|
208
|
+
|
|
209
|
+
// Catalog-level lint (SELECT-only, params declared/used, datasource present).
|
|
210
|
+
try {
|
|
211
|
+
for (const e of lintCatalog(screenName, null, cwd).errors) warns.push({ message: e });
|
|
212
|
+
} catch {
|
|
213
|
+
/* no catalog → nothing to lint */
|
|
214
|
+
}
|
|
215
|
+
return warns;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* A test-data file no feature loads is dead in full: the runtime reads only
|
|
220
|
+
* `<feature>.yaml` and its `<feature>.<env>.yaml` overlays. Typically a `<unit>.yaml` written by
|
|
221
|
+
* hand next to a feature `add --path` named after the URL — its keys never reach a test.
|
|
222
|
+
* Every existing feature file counts (even one that failed to parse), so a parse error never
|
|
223
|
+
* turns its data into a false "loaded by no feature".
|
|
224
|
+
*/
|
|
225
|
+
function unloadedTestDataFiles(screenDir: string, featureNames: string[]): DataDrivenWarning[] {
|
|
226
|
+
let files: string[];
|
|
227
|
+
try {
|
|
228
|
+
files = fs.readdirSync(path.join(screenDir, 'test-data')).filter((f) => /\.ya?ml$/.test(f) && !f.startsWith('_'));
|
|
229
|
+
} catch {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
const loaded = (stem: string): boolean => featureNames.some((n) => stem === n || stem.startsWith(`${n}.`));
|
|
233
|
+
return files
|
|
234
|
+
.filter((f) => !loaded(f.replace(/\.ya?ml$/, '')) || !f.endsWith('.yaml'))
|
|
235
|
+
.map((f) => ({
|
|
236
|
+
message: `test-data/${f} is loaded by no feature — the runtime reads test-data/<feature>.yaml (+ <feature>.<env>.yaml) for ${featureNames.map((n) => `${n}.feature`).join(', ')}: move its keys there or remove it.`,
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
176
239
|
|
|
177
|
-
|
|
178
|
-
const
|
|
240
|
+
function lintFeature(feature: FeatureUnderLint, ctx: UnitLintContext): DataDrivenWarning[] {
|
|
241
|
+
const { screenName, cwd, fileRefs, apiParams } = ctx;
|
|
242
|
+
const { scenarios, tags: featureTags } = feature;
|
|
243
|
+
const tdFile = `test-data/${feature.name}.yaml`;
|
|
244
|
+
const tdPath = path.join(ctx.screenDir, 'test-data', `${feature.name}.yaml`);
|
|
245
|
+
const td: Record<string, any> = fs.existsSync(tdPath) ? parseYaml(fs.readFileSync(tdPath, 'utf8')) || {} : {};
|
|
179
246
|
const topKeys = new Set(Object.keys(td));
|
|
180
247
|
const warns: DataDrivenWarning[] = [];
|
|
248
|
+
// H3: an `@extend:<base>` scenario runs the base's steps first — the base's captures bind for it.
|
|
249
|
+
// Per feature, like the compiler's steps registry: an `@extend` never reaches another file's base.
|
|
250
|
+
const stepsBase = new Map<string, ParsedScenario>();
|
|
251
|
+
for (const sc of scenarios) if (sc.stepsName) stepsBase.set(sc.stepsName, sc);
|
|
181
252
|
// Bindings a `@beforeAll` hook's `@api:`/`@query:` preconditions make are readable from EVERY
|
|
182
253
|
// scenario of the feature (the compiler registers them feature-wide). A `remember … as` inside the
|
|
183
254
|
// hook is NOT — it stays hook-local — so only the tag-bound names are exempted here.
|
|
@@ -186,18 +257,12 @@ export function lintDataDriven(screenDir: string, cwd: string = process.cwd()):
|
|
|
186
257
|
if (sc.hookType !== 'beforeAll') continue;
|
|
187
258
|
for (const t of sc.tags || []) { const m = t.match(/^@(?:api|query):([A-Za-z_]\w*)/); if (m) featureBound.add(m[1]); }
|
|
188
259
|
}
|
|
189
|
-
// H3: an `@extend:<base>` scenario runs the base's steps first — the base's captures bind for it.
|
|
190
|
-
const stepsBase = new Map<string, ParsedScenario>();
|
|
191
|
-
for (const sc of scenarios) if (sc.stepsName) stepsBase.set(sc.stepsName, sc);
|
|
192
|
-
// `{{key}}` refs carried by the unit's selectors (i18n) and mock catalog are live too.
|
|
193
|
-
const fileRefs = refsInUnitFiles(screenDir);
|
|
194
260
|
// A feature-level `@manual` makes every scenario manual (the whole file is skipped by the compiler).
|
|
195
261
|
const featureManual = featureTags.some((t) => t === '@manual' || t.startsWith('@manual:'));
|
|
196
262
|
|
|
197
263
|
// What the scenarios bind, collected up front — the @query pass below adds the params it binds by name.
|
|
198
264
|
const usedHeads = new Set<string>();
|
|
199
265
|
const usedDatasets = new Set<string>();
|
|
200
|
-
const apiParams = apiParamsByName(screenName, cwd);
|
|
201
266
|
for (const h of fileRefs) usedHeads.add(h);
|
|
202
267
|
for (const sc of scenarios) {
|
|
203
268
|
for (const r of collectRefs(sc, featureManual || isManualScenario(sc))) usedHeads.add(r.split(/[.[]/)[0]);
|
|
@@ -228,7 +293,7 @@ export function lintDataDriven(screenDir: string, cwd: string = process.cwd()):
|
|
|
228
293
|
const ds = casesTag.slice('@cases:'.length).trim();
|
|
229
294
|
const rows = td[ds];
|
|
230
295
|
if (!Array.isArray(rows)) {
|
|
231
|
-
warns.push({ scenario: sc.name, message: `@cases:${ds} → dataset "${ds}" is missing or not a list in
|
|
296
|
+
warns.push({ scenario: sc.name, message: `@cases:${ds} → dataset "${ds}" is missing or not a list in ${tdFile}.` });
|
|
232
297
|
} else {
|
|
233
298
|
const colSets = rows.map((r) => new Set(Object.keys(r || {}).filter((k) => !ROW_META.has(k))));
|
|
234
299
|
const allCols = new Set<string>();
|
|
@@ -256,7 +321,7 @@ export function lintDataDriven(screenDir: string, cwd: string = process.cwd()):
|
|
|
256
321
|
for (const r of refs) {
|
|
257
322
|
const head = r.split(/[.[]/)[0];
|
|
258
323
|
if (head.startsWith('$') || bound.has(head) || featureBound.has(head) || topKeys.has(head)) continue;
|
|
259
|
-
warns.push({ scenario: sc.name, message: `{{${r}}} is not a test-data key and nothing binds it at run time — it compiles to the literal text '\${${r}}' (add the key to
|
|
324
|
+
warns.push({ scenario: sc.name, message: `{{${r}}} is not a test-data key and nothing binds it at run time — it compiles to the literal text '\${${r}}' (add the key to ${tdFile}, or fix the ref).` });
|
|
260
325
|
}
|
|
261
326
|
}
|
|
262
327
|
|
|
@@ -289,15 +354,8 @@ export function lintDataDriven(screenDir: string, cwd: string = process.cwd()):
|
|
|
289
354
|
for (const k of topKeys) {
|
|
290
355
|
if (k.startsWith('_')) continue; // Data Factory metadata, not scenario data
|
|
291
356
|
if (!usedHeads.has(k) && !usedDatasets.has(k)) {
|
|
292
|
-
warns.push({ message: `test-data key "${k}" is defined but never referenced ({{${k}}}, a @cases dataset, or an override) — dead data: bind it into a scenario or remove it.` });
|
|
357
|
+
warns.push({ message: `test-data key "${k}" is defined but never referenced ({{${k}}}, a @cases dataset, or an override) by ${feature.name}.feature — dead data: bind it into a scenario or remove it from ${tdFile}.` });
|
|
293
358
|
}
|
|
294
359
|
}
|
|
295
|
-
|
|
296
|
-
// Catalog-level lint (SELECT-only, params declared/used, datasource present).
|
|
297
|
-
try {
|
|
298
|
-
for (const e of lintCatalog(screenName, null, cwd).errors) warns.push({ message: e });
|
|
299
|
-
} catch {
|
|
300
|
-
/* no catalog → nothing to lint */
|
|
301
|
-
}
|
|
302
360
|
return warns;
|
|
303
361
|
}
|
|
@@ -15,6 +15,7 @@ import * as fs from 'fs';
|
|
|
15
15
|
import * as path from 'path';
|
|
16
16
|
import { loadUnitScenarios, parseViewpointOverview, ScenarioInfo, ViewpointEntry } from './parse';
|
|
17
17
|
import { loadCatalog, viewpointGate, assertionDepth, dataThemesFor, CatalogTheme } from './sensors';
|
|
18
|
+
import { keywordHit } from './keyword-match';
|
|
18
19
|
|
|
19
20
|
export type DepthAction = 'deepen' | 'defer';
|
|
20
21
|
|
|
@@ -45,7 +46,7 @@ export interface DepthLintReport {
|
|
|
45
46
|
function matchTheme(s: ScenarioInfo, dataThemes: CatalogTheme[]): CatalogTheme | undefined {
|
|
46
47
|
return dataThemes.find((t) => {
|
|
47
48
|
const kws = t.depth?.keywords?.length ? t.depth.keywords : t.keywords;
|
|
48
|
-
return kws.some((k) => s.haystack
|
|
49
|
+
return kws.some((k) => keywordHit(s.haystack, k));
|
|
49
50
|
});
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import * as fs from 'fs';
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import { featureFilesFor } from './unit-paths';
|
|
15
|
+
import { keywordHit } from './keyword-match';
|
|
15
16
|
import { loadScenarios } from './parse';
|
|
16
17
|
import { parseScenarios } from './capability-plan';
|
|
17
18
|
import { readTextFile } from './read-text';
|
|
@@ -45,7 +46,10 @@ function featureTags(fp: string): string[] {
|
|
|
45
46
|
}
|
|
46
47
|
function targetsFromHint(hint: string): string[] {
|
|
47
48
|
const h = hint.toLowerCase();
|
|
48
|
-
|
|
49
|
+
// Word-aware (#779): every KNOWN_TARGET is an ordinary noun, so a substring test read
|
|
50
|
+
// `re·order` / `dis·order` as the target `order` and then matched the two to each other —
|
|
51
|
+
// a deferral about reorder history reported COVERED by a flow scenario about a disorder.
|
|
52
|
+
const hits = KNOWN_TARGETS.filter((k) => keywordHit(h, k));
|
|
49
53
|
if (hits.length) return hits;
|
|
50
54
|
// fallback: significant words after "->"
|
|
51
55
|
const after = h.split('->').pop() || h;
|
|
@@ -87,7 +91,7 @@ export function buildFlowCheck(cwd: string, onlyFlow?: string): FlowCheckReport
|
|
|
87
91
|
// `@owned-by:<flow>` names the owner explicitly → only that flow's scenarios can cover it
|
|
88
92
|
// (a false @owned-by is then surfaced as missing). Else any flow may cover it (legacy).
|
|
89
93
|
const pool = s.ownedBy ? flowScenarios.filter((fs2) => fs2.flow === s.ownedBy) : flowScenarios;
|
|
90
|
-
const matches = pool.filter((fs2) => targets.some((t) => fs2.haystack
|
|
94
|
+
const matches = pool.filter((fs2) => targets.some((t) => keywordHit(fs2.haystack, t)));
|
|
91
95
|
let verdict: Deferral['verdict'] = 'missing';
|
|
92
96
|
let via: string | undefined;
|
|
93
97
|
if (matches.some((m) => m.deep)) { verdict = 'covered'; via = matches.find((m) => m.deep)!.flow; }
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyword matching for the audit's coverage sensors — the single rule every "does a scenario
|
|
3
|
+
* mention this?" question goes through.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS (#776). The sensors used to ask `haystack.includes(keyword)`. A substring match
|
|
6
|
+
* is satisfied by a keyword buried inside an unrelated word, and it fails in the direction that
|
|
7
|
+
* HIDES problems: a theme nobody tested reads as covered. Measured case — the scenario title
|
|
8
|
+
* "…five steps from email through to information entry" made `format-boundary` covered on a screen
|
|
9
|
+
* with no text input at all, because in·format·ion contains `format`. That one word moved the
|
|
10
|
+
* screen from 7.9 FAIL to 8.9 PASS.
|
|
11
|
+
*
|
|
12
|
+
* THE RULE: anchor a word boundary at the START of the keyword, leave the END free.
|
|
13
|
+
*
|
|
14
|
+
* Not `\b…\b` at both ends, which is the obvious fix and is wrong. Every collision this defect
|
|
15
|
+
* produces is a PREFIX collision — the innocent word carries extra letters BEFORE the keyword:
|
|
16
|
+
*
|
|
17
|
+
* format ← in·format·ion, ·format·ting range ← ar·range·ment, o·range
|
|
18
|
+
* saved ← un·saved created ← re·created
|
|
19
|
+
* success ← un·success·ful length ← wave·length
|
|
20
|
+
*
|
|
21
|
+
* The `unsaved` / `unsuccessful` pair is the sharp end: the prefix INVERTS the meaning, so a
|
|
22
|
+
* scenario asserting a submit FAILED is what marks the submit-SUCCESS theme covered.
|
|
23
|
+
*
|
|
24
|
+
* Meanwhile English inflection lives entirely in the SUFFIX, and a trailing `\b` destroys it:
|
|
25
|
+
* `submit` stops matching "submitted", `success` stops matching "successfully", `valid` stops
|
|
26
|
+
* matching "validation", `create` stops matching "created". Those are true positives, and a
|
|
27
|
+
* both-ends boundary turns 4 of 5 of them into false gaps — trading a silent failure for a noisy
|
|
28
|
+
* one. A leading boundary alone kills all six collisions above and keeps all of them.
|
|
29
|
+
*
|
|
30
|
+
* WHAT THIS RULE DOES NOT FIX, stated rather than implied. A free suffix still admits a collision
|
|
31
|
+
* when the keyword is a true PREFIX of a common unrelated word:
|
|
32
|
+
*
|
|
33
|
+
* feed → "feedback" list → "listener" length → "lengthy"
|
|
34
|
+
*
|
|
35
|
+
* No boundary rule can separate `feed` from `feedback`, so the cure is a less ambiguous KEYWORD,
|
|
36
|
+
* which is a catalog decision rather than a matcher one. #778 took it: the shipped catalog no
|
|
37
|
+
* longer carries any of the three bare forms — `"product list"`, `"feed item"` and
|
|
38
|
+
* `"max length"` / `"min length"` replaced them, and `cart-correctness` lost bare `price` and
|
|
39
|
+
* `quantity` (ordinary listing vocabulary that credited a cart theme on a screen with no cart).
|
|
40
|
+
*
|
|
41
|
+
* That closes today's instances, not the class. `tests/harness/keyword-word-boundary.run.ts`
|
|
42
|
+
* section (e) sweeps every single-word ASCII keyword in the catalog — theme `keywords`,
|
|
43
|
+
* `detect_keywords` and `depth.keywords` alike — so a NEW loose keyword fails the suite instead of
|
|
44
|
+
* entering silently. Adding one is possible, but only as a conscious act via that file's
|
|
45
|
+
* `KNOWN_LOOSE` map, which #778 left empty.
|
|
46
|
+
*
|
|
47
|
+
* NON-ASCII KEYWORDS fall back to substring, on purpose. JavaScript's `\b` is defined against
|
|
48
|
+
* `[A-Za-z0-9_]`, so `\bログイン` and `\bđăng nhập` never match anything at all — the anchor would
|
|
49
|
+
* silently disable the keyword rather than tighten it. Japanese and Chinese have no word
|
|
50
|
+
* boundaries to anchor to, which is precisely where substring matching is the CORRECT behaviour
|
|
51
|
+
* and the false-positive risk is low. Today every keyword reaching here is ASCII by construction
|
|
52
|
+
* (the catalog is English; the ledger and spec sensors extract words with `[a-z]` character
|
|
53
|
+
* classes), so this branch is a guard against a future non-ASCII catalog entry failing closed.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/** True when `kw`'s first character can carry a `\b` anchor — i.e. it is an ASCII word character. */
|
|
57
|
+
const anchorable = (kw: string): boolean => /^[a-z0-9_]/i.test(kw);
|
|
58
|
+
|
|
59
|
+
const cache = new Map<string, RegExp | null>();
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Compile `kw` to its matcher, or `null` when the keyword must be compared as a plain substring.
|
|
63
|
+
* Cached: the sensors re-ask the same handful of catalog keywords once per scenario.
|
|
64
|
+
*/
|
|
65
|
+
function matcherFor(kw: string): RegExp | null {
|
|
66
|
+
const key = kw.toLowerCase();
|
|
67
|
+
if (cache.has(key)) return cache.get(key)!;
|
|
68
|
+
const re = anchorable(key)
|
|
69
|
+
? new RegExp(`\\b${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)
|
|
70
|
+
: null;
|
|
71
|
+
cache.set(key, re);
|
|
72
|
+
return re;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Does `haystack` mention `kw` as a word (not as the tail of a longer one)? */
|
|
76
|
+
export function keywordHit(haystack: string, kw: string): boolean {
|
|
77
|
+
const h = haystack.toLowerCase();
|
|
78
|
+
const re = matcherFor(kw);
|
|
79
|
+
return re ? re.test(h) : h.includes(kw.toLowerCase());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Does ANY of `haystacks` mention `kw`? */
|
|
83
|
+
export function hasKeyword(haystacks: string[], kw: string): boolean {
|
|
84
|
+
return haystacks.some((h) => keywordHit(h, kw));
|
|
85
|
+
}
|
package/src/harness/next-step.ts
CHANGED
|
@@ -170,6 +170,7 @@ const REPAIRABLE = [
|
|
|
170
170
|
'SPEC-RESTATED-UNVERIFIED', 'VIEWPOINT-GESTURE-SUBSTITUTED', 'SPEC-UNCOVERED', 'SPEC-CLAUSE-UNCOVERED',
|
|
171
171
|
'TRIGGER-UNCOVERED', 'PERMISSION-PAIR-MISSING', 'VISIBILITY-PAIR-MISSING',
|
|
172
172
|
'VIEWPOINT-ITEM-MISSING', 'MANUAL-CODE-MISSING', 'MANUAL-AUTOMATABLE', 'DEPTH-DEFERRED',
|
|
173
|
+
'UI-INVENTORY-MISSING', 'UI-INVENTORY-INCOMPLETE', 'UI-ITEM-UNCOVERED',
|
|
173
174
|
];
|
|
174
175
|
|
|
175
176
|
/**
|
package/src/harness/parse.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import { GherkinParser, ParsedScenario, ParsedStep } from '../generators/gherkin-parser';
|
|
10
|
-
import { readTextFile } from './read-text';
|
|
10
|
+
import { readTextFile, readViewpointText } from './read-text';
|
|
11
11
|
import { featureFilesFor } from './unit-paths';
|
|
12
12
|
|
|
13
13
|
export type Priority = 'high' | 'normal' | 'low' | 'unknown';
|
|
@@ -15,10 +15,49 @@ export type Priority = 'high' | 'normal' | 'low' | 'unknown';
|
|
|
15
15
|
export interface ViewpointEntry {
|
|
16
16
|
id: string; // e.g. VP-DATA-CONSISTENCY
|
|
17
17
|
priority: 'High' | 'Medium' | 'Low' | 'Unknown';
|
|
18
|
+
/** The declared priority word as written (`critical` · `high` · `medium` · `normal` · `low` ·
|
|
19
|
+
* `deferred` · `skip`, which also stands for skipped / n/a / none / not applicable), kept because `priority` folds several of them together and the
|
|
20
|
+
* PRIORITY-CONTRADICTED check needs them apart. Absent when no priority was declared. */
|
|
21
|
+
priorityWord?: string;
|
|
18
22
|
reason: string;
|
|
19
23
|
group?: 'Required' | 'Recommended' | 'Optional';
|
|
20
24
|
}
|
|
21
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Whole SECTIONS that declare something ABOUT the checklist rather than listing claims or
|
|
28
|
+
* viewpoints — known issues, traceability indexes, references, changelogs. Shared by the atomic
|
|
29
|
+
* ledger (their rows are not claims to prove, #630) and the viewpoint-overview parser (an id
|
|
30
|
+
* listed there is a reference, not a declaration, #833). `UI Patterns Identified` is the author's
|
|
31
|
+
* pick of WHICH sungen-viewpoint patterns apply — a `Skip` row there is not a claim to cover (#841).
|
|
32
|
+
* A `How to run` / `Procedure` section — that exact heading, optionally naming one id — is a manual
|
|
33
|
+
* check's steps, not claims of its own (#855); `How to test the login flow` stays a normal section.
|
|
34
|
+
*/
|
|
35
|
+
export const NON_DECLARATION_SECTION = /^(?:(?:ui patterns?(?: identified)?|(?:how to (?:run|test|reproduce|verify)|(?:manual )?procedures?)(?:\s+(?:vp|fl)-[^\s:]+)?)\s*:?$|(?:known issues?|open questions?|deferred|out of scope|viewpoint grouping|traceability|metadata|scope|journey phases?|revision history|references?|sources?|changelog)\b)/i;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Every word a viewpoint row may declare as its priority — ONE list for the overview parser, the
|
|
39
|
+
* atomic ledger and the integrity checks, which each carried their own copy and all missed the
|
|
40
|
+
* `Skip` that the `sungen add` / `add-flow` scaffolds themselves suggest (#853). `deferred` and the
|
|
41
|
+
* not-applicable words are declarations as well: such a row is not a claim to cover.
|
|
42
|
+
*/
|
|
43
|
+
export const PRIORITY_WORDS = 'critical|high|medium|normal|low|deferred|skip(?:ped)?|n\\/a|none|not applicable';
|
|
44
|
+
/** "Declared not applicable" — folded to `skip` so every consumer checks one word. */
|
|
45
|
+
const NOT_APPLICABLE = /^(?:skip(?:ped)?|n\/a|none|not applicable)$/i;
|
|
46
|
+
|
|
47
|
+
// The whole cell must BE the word (emphasis aside), as the old `| VP-… | high |` scan required:
|
|
48
|
+
// "Low risk" or "Not high" declares no priority.
|
|
49
|
+
const PRIORITY_WORD = new RegExp(`^[\\s*_\`]*(${PRIORITY_WORDS})[\\s*_\`.]*$`, 'i');
|
|
50
|
+
/** Header names of a priority column, in the languages viewpoint files are written in. */
|
|
51
|
+
const PRIORITY_HEADER = /^(?:priority|pri|level|severity|優先度?|độ ưu tiên|ưu tiên)(?:\s|\(|$)/i;
|
|
52
|
+
|
|
53
|
+
/** A priority cell → the folded level plus the word as written. */
|
|
54
|
+
function priorityOf(cell: string): { priority: ViewpointEntry['priority']; priorityWord?: string } {
|
|
55
|
+
const raw = cell.match(PRIORITY_WORD)?.[1].toLowerCase();
|
|
56
|
+
const word = raw && NOT_APPLICABLE.test(raw) ? 'skip' : raw;
|
|
57
|
+
const priority = /high/i.test(cell) ? 'High' : /medium/i.test(cell) ? 'Medium' : /low/i.test(cell) ? 'Low' : 'Unknown';
|
|
58
|
+
return word ? { priority, priorityWord: word } : { priority };
|
|
59
|
+
}
|
|
60
|
+
|
|
22
61
|
export interface ScenarioInfo {
|
|
23
62
|
name: string;
|
|
24
63
|
vpCode?: string; // VP-CART-001 style code in the scenario title
|
|
@@ -39,8 +78,10 @@ export interface ScenarioInfo {
|
|
|
39
78
|
exploration?: boolean; // @exploration — a probe outside the official suite, exempt from source tracing
|
|
40
79
|
deferredToFlow?: boolean; // @deferred:flow — owned by a flow, not automated on this screen (H6)
|
|
41
80
|
ownedByFlow?: string; // @owned-by:<flow> — the flow that owns this deferred scenario (H6)
|
|
81
|
+
featureManual?: boolean; // its Feature is tagged @manual — codegen skips the whole file (#835)
|
|
42
82
|
/** Ordered steps with their resolved bucket (And/But inherit) — flow handoff analysis (#569). */
|
|
43
|
-
|
|
83
|
+
/** `arg` = the step's data table, normalised (#817) — absent when the step has none. */
|
|
84
|
+
steps?: Array<{ bucket: 'given' | 'when' | 'then'; text: string; arg?: string }>;
|
|
44
85
|
/**
|
|
45
86
|
* The `.feature` this scenario was read from. A unit may hold several (`add --feature`), and
|
|
46
87
|
* `scenarioIdReuse` scopes its uniqueness check to ONE file — so the sensor needs to know
|
|
@@ -106,7 +147,7 @@ function headingParts(line: string): { level: number; text: string } | null {
|
|
|
106
147
|
|
|
107
148
|
export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
|
|
108
149
|
if (!fs.existsSync(filePath)) return [];
|
|
109
|
-
const text =
|
|
150
|
+
const text = readViewpointText(filePath);
|
|
110
151
|
const lines = text.split('\n');
|
|
111
152
|
|
|
112
153
|
const entries = new Map<string, ViewpointEntry>();
|
|
@@ -125,8 +166,7 @@ export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
|
|
|
125
166
|
// and an id read with its emphasis markers still attached matches no scenario tag.
|
|
126
167
|
const id = cells[0].replace(/[*`]/g, '').trim();
|
|
127
168
|
if (isViewpointId(id) && !/^-+$/.test(cells[1])) {
|
|
128
|
-
|
|
129
|
-
entries.set(id.toUpperCase(), { id: id.toUpperCase(), priority: pr as any, reason: cells[2] });
|
|
169
|
+
entries.set(id.toUpperCase(), { id: id.toUpperCase(), ...priorityOf(cells[1]), reason: cells[2] });
|
|
130
170
|
}
|
|
131
171
|
}
|
|
132
172
|
}
|
|
@@ -179,6 +219,52 @@ export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
|
|
|
179
219
|
}
|
|
180
220
|
}
|
|
181
221
|
|
|
222
|
+
// 3) ID-first tables (#833): per-axis tables such as `| ID | Priority | Input | Expected |`.
|
|
223
|
+
// A row whose FIRST cell is a viewpoint id declares that id — the same rows the
|
|
224
|
+
// PRIORITY-CONTRADICTED check always read, so the two readers now agree. Kept narrow on purpose,
|
|
225
|
+
// because a viewpoint file also holds tables that only MENTION ids:
|
|
226
|
+
// - the id must be a `VP-`/`FL-` id, or belong to a family this file already declared above
|
|
227
|
+
// (`MS-HP` in Priority Viewpoints → `MS-HP-001`); `TC-01`, `FR-001`, `ISO-27001` never are;
|
|
228
|
+
// - reference sections (Known Issues, Traceability…) are skipped WITH their subsections, the
|
|
229
|
+
// way the ledger inherits the exclusion (#657), and fenced blocks are skipped;
|
|
230
|
+
// - the header is the row directly above a `|---|` separator (structure, not guessed words);
|
|
231
|
+
// its Priority column (any position, any alias) gives the priority, else column 2 does, as
|
|
232
|
+
// the old scan read it;
|
|
233
|
+
// - `reason` stays empty: viewpoint reasons feed page-type detection, and these rows were never
|
|
234
|
+
// entries before — the page type of an untouched file must not move.
|
|
235
|
+
const declaredFamilies = [...entries.keys()];
|
|
236
|
+
const familyOk = (id: string): boolean =>
|
|
237
|
+
/^(?:VP|FL)-/i.test(id) || declaredFamilies.some((f) => id.startsWith(`${f}-`) || id === f);
|
|
238
|
+
const isSeparator = (l: string): boolean => /^\|[\s|:-]+\|?$/.test(l) && l.includes('-');
|
|
239
|
+
let excludedAt: number | null = null;
|
|
240
|
+
let inFence = false;
|
|
241
|
+
let priorityCol = 1;
|
|
242
|
+
for (let i = 0; i < lines.length; i++) {
|
|
243
|
+
const line = lines[i].trim();
|
|
244
|
+
if (/^(```|~~~)/.test(line)) { inFence = !inFence; continue; }
|
|
245
|
+
if (inFence) continue;
|
|
246
|
+
const head = headingParts(line);
|
|
247
|
+
if (head) {
|
|
248
|
+
if (excludedAt !== null && head.level > excludedAt) continue; // subsection inherits
|
|
249
|
+
const text = head.text.replace(/[*`]/g, '').trim();
|
|
250
|
+
excludedAt = /^Priority Viewpoints/i.test(text) || NON_DECLARATION_SECTION.test(text) ? head.level : null;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (excludedAt !== null || !line.startsWith('|') || isSeparator(line)) continue;
|
|
254
|
+
const cells = line.split('|').map((c) => c.trim()).filter((_, k, a) => k > 0 && k < a.length - 1);
|
|
255
|
+
if (isSeparator((lines[i + 1] ?? '').trim())) { // header row
|
|
256
|
+
const col = cells.findIndex((c) => PRIORITY_HEADER.test(c.replace(/[*`]/g, '').trim()));
|
|
257
|
+
priorityCol = col >= 0 ? col : 1;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const id = (cells[0] ?? '').replace(/[*`]/g, '').trim().toUpperCase();
|
|
261
|
+
if (!isViewpointId(id) || !familyOk(id)) continue;
|
|
262
|
+
const declared = priorityOf(cells[priorityCol] ?? '');
|
|
263
|
+
const existing = entries.get(id);
|
|
264
|
+
if (!existing) entries.set(id, { id, ...declared, reason: '' });
|
|
265
|
+
else if (!existing.priorityWord && declared.priorityWord) existing.priorityWord = declared.priorityWord;
|
|
266
|
+
}
|
|
267
|
+
|
|
182
268
|
return [...entries.values()];
|
|
183
269
|
}
|
|
184
270
|
|
|
@@ -246,22 +332,20 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
|
|
|
246
332
|
const skeletonParts: string[] = [];
|
|
247
333
|
const textParts: string[] = [sc.name];
|
|
248
334
|
const stepTextParts: string[] = [];
|
|
249
|
-
const orderedSteps: Array<{ bucket: 'given' | 'when' | 'then'; text: string }> = [];
|
|
335
|
+
const orderedSteps: Array<{ bucket: 'given' | 'when' | 'then'; text: string; arg?: string }> = [];
|
|
250
336
|
|
|
251
337
|
for (const step of sc.steps as ParsedStep[]) {
|
|
252
338
|
const kw = step.keyword.trim();
|
|
253
339
|
if (kw === 'Given' || kw === 'When' || kw === 'Then') last = kw;
|
|
254
340
|
textParts.push(step.text);
|
|
255
341
|
stepTextParts.push(step.text);
|
|
256
|
-
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
.trim()
|
|
264
|
-
.toLowerCase();
|
|
342
|
+
// The data table is part of what a step says (#817): `User see tab order:` with [FieldA]→[Cancel]
|
|
343
|
+
// and with [Save]→[Close] are different tests, and comparing step text alone called them exact
|
|
344
|
+
// duplicates and one oracle asserted N times. Kept OUT of `text`, which the claim/keyword sensors
|
|
345
|
+
// read as prose.
|
|
346
|
+
const arg = tableArg(step);
|
|
347
|
+
orderedSteps.push({ bucket: (kw === 'And' || kw === 'But' ? last : kw).toLowerCase() as 'given' | 'when' | 'then', text: step.text, ...(arg ? { arg } : {}) });
|
|
348
|
+
const skel = skeletonOf(step.text) + (arg ? ` ${skeletonOf(arg)}` : '');
|
|
265
349
|
skeletonParts.push(`${kw === 'And' || kw === 'But' ? last : kw}:${skel}`);
|
|
266
350
|
|
|
267
351
|
if (last === 'Then') {
|
|
@@ -309,9 +393,11 @@ export function loadScenarios(featurePath: string): ScenarioInfo[] {
|
|
|
309
393
|
if (!fs.existsSync(featurePath)) return [];
|
|
310
394
|
const parser = new GherkinParser();
|
|
311
395
|
const feature = parser.parseFeatureFile(featurePath);
|
|
396
|
+
// Kept apart from `manual` so the audit's accounting is unchanged; only `select` reads it.
|
|
397
|
+
const featureManual = (feature.tags || []).some((t: string) => /^@manual\b/i.test(t)) || undefined;
|
|
312
398
|
return (feature.scenarios || [])
|
|
313
399
|
.filter((s) => !s.stepsName && !s.hookType) // skip @steps/@hook blocks
|
|
314
|
-
.map((s) => ({ ...classifyScenario(withBackground(s, feature.background)), sourceFile: featurePath }));
|
|
400
|
+
.map((s) => ({ ...classifyScenario(withBackground(s, feature.background)), sourceFile: featurePath, featureManual }));
|
|
315
401
|
}
|
|
316
402
|
|
|
317
403
|
/**
|
|
@@ -334,3 +420,24 @@ export function readUnitFeatureText(unitDir: string, unit: string): string {
|
|
|
334
420
|
.map((p) => readTextFile(p))
|
|
335
421
|
.join('\n');
|
|
336
422
|
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Normalised skeleton of step text: keep [refs] (distinct targets = distinct tests), but neutralise
|
|
426
|
+
* {{vars}} and quoted values so EP/data families collapse.
|
|
427
|
+
*/
|
|
428
|
+
function skeletonOf(text: string): string {
|
|
429
|
+
return text
|
|
430
|
+
.replace(/\{\{[^}]*\}\}/g, '{}')
|
|
431
|
+
.replace(/"[^"]*"/g, '""')
|
|
432
|
+
.replace(/\s+/g, ' ')
|
|
433
|
+
.trim()
|
|
434
|
+
.toLowerCase();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** A step's data table as one comparable string — `<h1|h2 / c1|c2 / …>` — or undefined. */
|
|
438
|
+
function tableArg(step: ParsedStep): string | undefined {
|
|
439
|
+
const t = step.dataTable;
|
|
440
|
+
if (!t || (!t.headers.length && !t.rows.length)) return undefined;
|
|
441
|
+
const rows = [t.headers, ...t.rows.map((r) => r.cells)].map((cells) => cells.map((c) => c.trim()).join('|'));
|
|
442
|
+
return `<${rows.join(' / ')}>`;
|
|
443
|
+
}
|