@sun-asterisk/sungen 3.2.31 → 3.2.33
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 +1 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +13 -2
- 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 +46 -17
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +39 -18
- 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/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +2 -0
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/results-merge.d.ts.map +1 -1
- package/dist/cli/commands/results-merge.js +2 -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/setup-mobile-env.d.ts +15 -0
- package/dist/cli/commands/setup-mobile-env.d.ts.map +1 -0
- package/dist/cli/commands/setup-mobile-env.js +65 -0
- package/dist/cli/commands/setup-mobile-env.js.map +1 -0
- package/dist/cli/index.js +2 -0
- 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/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/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/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +13 -7
- 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 +2 -0
- package/dist/exporters/matrix/export.d.ts.map +1 -1
- package/dist/exporters/matrix/export.js +7 -1
- package/dist/exporters/matrix/export.js.map +1 -1
- package/dist/exporters/matrix/wording.d.ts +7 -0
- package/dist/exporters/matrix/wording.d.ts.map +1 -1
- package/dist/exporters/matrix/wording.js +27 -1
- package/dist/exporters/matrix/wording.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/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +1 -2
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +8 -5
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/count-assertion.hbs +5 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +1 -1
- 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/list-item-count-assertion.hbs +5 -2
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +1 -1
- 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/steps/partials/await-elements.hbs +12 -0
- package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +94 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +7 -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/row-scoped-column-assertion.hbs +23 -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/locator-base.hbs +4 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +4 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +17 -2
- package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +14 -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 +122 -15
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +70 -4
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +69 -3
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts +13 -3
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +16 -10
- 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 +81 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +187 -13
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +18 -0
- package/dist/generators/test-generator/template-engine.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 +84 -4
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +229 -82
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +35 -8
- 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/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 +2 -1
- package/dist/harness/next-step.js.map +1 -1
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +42 -7
- 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 +16 -4
- 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 +6 -0
- package/dist/harness/read-text.d.ts.map +1 -1
- package/dist/harness/read-text.js +7 -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/sensors.d.ts +12 -0
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +41 -18
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/spec-branches.d.ts +7 -20
- package/dist/harness/spec-branches.d.ts.map +1 -1
- package/dist/harness/spec-branches.js +85 -13
- package/dist/harness/spec-branches.js.map +1 -1
- package/dist/harness/spec-coverage.d.ts +16 -0
- package/dist/harness/spec-coverage.d.ts.map +1 -1
- package/dist/harness/spec-coverage.js +131 -12
- package/dist/harness/spec-coverage.js.map +1 -1
- package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
- package/dist/harness/viewpoint-ledger.js +2 -1
- 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/appium-environment-setup.d.ts +24 -0
- package/dist/orchestrator/appium-environment-setup.d.ts.map +1 -0
- package/dist/orchestrator/appium-environment-setup.js +157 -0
- package/dist/orchestrator/appium-environment-setup.js.map +1 -0
- 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/mobile-runtime-scaffolder.d.ts +2 -0
- package/dist/orchestrator/mobile-runtime-scaffolder.d.ts.map +1 -1
- package/dist/orchestrator/mobile-runtime-scaffolder.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 +9 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +63 -7
- package/dist/orchestrator/project-initializer.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/skills/sungen-delivery/SKILL.md +14 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +8 -2
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +64 -2
- 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 +20 -5
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +3 -3
- package/dist/orchestrator/templates/appium-environment/README.md +83 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/install-appium.ps1 +44 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/install-appium.sh +50 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/lib.ps1 +108 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/lib.sh +141 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/verify-appium.ps1 +39 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/verify-appium.sh +43 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/verify-node.ps1 +13 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/verify-node.sh +16 -0
- package/dist/orchestrator/templates/appium-environment/scripts/macos/install-android.sh +110 -0
- package/dist/orchestrator/templates/appium-environment/scripts/macos/install-ios.sh +32 -0
- package/dist/orchestrator/templates/appium-environment/scripts/macos/verify-android.sh +32 -0
- package/dist/orchestrator/templates/appium-environment/scripts/macos/verify-ios.sh +15 -0
- package/dist/orchestrator/templates/appium-environment/scripts/windows/install-android.ps1 +153 -0
- package/dist/orchestrator/templates/appium-environment/scripts/windows/verify-android.ps1 +26 -0
- package/dist/orchestrator/templates/appium-environment/setup.ps1 +58 -0
- package/dist/orchestrator/templates/appium-environment/setup.sh +88 -0
- package/dist/orchestrator/templates/specs-checked-state.d.ts +1 -1
- package/dist/orchestrator/templates/specs-checked-state.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-checked-state.js +26 -3
- package/dist/orchestrator/templates/specs-checked-state.js.map +1 -1
- package/dist/orchestrator/templates/specs-checked-state.ts +31 -6
- package/dist/orchestrator/templates/specs-locale-fixture.ts +21 -9
- package/dist/orchestrator/templates/specs-mock.d.ts +2 -1
- package/dist/orchestrator/templates/specs-mock.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-mock.js +10 -3
- package/dist/orchestrator/templates/specs-mock.js.map +1 -1
- package/dist/orchestrator/templates/specs-mock.ts +12 -4
- 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 +14 -2
- package/dist/orchestrator/templates/specs-url-assert.js.map +1 -1
- package/dist/orchestrator/templates/specs-url-assert.ts +24 -2
- package/dist/orchestrator/templates/wdio.conf.ts +24 -1
- package/package.json +3 -3
- package/src/cli/commands/audit.ts +12 -3
- package/src/cli/commands/capability.ts +63 -19
- package/src/cli/commands/delivery.ts +38 -16
- package/src/cli/commands/generate.ts +15 -7
- package/src/cli/commands/init.ts +3 -0
- package/src/cli/commands/results-merge.ts +2 -8
- package/src/cli/commands/results-sanitize.ts +2 -9
- package/src/cli/commands/setup-mobile-env.ts +64 -0
- package/src/cli/index.ts +2 -0
- package/src/dashboard/snapshot-builder.ts +6 -8
- package/src/exporters/feature-parser.ts +32 -2
- package/src/exporters/locale-variants.ts +23 -17
- package/src/exporters/matrix/build.ts +14 -8
- package/src/exporters/matrix/carry-manual-results.ts +229 -0
- package/src/exporters/matrix/export.ts +10 -4
- package/src/exporters/matrix/wording.ts +26 -1
- package/src/exporters/xlsx-exporter.ts +7 -2
- package/src/generators/test-generator/adapters/adapter-interface.ts +2 -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/all-contain-assertion.hbs +1 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +8 -5
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/count-assertion.hbs +5 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +1 -1
- 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/list-item-count-assertion.hbs +5 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +1 -1
- 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/steps/partials/await-elements.hbs +12 -0
- package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +94 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +7 -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/row-scoped-column-assertion.hbs +23 -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/locator-base.hbs +4 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +4 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +17 -2
- package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +14 -0
- package/src/generators/test-generator/code-generator.ts +119 -19
- package/src/generators/test-generator/diagnostics.ts +70 -4
- package/src/generators/test-generator/patterns/index.ts +16 -9
- 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 +196 -13
- package/src/generators/test-generator/template-engine.ts +18 -0
- 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 +256 -85
- package/src/harness/audit.ts +41 -12
- 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/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 +2 -1
- package/src/harness/parse.ts +42 -7
- package/src/harness/perf.ts +15 -7
- package/src/harness/quality-gates.ts +16 -4
- package/src/harness/query-catalog.ts +0 -0
- package/src/harness/read-text.ts +7 -1
- package/src/harness/secret-scan.ts +60 -4
- package/src/harness/sensors.ts +50 -19
- package/src/harness/spec-branches.ts +72 -14
- package/src/harness/spec-coverage.ts +133 -13
- package/src/harness/viewpoint-ledger.ts +2 -1
- package/src/harness/write-atomic.ts +22 -0
- package/src/orchestrator/ai-skills/writer.ts +18 -3
- package/src/orchestrator/appium-environment-setup.ts +127 -0
- package/src/orchestrator/assets-drift.ts +10 -5
- package/src/orchestrator/mobile-runtime-scaffolder.ts +2 -0
- package/src/orchestrator/project-deps-sync.ts +23 -1
- package/src/orchestrator/project-initializer.ts +69 -7
- package/src/orchestrator/security-overrides-catalog.ts +69 -0
- package/src/orchestrator/security-overrides.ts +149 -0
- 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 +8 -2
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +64 -2
- 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 +20 -5
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +3 -3
- package/src/orchestrator/templates/appium-environment/README.md +83 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/install-appium.ps1 +44 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/install-appium.sh +50 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/lib.ps1 +108 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/lib.sh +141 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/verify-appium.ps1 +39 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/verify-appium.sh +43 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/verify-node.ps1 +13 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/verify-node.sh +16 -0
- package/src/orchestrator/templates/appium-environment/scripts/macos/install-android.sh +110 -0
- package/src/orchestrator/templates/appium-environment/scripts/macos/install-ios.sh +32 -0
- package/src/orchestrator/templates/appium-environment/scripts/macos/verify-android.sh +32 -0
- package/src/orchestrator/templates/appium-environment/scripts/macos/verify-ios.sh +15 -0
- package/src/orchestrator/templates/appium-environment/scripts/windows/install-android.ps1 +153 -0
- package/src/orchestrator/templates/appium-environment/scripts/windows/verify-android.ps1 +26 -0
- package/src/orchestrator/templates/appium-environment/setup.ps1 +58 -0
- package/src/orchestrator/templates/appium-environment/setup.sh +88 -0
- package/src/orchestrator/templates/specs-checked-state.ts +31 -6
- package/src/orchestrator/templates/specs-locale-fixture.ts +21 -9
- package/src/orchestrator/templates/specs-mock.ts +12 -4
- package/src/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
- package/src/orchestrator/templates/specs-url-assert.ts +24 -2
- package/src/orchestrator/templates/wdio.conf.ts +24 -1
|
@@ -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
|
+
}
|
|
@@ -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
|
@@ -167,7 +167,8 @@ const REPAIRABLE = [
|
|
|
167
167
|
'FLOW-CONTRACT-MISSING', 'FLOW-INVENTORY-MISSING', 'FLOW-UNCOVERED', 'FLOW-UNDECLARED',
|
|
168
168
|
'FLOW-PHASE-MISFILED', 'FLOW-GUARANTEE-MISSING', 'FLOW-OUTCOME-UNPROVEN', 'CONTINUITY-ONE-SIDED',
|
|
169
169
|
'ORACLE-URL-ONLY',
|
|
170
|
-
'SPEC-RESTATED-UNVERIFIED', 'VIEWPOINT-GESTURE-SUBSTITUTED', 'SPEC-UNCOVERED', '
|
|
170
|
+
'SPEC-RESTATED-UNVERIFIED', 'VIEWPOINT-GESTURE-SUBSTITUTED', 'SPEC-UNCOVERED', 'SPEC-CLAUSE-UNCOVERED',
|
|
171
|
+
'TRIGGER-UNCOVERED', 'PERMISSION-PAIR-MISSING', 'VISIBILITY-PAIR-MISSING',
|
|
171
172
|
'VIEWPOINT-ITEM-MISSING', 'MANUAL-CODE-MISSING', 'MANUAL-AUTOMATABLE', 'DEPTH-DEFERRED',
|
|
172
173
|
];
|
|
173
174
|
|
package/src/harness/parse.ts
CHANGED
|
@@ -78,6 +78,32 @@ function isViewpointId(s: string): boolean {
|
|
|
78
78
|
return isIdLike(s) || /^(?:VP|FL)-[A-Z][A-Z0-9]*$/i.test(s);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* A markdown heading's TEXT, normalised the way the ledger already normalises it.
|
|
83
|
+
*
|
|
84
|
+
* Headings in a real viewpoint file are numbered ("## 2. Priority Viewpoints") and often bolded
|
|
85
|
+
* ("## **Priority Viewpoints**"), and both forms are what the authoring template produces. Every
|
|
86
|
+
* pass below anchored on the bare literal, so a numbered file parsed to ZERO viewpoints: the
|
|
87
|
+
* priority table was dropped whole, clean rows included, and the audit lost its traceability
|
|
88
|
+
* table. `viewpoint-ledger.ts` learned the same lesson at its own heading test (#657); this is
|
|
89
|
+
* that normalisation, applied where the overview reads headings.
|
|
90
|
+
*
|
|
91
|
+
* Returns null for a line that is not a heading, so a caller can tell "not a heading" from
|
|
92
|
+
* "a heading whose text is empty".
|
|
93
|
+
*/
|
|
94
|
+
function headingParts(line: string): { level: number; text: string } | null {
|
|
95
|
+
const m = line.match(/^(#{1,6})\s+(.*)$/);
|
|
96
|
+
if (!m) return null;
|
|
97
|
+
const text = m[2]
|
|
98
|
+
.replace(/[*`]/g, '')
|
|
99
|
+
// Multi-level (`2.1`, `3.2.4`) and roman numbering are at least as common in a numbered spec as
|
|
100
|
+
// a single digit, and a strip that only handled one level left `## 2.1 Priority Viewpoints`
|
|
101
|
+
// reading as `1 Priority Viewpoints` — the same zero-viewpoint failure, one dot away.
|
|
102
|
+
.replace(/^\s*(?:\d+(?:\.\d+)*|[IVXLCDM]+|[A-Z])\s*[.):]?\s+/, '')
|
|
103
|
+
.trim();
|
|
104
|
+
return { level: m[1].length, text };
|
|
105
|
+
}
|
|
106
|
+
|
|
81
107
|
export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
|
|
82
108
|
if (!fs.existsSync(filePath)) return [];
|
|
83
109
|
const text = readTextFile(filePath);
|
|
@@ -89,12 +115,15 @@ export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
|
|
|
89
115
|
let inPriorityTable = false;
|
|
90
116
|
for (const raw of lines) {
|
|
91
117
|
const line = raw.trim();
|
|
92
|
-
|
|
93
|
-
if (
|
|
118
|
+
const heading = headingParts(line);
|
|
119
|
+
if (heading && heading.level === 2 && /^Priority Viewpoints/i.test(heading.text)) { inPriorityTable = true; continue; }
|
|
120
|
+
if (inPriorityTable && heading && heading.level === 2) { inPriorityTable = false; }
|
|
94
121
|
if (inPriorityTable && line.startsWith('|')) {
|
|
95
122
|
const cells = line.split('|').map((c) => c.trim()).filter((_, i, a) => i > 0 && i < a.length - 1);
|
|
96
123
|
if (cells.length >= 3) {
|
|
97
|
-
|
|
124
|
+
// The same `**` the heading carries: ids are routinely written `| **VP-LOGIC** | High | … |`,
|
|
125
|
+
// and an id read with its emphasis markers still attached matches no scenario tag.
|
|
126
|
+
const id = cells[0].replace(/[*`]/g, '').trim();
|
|
98
127
|
if (isViewpointId(id) && !/^-+$/.test(cells[1])) {
|
|
99
128
|
const pr = /high/i.test(cells[1]) ? 'High' : /medium/i.test(cells[1]) ? 'Medium' : /low/i.test(cells[1]) ? 'Low' : 'Unknown';
|
|
100
129
|
entries.set(id.toUpperCase(), { id: id.toUpperCase(), priority: pr as any, reason: cells[2] });
|
|
@@ -110,7 +139,10 @@ export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
|
|
|
110
139
|
// FL-HP-001 were reported as unmapped. Both forms are additive here.
|
|
111
140
|
for (const raw of lines) {
|
|
112
141
|
const line = raw.trim();
|
|
113
|
-
const
|
|
142
|
+
const head = headingParts(line);
|
|
143
|
+
const section = head && head.level === 2
|
|
144
|
+
? head.text.match(/^([A-Z]{2,}(?:-[A-Z0-9]{2,})*)\s+[—–-]\s*(.*)$/)
|
|
145
|
+
: null;
|
|
114
146
|
if (section && isViewpointId(section[1] + '-0')) {
|
|
115
147
|
const id = section[1].toUpperCase();
|
|
116
148
|
if (!entries.has(id)) entries.set(id, { id, priority: 'Unknown', reason: section[2] ?? '' });
|
|
@@ -130,11 +162,14 @@ export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
|
|
|
130
162
|
let group: ViewpointEntry['group'] | undefined;
|
|
131
163
|
for (const raw of lines) {
|
|
132
164
|
const line = raw.trim();
|
|
133
|
-
const
|
|
165
|
+
const head = headingParts(line);
|
|
166
|
+
const g = head && head.level === 3 ? head.text.match(/^(Required|Recommended|Optional)/i) : null;
|
|
134
167
|
if (g) { group = (g[1][0].toUpperCase() + g[1].slice(1).toLowerCase()) as any; continue; }
|
|
135
|
-
if (
|
|
168
|
+
if (head && head.level === 2) { group = undefined; }
|
|
136
169
|
if (group) {
|
|
137
|
-
|
|
170
|
+
// `**` and backticks only — a single `*` is a bullet marker here, and stripping it would
|
|
171
|
+
// stop `* VP-A-001` being a bullet at all.
|
|
172
|
+
const m = line.replace(/\*\*|`/g, '').match(/^[-*+]\s+([A-Za-z][A-Za-z0-9.-]*)/);
|
|
138
173
|
if (m && isViewpointId(m[1])) {
|
|
139
174
|
const id = m[1].toUpperCase();
|
|
140
175
|
const existing = entries.get(id);
|
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,6 +5,7 @@
|
|
|
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';
|
|
10
11
|
import { readTextFile } from './read-text';
|
|
@@ -57,12 +58,19 @@ function downstreamRoutes(specText: string): string[] {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
|
-
* H5 — a
|
|
61
|
-
*
|
|
61
|
+
* H5 — a stateful (cart-mutating) flow with NO per-scenario isolation is flaky: scenarios share
|
|
62
|
+
* state, so cart count/quantity asserts race. Mitigations: @cleanup:cart, @isolate, a fresh
|
|
62
63
|
* browser context, or a "Given … empty cart" background. Returns true when the risk is unmitigated.
|
|
64
|
+
*
|
|
65
|
+
* #759 — this used to require a literal `@parallel`, which made the gate miss the common case.
|
|
66
|
+
* Parallel is the DEFAULT (code-generator.ts: `isParallelFeature = !isSerialFeature`); `@parallel`
|
|
67
|
+
* only NAMES that default and changes no codegen, so an untagged stateful flow is isolated exactly
|
|
68
|
+
* like a tagged one and carries exactly the same risk. The tag that actually changes the emitted
|
|
69
|
+
* suite is `@serial` — one shared ordered page, where state carry-over is the point — so that is
|
|
70
|
+
* what exempts a flow now. Same reading as serialCascadeRisk() below, which already said so.
|
|
63
71
|
*/
|
|
64
72
|
export function isolationRisk(featureText: string, stateful: boolean, collection = 'cart'): boolean {
|
|
65
|
-
if (!stateful ||
|
|
73
|
+
if (!stateful || /@serial\b/i.test(featureText)) return false;
|
|
66
74
|
// What discharges the risk is isolation of THE MUTATED COLLECTION — so the cleanup tag has
|
|
67
75
|
// to name it (`@cleanup:cart` for a cart; `@cleanup:registration` for a registration flow).
|
|
68
76
|
// A cleanup of something else (`@cleanup:overlay`) leaves the shared state shared.
|
|
@@ -149,8 +157,12 @@ export function downstreamScope(
|
|
|
149
157
|
// names the same screen under its human label (register ↔ [Registration]).
|
|
150
158
|
const labelsOf = (s: ScenarioInfo): string[] =>
|
|
151
159
|
[...s.haystack.matchAll(/\[([^\]]+)\]/g)].map((m) => m[1].split(':')[0]);
|
|
160
|
+
// The slug is the route's last segment — in practice an ordinary noun (`order`, `detail`,
|
|
161
|
+
// `login`), so it is matched as a WORD (#779). `dis·order` is not a reference to `/account/order`,
|
|
162
|
+
// and crediting it downgraded a route nothing mentions from `absent` to merely `underCovered`.
|
|
163
|
+
// The full route stays a literal: a path is an identifier, not prose.
|
|
152
164
|
const refs = scenarios.filter((s) =>
|
|
153
|
-
s.haystack
|
|
165
|
+
keywordHit(s.haystack, slug) || s.haystack.includes(route.toLowerCase()) ||
|
|
154
166
|
labelsOf(s).some((l) => sameScreenName(slug, l)));
|
|
155
167
|
if (!refs.length) {
|
|
156
168
|
// A scenario's haystack is its name + steps, so a route documented in a @manual scenario's
|
|
Binary file
|
package/src/harness/read-text.ts
CHANGED
|
@@ -17,9 +17,15 @@ 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. */
|
|
@@ -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
|
}
|
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. */
|