@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
package/src/harness/perf.ts
CHANGED
|
@@ -23,10 +23,8 @@
|
|
|
23
23
|
* units:
|
|
24
24
|
* place-order: { scenario_ms: 20000 }
|
|
25
25
|
*/
|
|
26
|
-
import * as fs from 'fs';
|
|
27
26
|
import * as path from 'path';
|
|
28
|
-
import {
|
|
29
|
-
import { readTextFile } from './read-text';
|
|
27
|
+
import { readYamlConfig, warnUnreadableConfig } from './config-file';
|
|
30
28
|
|
|
31
29
|
export interface PerfConfig {
|
|
32
30
|
/** 0..100 — e.g. 75 for p75. */
|
|
@@ -51,12 +49,22 @@ export function perfConfigPath(projectRoot: string): string {
|
|
|
51
49
|
return path.join(projectRoot, 'qa', 'perf.yaml');
|
|
52
50
|
}
|
|
53
51
|
|
|
54
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Absent file → null (perf reporting is opt-in; nothing changes until configured).
|
|
54
|
+
*
|
|
55
|
+
* An UNREADABLE file is not that. Returning null for it drops the perf section out of the delivered
|
|
56
|
+
* matrix (`exporters/matrix/export.ts`) exactly as if no budgets had ever been declared — so a typo
|
|
57
|
+
* in qa/perf.yaml reads, in the deliverable, as "this project has no performance budgets". Say so.
|
|
58
|
+
*/
|
|
55
59
|
export function loadPerfConfig(projectRoot: string): PerfConfig | null {
|
|
56
60
|
const p = perfConfigPath(projectRoot);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
const read = readYamlConfig<Record<string, unknown> | null>(p);
|
|
62
|
+
if (read.state === 'absent') return null;
|
|
63
|
+
if (read.state === 'unreadable') {
|
|
64
|
+
warnUnreadableConfig(p, read.error, 'perf budgets are not applied and the perf section is omitted from the delivered matrix');
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const raw = read.value as Record<string, unknown>;
|
|
60
68
|
if (!raw || typeof raw !== 'object') return null;
|
|
61
69
|
const pctRaw = String(raw.percentile ?? 'p75').toLowerCase().replace(/^p/, '');
|
|
62
70
|
const percentile = Math.min(100, Math.max(1, Number(pctRaw) || 75));
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from 'fs';
|
|
7
7
|
import * as path from 'path';
|
|
8
|
+
import { keywordHit } from './keyword-match';
|
|
8
9
|
import { ScenarioInfo, loadScenarios, idPrefix } from './parse';
|
|
9
10
|
import { parseManualComments } from '../exporters/scenario-merger';
|
|
11
|
+
import { commentLinesOf, isPresencePlaceholderHybrid, keywordStepsOf } from '../exporters/manual-hybrid-oracle';
|
|
10
12
|
import { readTextFile } from './read-text';
|
|
11
13
|
|
|
12
14
|
// ---------- #2 Downstream-scope ----------
|
|
@@ -57,12 +59,19 @@ function downstreamRoutes(specText: string): string[] {
|
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
/**
|
|
60
|
-
* H5 — a
|
|
61
|
-
*
|
|
62
|
+
* H5 — a stateful (cart-mutating) flow with NO per-scenario isolation is flaky: scenarios share
|
|
63
|
+
* state, so cart count/quantity asserts race. Mitigations: @cleanup:cart, @isolate, a fresh
|
|
62
64
|
* browser context, or a "Given … empty cart" background. Returns true when the risk is unmitigated.
|
|
65
|
+
*
|
|
66
|
+
* #759 — this used to require a literal `@parallel`, which made the gate miss the common case.
|
|
67
|
+
* Parallel is the DEFAULT (code-generator.ts: `isParallelFeature = !isSerialFeature`); `@parallel`
|
|
68
|
+
* only NAMES that default and changes no codegen, so an untagged stateful flow is isolated exactly
|
|
69
|
+
* like a tagged one and carries exactly the same risk. The tag that actually changes the emitted
|
|
70
|
+
* suite is `@serial` — one shared ordered page, where state carry-over is the point — so that is
|
|
71
|
+
* what exempts a flow now. Same reading as serialCascadeRisk() below, which already said so.
|
|
63
72
|
*/
|
|
64
73
|
export function isolationRisk(featureText: string, stateful: boolean, collection = 'cart'): boolean {
|
|
65
|
-
if (!stateful ||
|
|
74
|
+
if (!stateful || /@serial\b/i.test(featureText)) return false;
|
|
66
75
|
// What discharges the risk is isolation of THE MUTATED COLLECTION — so the cleanup tag has
|
|
67
76
|
// to name it (`@cleanup:cart` for a cart; `@cleanup:registration` for a registration flow).
|
|
68
77
|
// A cleanup of something else (`@cleanup:overlay`) leaves the shared state shared.
|
|
@@ -149,8 +158,12 @@ export function downstreamScope(
|
|
|
149
158
|
// names the same screen under its human label (register ↔ [Registration]).
|
|
150
159
|
const labelsOf = (s: ScenarioInfo): string[] =>
|
|
151
160
|
[...s.haystack.matchAll(/\[([^\]]+)\]/g)].map((m) => m[1].split(':')[0]);
|
|
161
|
+
// The slug is the route's last segment — in practice an ordinary noun (`order`, `detail`,
|
|
162
|
+
// `login`), so it is matched as a WORD (#779). `dis·order` is not a reference to `/account/order`,
|
|
163
|
+
// and crediting it downgraded a route nothing mentions from `absent` to merely `underCovered`.
|
|
164
|
+
// The full route stays a literal: a path is an identifier, not prose.
|
|
152
165
|
const refs = scenarios.filter((s) =>
|
|
153
|
-
s.haystack
|
|
166
|
+
keywordHit(s.haystack, slug) || s.haystack.includes(route.toLowerCase()) ||
|
|
154
167
|
labelsOf(s).some((l) => sameScreenName(slug, l)));
|
|
155
168
|
if (!refs.length) {
|
|
156
169
|
// A scenario's haystack is its name + steps, so a route documented in a @manual scenario's
|
|
@@ -208,14 +221,15 @@ export function manualOracle(featureText: string): ManualOracleResult {
|
|
|
208
221
|
// Observable:/Oracle:/Expected: line (parseManualComments — the SAME parser the
|
|
209
222
|
// delivery exporter uses, so this gate can't drift from the exported cells).
|
|
210
223
|
// Prose paragraphs / rationale-only comments export EMPTY Steps + Expected.
|
|
224
|
+
// • HAS a Then whose every assertion is a bare presence placeholder, with the real oracle
|
|
225
|
+
// in the comments (#810) — the same hybrid wearing a Then (manual-hybrid-oracle.ts, the
|
|
226
|
+
// SAME predicate as compile-time SG-W008).
|
|
211
227
|
const hasGherkinSteps = /^\s*(Given|When|Then)\s/m.test(b);
|
|
228
|
+
const commentLines = commentLinesOf(b);
|
|
212
229
|
let bad: boolean;
|
|
213
230
|
if (hasGherkinSteps) {
|
|
214
|
-
bad = !/^\s*Then\s/m.test(b);
|
|
231
|
+
bad = !/^\s*Then\s/m.test(b) || isPresencePlaceholderHybrid(keywordStepsOf(b), commentLines);
|
|
215
232
|
} else {
|
|
216
|
-
const commentLines = b.split('\n')
|
|
217
|
-
.filter((l) => /^\s*#/.test(l))
|
|
218
|
-
.map((l) => l.replace(/^\s*#\s?/, ''));
|
|
219
233
|
const parsed = parseManualComments(commentLines);
|
|
220
234
|
bad = !parsed.steps.length || !parsed.expected.length;
|
|
221
235
|
}
|
|
Binary file
|
package/src/harness/read-text.ts
CHANGED
|
@@ -17,12 +17,43 @@ import * as fs from 'fs';
|
|
|
17
17
|
*
|
|
18
18
|
* A Windows-authored spec is completely normal, so every harness parser that
|
|
19
19
|
* reads a user file must go through here.
|
|
20
|
+
*
|
|
21
|
+
* A leading UTF-8 BOM is stripped for the same reason. It is what PowerShell's `Out-File` and
|
|
22
|
+
* several Windows editors write by default, and it is invisible in every editor — but it breaks a
|
|
23
|
+
* line-anchored `/^\s*Scenario:/` on line 1, and `JSON.parse` rejects it outright while npm and
|
|
24
|
+
* most other tools quietly ignore it. A file the rest of the ecosystem reads fine must not read as
|
|
25
|
+
* corrupt here.
|
|
20
26
|
*/
|
|
21
27
|
export function readTextFile(filePath: string): string {
|
|
22
|
-
return fs.readFileSync(filePath, 'utf-8').replace(/\r\n?/g, '\n');
|
|
28
|
+
return fs.readFileSync(filePath, 'utf-8').replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n');
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
/** `readTextFile` split into lines — the common case for line-anchored parsers. */
|
|
26
32
|
export function readTextLines(filePath: string): string[] {
|
|
27
33
|
return readTextFile(filePath).split('\n');
|
|
28
34
|
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Drop HTML comments (`<!-- … -->`, multi-line too) — they are author notes and scaffold samples,
|
|
38
|
+
* never content. Newlines inside a comment are kept so every later line keeps its position (#841).
|
|
39
|
+
*
|
|
40
|
+
* Only a CLOSED comment is dropped. An unclosed `<!--` (a forgotten `-->`) stays text: dropping it
|
|
41
|
+
* to the end of the file would silently delete every declaration below it. A fenced code block is
|
|
42
|
+
* content, and so is a `<!--` written inside backticks: both are how a document SHOWS the syntax.
|
|
43
|
+
*/
|
|
44
|
+
export function stripHtmlComments(text: string): string {
|
|
45
|
+
// A comment body never holds another `<!--`: an opener with no `-->` before the next opener is
|
|
46
|
+
// the unclosed one, and must not reach a LATER comment's closer and eat everything between.
|
|
47
|
+
const blank = (s: string): string => s.replace(/(?<!`)<!--(?:(?!<!--)[\s\S])*?-->/g, (m) => m.replace(/[^\n]/g, ''));
|
|
48
|
+
// split() with a capture group keeps the fences at the odd indexes.
|
|
49
|
+
return text.split(/(^```[^\n]*\n[\s\S]*?^```[^\n]*$)/m).map((part, i) => (i % 2 ? part : blank(part))).join('');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `test-viewpoint.md` as every harness sensor must read it: normalised, comments removed, and
|
|
54
|
+
* '' when the unit has none. `sungen add` scaffolds its samples inside `<!-- -->`; read raw, an
|
|
55
|
+
* untouched scaffold declared VP-UI/VP-VAL/VP-LOGIC/VP-SEC and carried 12 phantom claims (#841).
|
|
56
|
+
*/
|
|
57
|
+
export function readViewpointText(filePath: string): string {
|
|
58
|
+
return fs.existsSync(filePath) ? stripHtmlComments(readTextFile(filePath)) : '';
|
|
59
|
+
}
|
|
@@ -11,6 +11,19 @@ import * as path from 'path';
|
|
|
11
11
|
|
|
12
12
|
export interface SecretHit { file: string; line: number; reason: string }
|
|
13
13
|
|
|
14
|
+
/** A yaml sink the scanner could not read. Not a hit — the absence of one it cannot vouch for. */
|
|
15
|
+
export interface UnscannedFile { file: string; reason: string }
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The scan result, deliberately two lists rather than one.
|
|
19
|
+
*
|
|
20
|
+
* This is a security control, and its whole value is that "no hits" means "I looked". A file the
|
|
21
|
+
* scanner could not read used to be dropped by a bare `catch`, so an unread yaml and a yaml with no
|
|
22
|
+
* secrets produced the identical answer: clean. Returning `unscanned` alongside makes the caller
|
|
23
|
+
* hold both facts — what was found, and what the finding does not cover.
|
|
24
|
+
*/
|
|
25
|
+
export interface SecretScanResult { hits: SecretHit[]; unscanned: UnscannedFile[] }
|
|
26
|
+
|
|
14
27
|
// High-confidence vendor token prefixes (very low false-positive rate).
|
|
15
28
|
const VENDOR_PREFIXES = /\b(figd_[A-Za-z0-9_-]{12,}|ghp_[A-Za-z0-9]{20,}|gho_[A-Za-z0-9]{20,}|glpat-[A-Za-z0-9_-]{16,}|sk-[A-Za-z0-9]{16,}|AKIA[0-9A-Z]{12,}|xox[baprs]-[A-Za-z0-9-]{10,}|AIza[0-9A-Za-z_-]{30,})/;
|
|
16
29
|
// A secret-named key assigned a long, non-placeholder value.
|
|
@@ -51,16 +64,59 @@ function scanText(text: string, file: string): SecretHit[] {
|
|
|
51
64
|
/** Scan a unit dir's committed yaml sinks for likely real secrets: test-data/*.yaml,
|
|
52
65
|
* mock/*.yaml (mock response bodies are a credential magnet — pasted DevTools
|
|
53
66
|
* responses carry real tokens), and api/*.yaml (endpoint catalogs carry headers). */
|
|
54
|
-
export function scanTestDataSecrets(baseDir: string):
|
|
67
|
+
export function scanTestDataSecrets(baseDir: string): SecretScanResult {
|
|
55
68
|
const hits: SecretHit[] = [];
|
|
69
|
+
const unscanned: UnscannedFile[] = [];
|
|
70
|
+
const why = (e: unknown): string => (e instanceof Error ? e.message.split('\n')[0] : String(e));
|
|
71
|
+
|
|
56
72
|
for (const sub of ['test-data', 'mock', 'api']) {
|
|
57
73
|
const dir = path.join(baseDir, sub);
|
|
58
74
|
if (!fs.existsSync(dir)) continue;
|
|
59
|
-
|
|
75
|
+
|
|
76
|
+
// A directory that cannot be LISTED is a directory that cannot be scanned. This used to throw
|
|
77
|
+
// out of the scanner and abort the whole `generate`/`delivery` run; report it in the same place
|
|
78
|
+
// a hit would appear instead, so the operator learns what the clean verdict does not cover
|
|
79
|
+
// rather than losing the deliverable to an unrelated permission fault.
|
|
80
|
+
let entries: string[];
|
|
81
|
+
try { entries = fs.readdirSync(dir); } catch (e) { unscanned.push({ file: sub, reason: why(e) }); continue; }
|
|
82
|
+
|
|
83
|
+
for (const f of entries) {
|
|
60
84
|
if (!/\.ya?ml$/i.test(f)) continue;
|
|
61
85
|
const p = path.join(dir, f);
|
|
62
|
-
|
|
86
|
+
const rel = path.join(sub, f);
|
|
87
|
+
let text: string;
|
|
88
|
+
// `readdirSync` listed this file, so it exists as a name. A read that fails here means a
|
|
89
|
+
// restricted mount, a dangling symlink, or a file removed between the two calls — never
|
|
90
|
+
// "there was nothing to read". Silently skipping it let a committed credential ship under an
|
|
91
|
+
// explicit "clean" verdict.
|
|
92
|
+
try { text = fs.readFileSync(p, 'utf-8'); } catch (e) { unscanned.push({ file: rel, reason: why(e) }); continue; }
|
|
93
|
+
hits.push(...scanText(text, rel));
|
|
63
94
|
}
|
|
64
95
|
}
|
|
65
|
-
return hits;
|
|
96
|
+
return { hits, unscanned };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The operator-facing lines for a scan result — one renderer, because `generate` and `delivery`
|
|
101
|
+
* had grown two copies of it and only one of them would have learned about `unscanned`.
|
|
102
|
+
*
|
|
103
|
+
* Empty array when there is nothing to say. The unscanned block is NOT folded into the hit block:
|
|
104
|
+
* "possible secret" and "not examined" are different claims, and printing the second under the
|
|
105
|
+
* first's heading invites it to be dismissed as another false positive.
|
|
106
|
+
*/
|
|
107
|
+
export function formatSecretScan(result: SecretScanResult, limit = 10): string[] {
|
|
108
|
+
const out: string[] = [];
|
|
109
|
+
if (result.hits.length) {
|
|
110
|
+
out.push('', '⚠️ Possible secret(s) in committed test-data (review — do NOT commit real credentials):');
|
|
111
|
+
for (const h of result.hits.slice(0, limit)) out.push(` ${h.file}:${h.line} — ${h.reason}`);
|
|
112
|
+
if (result.hits.length > limit) out.push(` … and ${result.hits.length - limit} more`);
|
|
113
|
+
out.push(' Move real secrets to an env overlay / CI secret; keep test-data placeholders only.');
|
|
114
|
+
}
|
|
115
|
+
if (result.unscanned.length) {
|
|
116
|
+
out.push('', `⚠️ Secret scan could NOT read ${result.unscanned.length} committed yaml file(s) — they are NOT covered by the result above:`);
|
|
117
|
+
for (const u of result.unscanned.slice(0, limit)) out.push(` ${u.file} — ${u.reason}`);
|
|
118
|
+
if (result.unscanned.length > limit) out.push(` … and ${result.unscanned.length - limit} more`);
|
|
119
|
+
out.push(' Make them readable and re-run before treating this unit as scanned.');
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
66
122
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Select scenarios by test-case ID — backs `sungen select` and `run-test --ids` (#835).
|
|
3
|
+
*
|
|
4
|
+
* An ID means what it means everywhere else in the harness: `vpId`, the leading ID token of the
|
|
5
|
+
* scenario title (`VP-UI-001`, `MS-HP-001`, `VP0-001`). Matching is exact and case-insensitive —
|
|
6
|
+
* `VP-UI-001` never selects `VP-UI-0010` or `VP-UI-001-B` — and every ID that selects nothing
|
|
7
|
+
* runnable is reported, because the alternative is a 0-test run that reports green.
|
|
8
|
+
*/
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { ScenarioInfo } from './parse';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Escape scenario titles into one `-g` / `--mochaOpts.grep` regex. Full titles, not bare IDs:
|
|
14
|
+
* a title starts with its own ID, so it can only appear inside its own test's title — and a
|
|
15
|
+
* `@cases` row test (`<title> — <row label>`) still contains it.
|
|
16
|
+
*/
|
|
17
|
+
export function titlesToGrep(titles: string[]): string {
|
|
18
|
+
return titles.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface IdSelectionGroup {
|
|
22
|
+
/** Feature basename (`products`, `home-modal`) — names the spec and the result file. */
|
|
23
|
+
feature: string;
|
|
24
|
+
ids: string[];
|
|
25
|
+
titles: string[];
|
|
26
|
+
grep: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface IdSelection {
|
|
30
|
+
features: IdSelectionGroup[];
|
|
31
|
+
/** IDs no scenario carries, with near matches (`VP-VAL-001` → `VP-VAL-001-B`). */
|
|
32
|
+
unknown: Array<{ id: string; didYouMean: string[] }>;
|
|
33
|
+
/** IDs whose every scenario is manual, owned by a flow or waiting on a capability — nothing to run. */
|
|
34
|
+
notRunnable: Array<{ id: string; reason: string }>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** `"VP-UI-001, vp-ui-002 VP-UI-001"` → `['VP-UI-001', 'VP-UI-002']`. */
|
|
38
|
+
export function parseIdList(raw: string): string[] {
|
|
39
|
+
return [...new Set(raw.split(/[\s,]+/).map((s) => s.trim().toUpperCase()).filter(Boolean))];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** `VP-C-002.` → `VP-C-002`: the title token may carry trailing punctuation the user never types. */
|
|
43
|
+
const idOf = (s: ScenarioInfo): string | undefined => s.vpId?.replace(/[.-]+$/, '') || undefined;
|
|
44
|
+
|
|
45
|
+
function notRunnableReason(s: ScenarioInfo, enabledCaps: Set<string>): string | null {
|
|
46
|
+
if (s.deferredToFlow) return `owned by a flow${s.ownedByFlow ? ` (${s.ownedByFlow})` : ''} — run it there`;
|
|
47
|
+
if (s.featureManual) return 'its Feature is @manual — no spec is generated for it';
|
|
48
|
+
if (s.manual) return 'manual — no automated test exists';
|
|
49
|
+
// Compiled as `test.skip` while the capability is off: the run would report a skip, not a result.
|
|
50
|
+
const missing = (s.requiresCaps ?? []).filter((c) => !enabledCaps.has(c));
|
|
51
|
+
if (missing.length) return `needs capability ${missing.join(', ')} — compiled as a skip until \`sungen capability add ${missing.join(' ')}\``;
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function selectByIds(scenarios: ScenarioInfo[], ids: string[], unit: string, enabledCaps: Set<string> = new Set()): IdSelection {
|
|
56
|
+
const out: IdSelection = { features: [], unknown: [], notRunnable: [] };
|
|
57
|
+
const groups = new Map<string, IdSelectionGroup>();
|
|
58
|
+
const allIds = [...new Set(scenarios.map(idOf).filter((v): v is string => !!v))];
|
|
59
|
+
|
|
60
|
+
for (const id of ids) {
|
|
61
|
+
const hits = scenarios.filter((s) => idOf(s) === id);
|
|
62
|
+
if (hits.length === 0) {
|
|
63
|
+
const didYouMean = allIds.filter((v) => v.startsWith(`${id}-`) || id.startsWith(`${v}-`)).slice(0, 5);
|
|
64
|
+
out.unknown.push({ id, didYouMean });
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const runnable = hits.filter((s) => !notRunnableReason(s, enabledCaps));
|
|
68
|
+
if (runnable.length === 0) {
|
|
69
|
+
out.notRunnable.push({ id, reason: notRunnableReason(hits[0], enabledCaps)! });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
for (const s of runnable) {
|
|
73
|
+
const feature = s.sourceFile ? path.basename(s.sourceFile, '.feature') : unit;
|
|
74
|
+
const g = groups.get(feature) ?? { feature, ids: [], titles: [], grep: '' };
|
|
75
|
+
if (!g.ids.includes(id)) g.ids.push(id);
|
|
76
|
+
if (!g.titles.includes(s.name)) g.titles.push(s.name);
|
|
77
|
+
groups.set(feature, g);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const g of groups.values()) g.grep = titlesToGrep(g.titles);
|
|
81
|
+
out.features = [...groups.values()];
|
|
82
|
+
return out;
|
|
83
|
+
}
|
package/src/harness/sensors.ts
CHANGED
|
@@ -11,6 +11,7 @@ import * as path from 'path';
|
|
|
11
11
|
import { parse as parseYaml } from 'yaml';
|
|
12
12
|
import { ScenarioInfo, ViewpointEntry, idPrefix } from './parse';
|
|
13
13
|
import { readTextFile } from './read-text';
|
|
14
|
+
import { keywordHit, hasKeyword } from './keyword-match';
|
|
14
15
|
|
|
15
16
|
// Business-critical category keywords (matched by CONTAINMENT against the VP category, so a
|
|
16
17
|
// compound category like LIST-DISPLAY / ADD-TO-CART / PRODUCT-DISCOVERY classifies correctly).
|
|
@@ -86,18 +87,11 @@ export function loadCatalog(): Catalog {
|
|
|
86
87
|
return parseYaml(readTextFile(p)) as Catalog;
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
// Word-aware keyword match
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
const
|
|
94
|
-
const esc = kw.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
95
|
-
return new RegExp(`\\b${esc}(?:s|es|ed|ing)?\\b`);
|
|
96
|
-
};
|
|
97
|
-
const has = (haystacks: string[], kw: string) => {
|
|
98
|
-
const re = wordRe(kw);
|
|
99
|
-
return haystacks.some((h) => re.test(h));
|
|
100
|
-
};
|
|
90
|
+
// Word-aware keyword match — one rule for every sensor, in ./keyword-match (#776). A keyword
|
|
91
|
+
// matches at a word START but may carry any suffix, so `form` does not match inside `Format` or
|
|
92
|
+
// `@platform` while `submit` still matches `submitted`. The old local copy anchored BOTH ends,
|
|
93
|
+
// which silently dropped every inflected true positive; see that module for the measurements.
|
|
94
|
+
const has = hasKeyword;
|
|
101
95
|
|
|
102
96
|
// ---------- Sensor 1: Viewpoint Gate (catalog-driven) ----------
|
|
103
97
|
|
|
@@ -112,6 +106,13 @@ export interface GateResult {
|
|
|
112
106
|
coverageRatio: number;
|
|
113
107
|
gaps: { theme: string; keywords: string[]; status: 'missing' | 'shallow' }[];
|
|
114
108
|
universalGaps: string[];
|
|
109
|
+
/**
|
|
110
|
+
* WHICH scenario covered each satisfied theme, and on which keyword — the audit's second line of
|
|
111
|
+
* defence against a bogus coverage claim (#776). A bare count ("themes 3/3") gives a reviewer
|
|
112
|
+
* nothing to check, so a nonsense match survives; naming the evidence makes it obvious on sight.
|
|
113
|
+
* Mirrors how the Claim-Proof section already names the scenario behind each claim.
|
|
114
|
+
*/
|
|
115
|
+
coveredBy: { theme: string; by: string; keyword: string }[];
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
/**
|
|
@@ -166,20 +167,36 @@ export function viewpointGate(scenarios: ScenarioInfo[], viewpoints: ViewpointEn
|
|
|
166
167
|
|
|
167
168
|
const evidence = { hits: best, runnerUp };
|
|
168
169
|
const gaps: GateResult['gaps'] = [];
|
|
170
|
+
const coveredBy: GateResult['coveredBy'] = [];
|
|
169
171
|
let total = 0, covered = 0;
|
|
170
172
|
if (pageType) {
|
|
171
173
|
for (const t of catalog.page_types[pageType].must_cover) {
|
|
172
174
|
total++;
|
|
173
175
|
// Scenarios that cover this theme by keyword (include @manual — a manual
|
|
174
176
|
// scenario with a real assertion still covers the viewpoint for design).
|
|
175
|
-
|
|
177
|
+
// Word-aware since #776: a substring match let `information` satisfy `format-boundary`.
|
|
178
|
+
const coverers = scenarios.filter((s) => t.keywords.some((k) => keywordHit(s.haystack, k)));
|
|
176
179
|
if (coverers.length === 0) { gaps.push({ theme: t.theme, keywords: t.keywords, status: 'missing' }); continue; }
|
|
177
180
|
// A theme WITHOUT `depth:` is visibility/navigation — landing/seeing/leaving IS the
|
|
178
181
|
// assertion, so mere presence covers it. Only DATA-correctness themes (depth.requires:
|
|
179
182
|
// data-assertion) need a data assertion to count as deeply covered.
|
|
180
183
|
const needsDepth = t.depth?.requires === 'data-assertion';
|
|
181
|
-
|
|
182
|
-
|
|
184
|
+
// Name the evidence for whichever scenario actually carries the theme — the one with the
|
|
185
|
+
// data assertion when depth is required, since that is the scenario the credit rests on.
|
|
186
|
+
const proof = (needsDepth ? coverers.find((s) => s.hasDataAssertion) : undefined) ?? coverers[0];
|
|
187
|
+
const credit = (): void => {
|
|
188
|
+
covered++;
|
|
189
|
+
coveredBy.push({
|
|
190
|
+
theme: t.theme,
|
|
191
|
+
by: proof.haystack,
|
|
192
|
+
// `proof` came out of `coverers`, which was filtered on this same predicate, so the find
|
|
193
|
+
// always lands — the `!` states that rather than inventing a fallback keyword that would
|
|
194
|
+
// misattribute the credit if the invariant ever broke.
|
|
195
|
+
keyword: t.keywords.find((k) => keywordHit(proof.haystack, k))!,
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
if (!needsDepth) { credit(); continue; }
|
|
199
|
+
if (coverers.some((s) => s.hasDataAssertion)) credit();
|
|
183
200
|
else gaps.push({ theme: t.theme, keywords: t.keywords, status: 'shallow' });
|
|
184
201
|
}
|
|
185
202
|
}
|
|
@@ -203,6 +220,7 @@ export function viewpointGate(scenarios: ScenarioInfo[], viewpoints: ViewpointEn
|
|
|
203
220
|
coverageRatio: total ? covered / total : 1,
|
|
204
221
|
gaps,
|
|
205
222
|
universalGaps,
|
|
223
|
+
coveredBy,
|
|
206
224
|
};
|
|
207
225
|
}
|
|
208
226
|
|
|
@@ -269,7 +287,7 @@ export function assertionDepth(
|
|
|
269
287
|
const isDismissBehavior = (s: ScenarioInfo) =>
|
|
270
288
|
/\bis hidden\b|\bare hidden\b|\bdismiss|\bdisappear|\bno longer (visible|shown|present|displayed)\b|\bcloses? the (modal|dialog|popup|overlay|panel|menu)\b/.test(s.haystack);
|
|
271
289
|
const isDepthRequired = (s: ScenarioInfo) =>
|
|
272
|
-
s.category !== 'NAV' && !isDismissBehavior(s) && depthKeywords.some((k) => s.haystack
|
|
290
|
+
s.category !== 'NAV' && !isDismissBehavior(s) && depthKeywords.some((k) => keywordHit(s.haystack, k));
|
|
273
291
|
|
|
274
292
|
const required = nonManual.filter(isDepthRequired);
|
|
275
293
|
const reqShallow = required.filter((s) => s.shallow);
|
|
@@ -347,7 +365,7 @@ export function flowCoveredThemes(
|
|
|
347
365
|
): { theme: string; flow: string }[] {
|
|
348
366
|
const out: { theme: string; flow: string }[] = [];
|
|
349
367
|
for (const g of gaps) {
|
|
350
|
-
const hit = flowScenarios.find((s) => s.deep && g.keywords.some((k) => s.haystack
|
|
368
|
+
const hit = flowScenarios.find((s) => s.deep && g.keywords.some((k) => keywordHit(s.haystack, k)));
|
|
351
369
|
if (hit) out.push({ theme: g.theme, flow: hit.flow });
|
|
352
370
|
}
|
|
353
371
|
return out;
|
|
@@ -394,7 +412,8 @@ export function flowRegressionDepth(scenarios: ScenarioInfo[]): FlowDepthResult
|
|
|
394
412
|
export interface OracleStrengthResult {
|
|
395
413
|
weak: { name: string; hint: string; code?: string }[]; // facet-by-name-substring, or URL-only (code ORACLE-URL-ONLY)
|
|
396
414
|
facetClaims: number; // scenarios that touch a category/brand facet (denominator)
|
|
397
|
-
|
|
415
|
+
weakFacet: number; // the facet-class subset of `weak` — the only numerator `facetClaims` denominates
|
|
416
|
+
ratio: number; // 1 - weakFacet/facetClaims, clamped 0..1 (1 when no facet claim) — caps businessDepth
|
|
398
417
|
}
|
|
399
418
|
|
|
400
419
|
// "see all [<item name/title>] ... contain(s) {{<facet>.term}}" — asserting every item's NAME carries
|
|
@@ -429,9 +448,11 @@ export function oracleStrength(scenarios: ScenarioInfo[]): OracleStrengthResult
|
|
|
429
448
|
hint: `every \`Then\` is a URL assertion${guardLike ? ' on a permission / guard scenario' : ''} — it proves the router pushed a path, not that the page rendered for this user${guardLike ? ', and this is exactly the scenario a late 403 redirect turns falsely green' : ''}. Give the page selector an \`anchor:\` (a heading/landmark only the rendered page has) — one line per screen, and every \`see [X] page\` on it becomes a content oracle — or add a content assertion after it.`,
|
|
430
449
|
});
|
|
431
450
|
}
|
|
451
|
+
let weakFacet = 0;
|
|
432
452
|
for (const s of scenarios) {
|
|
433
453
|
if (s.manual) continue; // a @manual facet check is a deliberate deferral, not a weak automated oracle
|
|
434
454
|
if (WEAK_FACET_ORACLE.test(s.stepsText)) {
|
|
455
|
+
weakFacet++;
|
|
435
456
|
weak.push({
|
|
436
457
|
name: s.name.slice(0, 80),
|
|
437
458
|
hint: 'asserting every item NAME contains a category/brand term does not prove facet membership — assert the results-page TITLE/header carries the facet, a detail-page facet field, or an API/DB oracle; or defer the exhaustive check to @manual:M2.',
|
|
@@ -439,7 +460,17 @@ export function oracleStrength(scenarios: ScenarioInfo[]): OracleStrengthResult
|
|
|
439
460
|
}
|
|
440
461
|
}
|
|
441
462
|
const facetClaims = scenarios.filter((s) => FACET_REF.test(s.stepsText) || FACET_REF.test(s.name)).length;
|
|
442
|
-
|
|
463
|
+
// Only the FACET class is denominated by `facetClaims`. `weak` also carries ORACLE-URL-ONLY
|
|
464
|
+
// findings (#662), which make no category/brand claim at all — averaging them into this axis
|
|
465
|
+
// drove `ratio` negative (5 URL-only over 2 facet claims → 1 - 5/2 = -1.5), which walked through
|
|
466
|
+
// the businessDepth floor and took the CLI's bar renderer down with it (#762). A URL-only oracle
|
|
467
|
+
// is NAMED as a finding and holds the score out of the senior band (`seniorBandedOverall`);
|
|
468
|
+
// it is not averaged into the facet axis — the invariant `url-only-oracle.run.ts` asserts.
|
|
469
|
+
// The clamp is not belt-and-braces: a weak-facet scenario need not itself satisfy FACET_REF
|
|
470
|
+
// (`see all [name] contains {{price.term}}` is weak but claims no facet), so `weakFacet` can
|
|
471
|
+
// still outrun `facetClaims`. This axis is declared 0..1; it leaves here inside 0..1.
|
|
472
|
+
const ratio = facetClaims ? Math.max(0, Math.min(1, 1 - weakFacet / facetClaims)) : 1;
|
|
473
|
+
return { weak, facetClaims, weakFacet, ratio };
|
|
443
474
|
}
|
|
444
475
|
|
|
445
476
|
/** Collect data-correctness themes (depth.requires) for a page-type + universal. */
|
|
@@ -603,6 +634,9 @@ export interface TraceResult {
|
|
|
603
634
|
mappedToOverview: number;
|
|
604
635
|
withVpCodeRatio: number;
|
|
605
636
|
mappedRatio: number;
|
|
637
|
+
/** Viewpoint IDs read from test-viewpoint.md. 0 = nothing to map to, so `mappedRatio` is not a
|
|
638
|
+
* measurement — the scenario IDs were never checked (#832). */
|
|
639
|
+
overviewIds: number;
|
|
606
640
|
note: string;
|
|
607
641
|
}
|
|
608
642
|
|
|
@@ -614,6 +648,24 @@ export interface TraceResult {
|
|
|
614
648
|
*/
|
|
615
649
|
const unprefixed = (id: string): string => id.replace(/^(?:VP|FL)-/i, '').toUpperCase();
|
|
616
650
|
|
|
651
|
+
/**
|
|
652
|
+
* The traceability note, worded from the SAME ratio the audit's TRACE finding fires on —
|
|
653
|
+
* mapped / ALL scenarios (#852). It used to compare mapped against scenarios WITH an id, so 0 of
|
|
654
|
+
* 0 (no scenario yet, or none tagged) and 2 of 2 tagged out of 10 all read "Traceable." while
|
|
655
|
+
* the audit reported `TRACE: Traceable.` for a 0–20% mapping.
|
|
656
|
+
*/
|
|
657
|
+
function traceNote(total: number, withCode: number, mapped: number, declared: number, exampleId?: string): string {
|
|
658
|
+
if (declared === 0) {
|
|
659
|
+
return 'No viewpoint ID found in test-viewpoint.md — the file is missing, or none of its sections declares one — so the scenario IDs were not checked. Declarations are read from a `## Priority Viewpoints` table, `### Required / Recommended / Optional` lists, and tables whose first column is a `VP-`/`FL-` ID or an ID of a family declared there (Known Issues / Traceability / References sections are skipped).';
|
|
660
|
+
}
|
|
661
|
+
if (total === 0) return 'No scenarios yet — traceability is measured once the feature has scenarios.';
|
|
662
|
+
if (mapped >= total * 0.5) return 'Traceable.';
|
|
663
|
+
const example = exampleId ? ` (e.g. \`${exampleId}-001 …\`)` : '';
|
|
664
|
+
if (withCode === 0) return `No scenario carries a viewpoint ID — start each title with one of the IDs declared in test-viewpoint.md${example}.`;
|
|
665
|
+
if (mapped < withCode * 0.5) return 'Scenario IDs do not match the viewpoint-overview ids (weak traceability — re-tag to the project viewpoint IDs).';
|
|
666
|
+
return `Only ${mapped} of ${total} scenarios carry a declared viewpoint ID — ${total - withCode} have no ID at all; tag them with the project's IDs${example}.`;
|
|
667
|
+
}
|
|
668
|
+
|
|
617
669
|
export function traceability(scenarios: ScenarioInfo[], viewpoints: ViewpointEntry[]): TraceResult {
|
|
618
670
|
const overviewIds = new Set(viewpoints.map((v) => v.id.toUpperCase()));
|
|
619
671
|
const overviewBare = new Set(viewpoints.map((v) => unprefixed(v.id)));
|
|
@@ -638,9 +690,10 @@ export function traceability(scenarios: ScenarioInfo[], viewpoints: ViewpointEnt
|
|
|
638
690
|
mappedToOverview: mapped.length,
|
|
639
691
|
withVpCodeRatio: scenarios.length ? withCode.length / scenarios.length : 0,
|
|
640
692
|
mappedRatio: scenarios.length ? mapped.length / scenarios.length : 0,
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
693
|
+
overviewIds: overviewIds.size,
|
|
694
|
+
// With no ID read there is nothing to match against: telling the author to re-tag sends them
|
|
695
|
+
// to change scenarios that may be right (#832). Name the file and the sections read instead.
|
|
696
|
+
note: traceNote(scenarios.length, withCode.length, mapped.length, overviewIds.size, viewpoints[0]?.id),
|
|
644
697
|
};
|
|
645
698
|
}
|
|
646
699
|
|
|
@@ -702,6 +755,31 @@ const CLAIM_RULES: ClaimRule[] = [
|
|
|
702
755
|
hint: 'swap `contains {{v}}` for `with {{v}}` on the continuity rows. A substring match cannot distinguish "unchanged" from "changed by adding a prefix" — and if the screen genuinely renders extra text around the value, say so in the title instead of claiming exactness.',
|
|
703
756
|
severity: 'warn',
|
|
704
757
|
},
|
|
758
|
+
{
|
|
759
|
+
// An ORDER claim ("sorted by date descending", "ties broken by last-updated") is invisible to
|
|
760
|
+
// a presence check: `see [List] table` passes whatever order the rows are in. It sits ahead of
|
|
761
|
+
// the generic rules so "all results sorted …" is judged as an ordering claim, not an all-claim.
|
|
762
|
+
// A tiebreaker is a Tier-1 guard in the tc-generation skill, yet no rule recognised it (#810).
|
|
763
|
+
claim: 'ordering/sort',
|
|
764
|
+
title: /\b(ascending|descending|sorted|sorts|sort order|ordered by|order by|in (?:chronological|alphabetical|reverse|numeric) order|(?:newest|oldest|latest|earliest|highest|lowest) first|tie-?break(?:s|er|ers)?|ties?)\b|降順|昇順|並び順|giảm dần|tăng dần|sắp xếp/,
|
|
765
|
+
proof: /match data|is sorted|sorted (?:ascending|descending)|\brow \d+\b|\.first\.|\.last\.|\[\d+\]\.|order by/,
|
|
766
|
+
need: 'an order-sensitive assertion (`table match data:` rows in the expected order, `is sorted ascending|descending`, or positional `row N` checks)',
|
|
767
|
+
hint: 'a presence check passes in any order — assert the rows in their expected sequence (`User see [T] table match data:`), or the column sort state; a tiebreak needs two rows sharing the primary key.',
|
|
768
|
+
severity: 'fail',
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
// A LIMIT claim ("never exceeds 10", "at most 5 files") is a count claim in disguise.
|
|
772
|
+
// Ahead of 'no-side-effect', whose negation+effect pattern would otherwise swallow
|
|
773
|
+
// "never exceeds 10 records" and ask for a duplicate-count proof.
|
|
774
|
+
claim: 'limit/cap',
|
|
775
|
+
// The quantity must be ≥2 ("at most ONE order" is a no-duplicate claim — left to the rules
|
|
776
|
+
// below) and not a duration ("up to 30 seconds" is a timing claim, not a cap).
|
|
777
|
+
title: /\b(?:never exceeds?|(?:does not|doesn['’]t|not) exceed|at most|no more than|up to|capped at|caps? at|maximum of|max(?:imum)?)\s+(?:\d{2,}|[2-9]|two|three|four|five|six|seven|eight|nine|ten)\b(?!\s*(?:ms|milliseconds?|s|secs?|seconds?|mins?|minutes?|hours?|days?)\b)|\b(?:never exceeds?|(?:does not|doesn['’]t) exceed) (?:the )?(?:limit|cap|quota|maximum)\b|以内|上限|tối đa/,
|
|
778
|
+
proof: /\bcount\b|tohavecount|table with|row with \{\{|match data|is at most|\bexactly\b|field with \{\{|\berror\b|invalid|is disabled/,
|
|
779
|
+
need: 'a count/boundary assertion (count is N, `is at most`, the N+1th input rejected or truncated)',
|
|
780
|
+
hint: 'assert the boundary itself — the count at the cap, or the value/error when one more is attempted — not just that the list or field is present.',
|
|
781
|
+
severity: 'fail',
|
|
782
|
+
},
|
|
705
783
|
{
|
|
706
784
|
claim: 'all/every/each',
|
|
707
785
|
title: /\b(all|every|each)\b/,
|
|
@@ -994,14 +1072,16 @@ export function numericClaimGaps(scenarios: ScenarioInfo[]): Array<{ name: strin
|
|
|
994
1072
|
* The Priority-Viewpoints table is where release selection is supposed to come from. When the
|
|
995
1073
|
* table says a flow is `Low` and its scenario is tagged `@high`, a smoke run picks up work the QA
|
|
996
1074
|
* deliberately deprioritised — and nobody can tell which of the two was the decision (#622).
|
|
1075
|
+
*
|
|
1076
|
+
* Reads the SAME parse traceability does (`parseViewpointOverview`). It used to scan the raw text
|
|
1077
|
+
* for any `| VP-… | high |` row, so one audit run "saw" an id for this check and not for
|
|
1078
|
+
* traceability (#833), and a row in a Known Issues table could raise a contradiction.
|
|
997
1079
|
*/
|
|
998
1080
|
export function priorityMismatches(
|
|
999
|
-
|
|
1081
|
+
viewpoints: ViewpointEntry[], scenarios: ScenarioInfo[],
|
|
1000
1082
|
): Array<{ id: string; declared: string; tagged: string; scenario: string }> {
|
|
1001
1083
|
const declared = new Map<string, string>();
|
|
1002
|
-
for (const
|
|
1003
|
-
declared.set(m[1].toUpperCase(), m[2].toLowerCase());
|
|
1004
|
-
}
|
|
1084
|
+
for (const v of viewpoints) if (v.priorityWord) declared.set(v.id.toUpperCase(), v.priorityWord);
|
|
1005
1085
|
if (declared.size === 0) return [];
|
|
1006
1086
|
// `medium` and `normal` are the same rank under different names in real documents.
|
|
1007
1087
|
const rank = (p: string): string => (p === 'medium' ? 'normal' : p);
|
|
@@ -1014,8 +1094,9 @@ export function priorityMismatches(
|
|
|
1014
1094
|
if (!key) continue;
|
|
1015
1095
|
const want = rank(declared.get(key)!);
|
|
1016
1096
|
const got = rank(String(s.priority).toLowerCase());
|
|
1017
|
-
// `critical` and `
|
|
1018
|
-
|
|
1097
|
+
// `critical`, `deferred` and `skip` (declared not applicable, #853) have no tag equivalent —
|
|
1098
|
+
// nothing to contradict.
|
|
1099
|
+
if (want === 'critical' || want === 'deferred' || want === 'skip') continue;
|
|
1019
1100
|
if (want !== got) out.push({ id: key, declared: declared.get(key)!, tagged: got, scenario: s.name });
|
|
1020
1101
|
}
|
|
1021
1102
|
return out;
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* explicit — a named heading, a table that declares conditions and outcomes, or an if/else
|
|
17
17
|
* cascade — because a false "you missed a branch" costs more trust than it buys coverage.
|
|
18
18
|
*/
|
|
19
|
+
import { keywordHit } from './keyword-match';
|
|
19
20
|
import { ScenarioInfo } from './parse';
|
|
20
21
|
|
|
21
22
|
export interface SpecBranch {
|
|
@@ -172,7 +173,9 @@ export function unenumeratedBranches(branches: SpecBranch[], featureText: string
|
|
|
172
173
|
const words = [...new Set((b.label.toLowerCase().match(/[a-z][a-z-]{3,}/g) ?? []))]
|
|
173
174
|
.filter((w) => !GENERIC_WORD.has(w));
|
|
174
175
|
if (words.length === 0) return !hay.includes(b.label.toLowerCase());
|
|
175
|
-
|
|
176
|
+
// Word-aware (#779): `in·active` and `check·list` are not mentions of `active` and `list`,
|
|
177
|
+
// and two of them credited a branch no scenario cites.
|
|
178
|
+
const hits = words.filter((w) => keywordHit(hay, w)).length;
|
|
176
179
|
return hits < Math.min(2, words.length);
|
|
177
180
|
});
|
|
178
181
|
}
|
|
@@ -191,7 +194,7 @@ export function sameOracleClusters(scenarios: ScenarioInfo[]): Array<{ oracle: s
|
|
|
191
194
|
if (s.manual) continue; // a procedure's oracle is prose
|
|
192
195
|
const asserts = [...(s.steps ?? [])]
|
|
193
196
|
.filter((st) => st.bucket === 'then')
|
|
194
|
-
.map((st) => st.text.toLowerCase().replace(/\s+/g, ' ').trim())
|
|
197
|
+
.map((st) => `${st.text} ${st.arg ?? ''}`.toLowerCase().replace(/\s+/g, ' ').trim()) // the table is the oracle too (#817)
|
|
195
198
|
.sort();
|
|
196
199
|
if (asserts.length === 0) continue;
|
|
197
200
|
const key = asserts.join(' ; ');
|
|
@@ -289,7 +292,7 @@ export function visibilityPairGaps(scenarios: ScenarioInfo[]): VisibilityPairGap
|
|
|
289
292
|
const subject = distinctive(s);
|
|
290
293
|
// The counterpart has to be about the SAME thing — two shared distinctive words, the same
|
|
291
294
|
// bar the ledger's prose matching uses.
|
|
292
|
-
const paired = allows.some((a) => subject.filter((w) => a.haystack
|
|
295
|
+
const paired = allows.some((a) => subject.filter((w) => keywordHit(a.haystack, w)).length >= Math.min(2, subject.length));
|
|
293
296
|
if (!paired) out.push({ scenario: s.name, subject: subject.slice(0, 4), kind: 'permission' });
|
|
294
297
|
}
|
|
295
298
|
return out;
|