@sun-asterisk/sungen 3.2.32 → 3.2.34-beta.1
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 +64 -11
- 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 +40 -18
- 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/defect.d.ts +7 -0
- package/dist/cli/commands/defect.d.ts.map +1 -0
- package/dist/cli/commands/defect.js +68 -0
- package/dist/cli/commands/defect.js.map +1 -0
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +113 -59
- 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/triage.d.ts +7 -0
- package/dist/cli/commands/triage.d.ts.map +1 -0
- package/dist/cli/commands/triage.js +98 -0
- package/dist/cli/commands/triage.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/commands/viewpoint.d.ts +7 -0
- package/dist/cli/commands/viewpoint.d.ts.map +1 -0
- package/dist/cli/commands/viewpoint.js +70 -0
- package/dist/cli/commands/viewpoint.js.map +1 -0
- package/dist/cli/index.js +13 -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 +4 -0
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +12 -11
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/feature-parser.d.ts +29 -0
- package/dist/exporters/feature-parser.d.ts.map +1 -1
- package/dist/exporters/feature-parser.js +44 -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 +68 -19
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/carry-manual-results.d.ts +89 -0
- package/dist/exporters/matrix/carry-manual-results.d.ts.map +1 -0
- package/dist/exporters/matrix/carry-manual-results.js +302 -0
- package/dist/exporters/matrix/carry-manual-results.js.map +1 -0
- package/dist/exporters/matrix/export.d.ts +9 -1
- package/dist/exporters/matrix/export.d.ts.map +1 -1
- package/dist/exporters/matrix/export.js +21 -3
- 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-csv.js +1 -1
- package/dist/exporters/matrix/render-csv.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/render-xlsx.d.ts.map +1 -1
- package/dist/exporters/matrix/render-xlsx.js +3 -0
- package/dist/exporters/matrix/render-xlsx.js.map +1 -1
- package/dist/exporters/matrix/types.d.ts +13 -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/playwright-report-parser.d.ts.map +1 -1
- package/dist/exporters/playwright-report-parser.js +1 -0
- package/dist/exporters/playwright-report-parser.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 +49 -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/types.d.ts +5 -0
- package/dist/exporters/types.d.ts.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 +60 -19
- 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/counted-assertions.d.ts +2 -0
- package/dist/harness/counted-assertions.d.ts.map +1 -0
- package/dist/harness/counted-assertions.js +38 -0
- package/dist/harness/counted-assertions.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/defect-draft.d.ts +14 -0
- package/dist/harness/defect-draft.d.ts.map +1 -0
- package/dist/harness/defect-draft.js +143 -0
- package/dist/harness/defect-draft.js.map +1 -0
- package/dist/harness/defects.d.ts +24 -0
- package/dist/harness/defects.d.ts.map +1 -0
- package/dist/harness/defects.js +118 -0
- package/dist/harness/defects.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/manifest.d.ts.map +1 -1
- package/dist/harness/manifest.js +2 -2
- package/dist/harness/manifest.js.map +1 -1
- package/dist/harness/next-step.d.ts +2 -4
- package/dist/harness/next-step.d.ts.map +1 -1
- package/dist/harness/next-step.js +50 -5
- package/dist/harness/next-step.js.map +1 -1
- package/dist/harness/parse.d.ts +45 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +142 -24
- 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/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +7 -1
- package/dist/harness/script-check.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 +133 -39
- 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/triage-signals.d.ts +20 -0
- package/dist/harness/triage-signals.d.ts.map +1 -0
- package/dist/harness/triage-signals.js +30 -0
- package/dist/harness/triage-signals.js.map +1 -0
- package/dist/harness/triage-unit.d.ts +23 -0
- package/dist/harness/triage-unit.d.ts.map +1 -0
- package/dist/harness/triage-unit.js +142 -0
- package/dist/harness/triage-unit.js.map +1 -0
- package/dist/harness/triage.d.ts +28 -0
- package/dist/harness/triage.d.ts.map +1 -0
- package/dist/harness/triage.js +99 -0
- package/dist/harness/triage.js.map +1 -0
- 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-baseline.d.ts.map +1 -1
- package/dist/harness/viewpoint-baseline.js +7 -1
- package/dist/harness/viewpoint-baseline.js.map +1 -1
- package/dist/harness/viewpoint-draft-header.d.ts +7 -0
- package/dist/harness/viewpoint-draft-header.d.ts.map +1 -0
- package/dist/harness/viewpoint-draft-header.js +100 -0
- package/dist/harness/viewpoint-draft-header.js.map +1 -0
- package/dist/harness/viewpoint-ledger.d.ts +13 -3
- package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
- package/dist/harness/viewpoint-ledger.js +106 -19
- package/dist/harness/viewpoint-ledger.js.map +1 -1
- package/dist/harness/viewpoint-mode.d.ts +9 -0
- package/dist/harness/viewpoint-mode.d.ts.map +1 -0
- package/dist/harness/viewpoint-mode.js +52 -0
- package/dist/harness/viewpoint-mode.js.map +1 -0
- package/dist/harness/viewpoint-priority.d.ts +19 -0
- package/dist/harness/viewpoint-priority.d.ts.map +1 -0
- package/dist/harness/viewpoint-priority.js +36 -0
- package/dist/harness/viewpoint-priority.js.map +1 -0
- package/dist/harness/viewpoint-review-open.d.ts +6 -0
- package/dist/harness/viewpoint-review-open.d.ts.map +1 -0
- package/dist/harness/viewpoint-review-open.js +90 -0
- package/dist/harness/viewpoint-review-open.js.map +1 -0
- package/dist/harness/viewpoint-scan-refs.d.ts +50 -0
- package/dist/harness/viewpoint-scan-refs.d.ts.map +1 -0
- package/dist/harness/viewpoint-scan-refs.js +200 -0
- package/dist/harness/viewpoint-scan-refs.js.map +1 -0
- package/dist/harness/viewpoint-scan.d.ts +41 -0
- package/dist/harness/viewpoint-scan.d.ts.map +1 -0
- package/dist/harness/viewpoint-scan.js +118 -0
- package/dist/harness/viewpoint-scan.js.map +1 -0
- 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/flow-manager.d.ts +0 -1
- package/dist/orchestrator/flow-manager.d.ts.map +1 -1
- package/dist/orchestrator/flow-manager.js +3 -52
- package/dist/orchestrator/flow-manager.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 +8 -62
- 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 +32 -24
- package/dist/orchestrator/templates/ai-src/commands/review.md +2 -2
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +78 -15
- 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 +25 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +26 -8
- package/dist/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +5 -1
- 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 +42 -2
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +7 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md +18 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/review-checklist.md +63 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/viewpoint-template.md +158 -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 +23 -0
- package/dist/orchestrator/viewpoint-pattern-catalog.d.ts.map +1 -0
- package/dist/orchestrator/viewpoint-pattern-catalog.js +48 -0
- package/dist/orchestrator/viewpoint-pattern-catalog.js.map +1 -0
- package/dist/orchestrator/viewpoint-scaffold.d.ts +3 -0
- package/dist/orchestrator/viewpoint-scaffold.d.ts.map +1 -0
- package/dist/orchestrator/viewpoint-scaffold.js +70 -0
- package/dist/orchestrator/viewpoint-scaffold.js.map +1 -0
- package/package.json +3 -3
- package/src/cli/commands/audit.ts +59 -12
- package/src/cli/commands/capability.ts +54 -19
- package/src/cli/commands/changed.ts +2 -4
- package/src/cli/commands/dashboard.ts +1 -26
- package/src/cli/commands/defect.ts +49 -0
- package/src/cli/commands/delivery.ts +106 -56
- 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/triage.ts +57 -0
- package/src/cli/commands/update.ts +3 -0
- package/src/cli/commands/viewpoint.ts +58 -0
- package/src/cli/index.ts +13 -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 +12 -11
- package/src/exporters/feature-parser.ts +44 -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 +71 -19
- package/src/exporters/matrix/carry-manual-results.ts +292 -0
- package/src/exporters/matrix/export.ts +24 -4
- package/src/exporters/matrix/gates.ts +6 -0
- package/src/exporters/matrix/render-csv.ts +1 -1
- package/src/exporters/matrix/render-testcase-sheet.ts +10 -4
- package/src/exporters/matrix/render-xlsx.ts +3 -0
- package/src/exporters/matrix/types.ts +13 -1
- package/src/exporters/matrix/wording.ts +36 -2
- package/src/exporters/playwright-report-parser.ts +1 -0
- package/src/exporters/run-environment.ts +89 -0
- package/src/exporters/scenario-merger.ts +51 -5
- package/src/exporters/test-data-resolver.ts +0 -6
- package/src/exporters/types.ts +5 -0
- 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 +69 -24
- 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/counted-assertions.ts +33 -0
- package/src/harness/data-driven-lint.ts +82 -24
- package/src/harness/defect-draft.ts +115 -0
- package/src/harness/defects.ts +94 -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/manifest.ts +2 -2
- package/src/harness/next-step.ts +47 -5
- package/src/harness/parse.ts +151 -27
- 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/script-check.ts +7 -1
- package/src/harness/secret-scan.ts +60 -4
- package/src/harness/select-by-ids.ts +83 -0
- package/src/harness/sensors.ts +141 -40
- package/src/harness/spec-branches.ts +6 -3
- package/src/harness/spec-coverage.ts +14 -4
- package/src/harness/triage-signals.ts +46 -0
- package/src/harness/triage-unit.ts +113 -0
- package/src/harness/triage.ts +127 -0
- package/src/harness/ui-inventory.ts +164 -0
- package/src/harness/viewpoint-baseline.ts +7 -1
- package/src/harness/viewpoint-draft-header.ts +55 -0
- package/src/harness/viewpoint-ledger.ts +113 -25
- package/src/harness/viewpoint-mode.ts +49 -0
- package/src/harness/viewpoint-priority.ts +38 -0
- package/src/harness/viewpoint-review-open.ts +51 -0
- package/src/harness/viewpoint-scan-refs.ts +154 -0
- package/src/harness/viewpoint-scan.ts +111 -0
- 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/flow-manager.ts +3 -53
- package/src/orchestrator/project-deps-sync.ts +23 -1
- package/src/orchestrator/project-initializer.ts +54 -6
- package/src/orchestrator/screen-manager.ts +7 -61
- 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 +32 -24
- package/src/orchestrator/templates/ai-src/commands/review.md +2 -2
- package/src/orchestrator/templates/ai-src/commands/run-test.md +78 -15
- 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 +25 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +26 -8
- package/src/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +5 -1
- 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 +42 -2
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +7 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md +18 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/review-checklist.md +63 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/viewpoint-template.md +158 -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 +61 -0
- package/src/orchestrator/viewpoint-scaffold.ts +36 -0
package/src/harness/sensors.ts
CHANGED
|
@@ -11,6 +11,9 @@ 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';
|
|
15
|
+
import { parsePriority } from './viewpoint-priority';
|
|
16
|
+
import { countedAssertions } from './counted-assertions';
|
|
14
17
|
|
|
15
18
|
// Business-critical category keywords (matched by CONTAINMENT against the VP category, so a
|
|
16
19
|
// compound category like LIST-DISPLAY / ADD-TO-CART / PRODUCT-DISCOVERY classifies correctly).
|
|
@@ -86,18 +89,11 @@ export function loadCatalog(): Catalog {
|
|
|
86
89
|
return parseYaml(readTextFile(p)) as Catalog;
|
|
87
90
|
}
|
|
88
91
|
|
|
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
|
-
};
|
|
92
|
+
// Word-aware keyword match — one rule for every sensor, in ./keyword-match (#776). A keyword
|
|
93
|
+
// matches at a word START but may carry any suffix, so `form` does not match inside `Format` or
|
|
94
|
+
// `@platform` while `submit` still matches `submitted`. The old local copy anchored BOTH ends,
|
|
95
|
+
// which silently dropped every inflected true positive; see that module for the measurements.
|
|
96
|
+
const has = hasKeyword;
|
|
101
97
|
|
|
102
98
|
// ---------- Sensor 1: Viewpoint Gate (catalog-driven) ----------
|
|
103
99
|
|
|
@@ -112,6 +108,13 @@ export interface GateResult {
|
|
|
112
108
|
coverageRatio: number;
|
|
113
109
|
gaps: { theme: string; keywords: string[]; status: 'missing' | 'shallow' }[];
|
|
114
110
|
universalGaps: string[];
|
|
111
|
+
/**
|
|
112
|
+
* WHICH scenario covered each satisfied theme, and on which keyword — the audit's second line of
|
|
113
|
+
* defence against a bogus coverage claim (#776). A bare count ("themes 3/3") gives a reviewer
|
|
114
|
+
* nothing to check, so a nonsense match survives; naming the evidence makes it obvious on sight.
|
|
115
|
+
* Mirrors how the Claim-Proof section already names the scenario behind each claim.
|
|
116
|
+
*/
|
|
117
|
+
coveredBy: { theme: string; by: string; keyword: string }[];
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
/**
|
|
@@ -166,20 +169,36 @@ export function viewpointGate(scenarios: ScenarioInfo[], viewpoints: ViewpointEn
|
|
|
166
169
|
|
|
167
170
|
const evidence = { hits: best, runnerUp };
|
|
168
171
|
const gaps: GateResult['gaps'] = [];
|
|
172
|
+
const coveredBy: GateResult['coveredBy'] = [];
|
|
169
173
|
let total = 0, covered = 0;
|
|
170
174
|
if (pageType) {
|
|
171
175
|
for (const t of catalog.page_types[pageType].must_cover) {
|
|
172
176
|
total++;
|
|
173
177
|
// Scenarios that cover this theme by keyword (include @manual — a manual
|
|
174
178
|
// scenario with a real assertion still covers the viewpoint for design).
|
|
175
|
-
|
|
179
|
+
// Word-aware since #776: a substring match let `information` satisfy `format-boundary`.
|
|
180
|
+
const coverers = scenarios.filter((s) => t.keywords.some((k) => keywordHit(s.haystack, k)));
|
|
176
181
|
if (coverers.length === 0) { gaps.push({ theme: t.theme, keywords: t.keywords, status: 'missing' }); continue; }
|
|
177
182
|
// A theme WITHOUT `depth:` is visibility/navigation — landing/seeing/leaving IS the
|
|
178
183
|
// assertion, so mere presence covers it. Only DATA-correctness themes (depth.requires:
|
|
179
184
|
// data-assertion) need a data assertion to count as deeply covered.
|
|
180
185
|
const needsDepth = t.depth?.requires === 'data-assertion';
|
|
181
|
-
|
|
182
|
-
|
|
186
|
+
// Name the evidence for whichever scenario actually carries the theme — the one with the
|
|
187
|
+
// data assertion when depth is required, since that is the scenario the credit rests on.
|
|
188
|
+
const proof = (needsDepth ? coverers.find((s) => s.hasDataAssertion) : undefined) ?? coverers[0];
|
|
189
|
+
const credit = (): void => {
|
|
190
|
+
covered++;
|
|
191
|
+
coveredBy.push({
|
|
192
|
+
theme: t.theme,
|
|
193
|
+
by: proof.haystack,
|
|
194
|
+
// `proof` came out of `coverers`, which was filtered on this same predicate, so the find
|
|
195
|
+
// always lands — the `!` states that rather than inventing a fallback keyword that would
|
|
196
|
+
// misattribute the credit if the invariant ever broke.
|
|
197
|
+
keyword: t.keywords.find((k) => keywordHit(proof.haystack, k))!,
|
|
198
|
+
});
|
|
199
|
+
};
|
|
200
|
+
if (!needsDepth) { credit(); continue; }
|
|
201
|
+
if (coverers.some((s) => s.hasDataAssertion)) credit();
|
|
183
202
|
else gaps.push({ theme: t.theme, keywords: t.keywords, status: 'shallow' });
|
|
184
203
|
}
|
|
185
204
|
}
|
|
@@ -203,6 +222,7 @@ export function viewpointGate(scenarios: ScenarioInfo[], viewpoints: ViewpointEn
|
|
|
203
222
|
coverageRatio: total ? covered / total : 1,
|
|
204
223
|
gaps,
|
|
205
224
|
universalGaps,
|
|
225
|
+
coveredBy,
|
|
206
226
|
};
|
|
207
227
|
}
|
|
208
228
|
|
|
@@ -269,7 +289,7 @@ export function assertionDepth(
|
|
|
269
289
|
const isDismissBehavior = (s: ScenarioInfo) =>
|
|
270
290
|
/\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
291
|
const isDepthRequired = (s: ScenarioInfo) =>
|
|
272
|
-
s.category !== 'NAV' && !isDismissBehavior(s) && depthKeywords.some((k) => s.haystack
|
|
292
|
+
s.category !== 'NAV' && !isDismissBehavior(s) && depthKeywords.some((k) => keywordHit(s.haystack, k));
|
|
273
293
|
|
|
274
294
|
const required = nonManual.filter(isDepthRequired);
|
|
275
295
|
const reqShallow = required.filter((s) => s.shallow);
|
|
@@ -347,7 +367,7 @@ export function flowCoveredThemes(
|
|
|
347
367
|
): { theme: string; flow: string }[] {
|
|
348
368
|
const out: { theme: string; flow: string }[] = [];
|
|
349
369
|
for (const g of gaps) {
|
|
350
|
-
const hit = flowScenarios.find((s) => s.deep && g.keywords.some((k) => s.haystack
|
|
370
|
+
const hit = flowScenarios.find((s) => s.deep && g.keywords.some((k) => keywordHit(s.haystack, k)));
|
|
351
371
|
if (hit) out.push({ theme: g.theme, flow: hit.flow });
|
|
352
372
|
}
|
|
353
373
|
return out;
|
|
@@ -394,7 +414,8 @@ export function flowRegressionDepth(scenarios: ScenarioInfo[]): FlowDepthResult
|
|
|
394
414
|
export interface OracleStrengthResult {
|
|
395
415
|
weak: { name: string; hint: string; code?: string }[]; // facet-by-name-substring, or URL-only (code ORACLE-URL-ONLY)
|
|
396
416
|
facetClaims: number; // scenarios that touch a category/brand facet (denominator)
|
|
397
|
-
|
|
417
|
+
weakFacet: number; // the facet-class subset of `weak` — the only numerator `facetClaims` denominates
|
|
418
|
+
ratio: number; // 1 - weakFacet/facetClaims, clamped 0..1 (1 when no facet claim) — caps businessDepth
|
|
398
419
|
}
|
|
399
420
|
|
|
400
421
|
// "see all [<item name/title>] ... contain(s) {{<facet>.term}}" — asserting every item's NAME carries
|
|
@@ -429,9 +450,11 @@ export function oracleStrength(scenarios: ScenarioInfo[]): OracleStrengthResult
|
|
|
429
450
|
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
451
|
});
|
|
431
452
|
}
|
|
453
|
+
let weakFacet = 0;
|
|
432
454
|
for (const s of scenarios) {
|
|
433
455
|
if (s.manual) continue; // a @manual facet check is a deliberate deferral, not a weak automated oracle
|
|
434
456
|
if (WEAK_FACET_ORACLE.test(s.stepsText)) {
|
|
457
|
+
weakFacet++;
|
|
435
458
|
weak.push({
|
|
436
459
|
name: s.name.slice(0, 80),
|
|
437
460
|
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 +462,17 @@ export function oracleStrength(scenarios: ScenarioInfo[]): OracleStrengthResult
|
|
|
439
462
|
}
|
|
440
463
|
}
|
|
441
464
|
const facetClaims = scenarios.filter((s) => FACET_REF.test(s.stepsText) || FACET_REF.test(s.name)).length;
|
|
442
|
-
|
|
465
|
+
// Only the FACET class is denominated by `facetClaims`. `weak` also carries ORACLE-URL-ONLY
|
|
466
|
+
// findings (#662), which make no category/brand claim at all — averaging them into this axis
|
|
467
|
+
// drove `ratio` negative (5 URL-only over 2 facet claims → 1 - 5/2 = -1.5), which walked through
|
|
468
|
+
// the businessDepth floor and took the CLI's bar renderer down with it (#762). A URL-only oracle
|
|
469
|
+
// is NAMED as a finding and holds the score out of the senior band (`seniorBandedOverall`);
|
|
470
|
+
// it is not averaged into the facet axis — the invariant `url-only-oracle.run.ts` asserts.
|
|
471
|
+
// The clamp is not belt-and-braces: a weak-facet scenario need not itself satisfy FACET_REF
|
|
472
|
+
// (`see all [name] contains {{price.term}}` is weak but claims no facet), so `weakFacet` can
|
|
473
|
+
// still outrun `facetClaims`. This axis is declared 0..1; it leaves here inside 0..1.
|
|
474
|
+
const ratio = facetClaims ? Math.max(0, Math.min(1, 1 - weakFacet / facetClaims)) : 1;
|
|
475
|
+
return { weak, facetClaims, weakFacet, ratio };
|
|
443
476
|
}
|
|
444
477
|
|
|
445
478
|
/** Collect data-correctness themes (depth.requires) for a page-type + universal. */
|
|
@@ -603,6 +636,9 @@ export interface TraceResult {
|
|
|
603
636
|
mappedToOverview: number;
|
|
604
637
|
withVpCodeRatio: number;
|
|
605
638
|
mappedRatio: number;
|
|
639
|
+
/** Viewpoint IDs read from test-viewpoint.md. 0 = nothing to map to, so `mappedRatio` is not a
|
|
640
|
+
* measurement — the scenario IDs were never checked (#832). */
|
|
641
|
+
overviewIds: number;
|
|
606
642
|
note: string;
|
|
607
643
|
}
|
|
608
644
|
|
|
@@ -614,6 +650,24 @@ export interface TraceResult {
|
|
|
614
650
|
*/
|
|
615
651
|
const unprefixed = (id: string): string => id.replace(/^(?:VP|FL)-/i, '').toUpperCase();
|
|
616
652
|
|
|
653
|
+
/**
|
|
654
|
+
* The traceability note, worded from the SAME ratio the audit's TRACE finding fires on —
|
|
655
|
+
* mapped / ALL scenarios (#852). It used to compare mapped against scenarios WITH an id, so 0 of
|
|
656
|
+
* 0 (no scenario yet, or none tagged) and 2 of 2 tagged out of 10 all read "Traceable." while
|
|
657
|
+
* the audit reported `TRACE: Traceable.` for a 0–20% mapping.
|
|
658
|
+
*/
|
|
659
|
+
function traceNote(total: number, withCode: number, mapped: number, declared: number, exampleId?: string): string {
|
|
660
|
+
if (declared === 0) {
|
|
661
|
+
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).';
|
|
662
|
+
}
|
|
663
|
+
if (total === 0) return 'No scenarios yet — traceability is measured once the feature has scenarios.';
|
|
664
|
+
if (mapped >= total * 0.5) return 'Traceable.';
|
|
665
|
+
const example = exampleId ? ` (e.g. \`${exampleId}-001 …\`)` : '';
|
|
666
|
+
if (withCode === 0) return `No scenario carries a viewpoint ID — start each title with one of the IDs declared in test-viewpoint.md${example}.`;
|
|
667
|
+
if (mapped < withCode * 0.5) return 'Scenario IDs do not match the viewpoint-overview ids (weak traceability — re-tag to the project viewpoint IDs).';
|
|
668
|
+
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}.`;
|
|
669
|
+
}
|
|
670
|
+
|
|
617
671
|
export function traceability(scenarios: ScenarioInfo[], viewpoints: ViewpointEntry[]): TraceResult {
|
|
618
672
|
const overviewIds = new Set(viewpoints.map((v) => v.id.toUpperCase()));
|
|
619
673
|
const overviewBare = new Set(viewpoints.map((v) => unprefixed(v.id)));
|
|
@@ -638,9 +692,10 @@ export function traceability(scenarios: ScenarioInfo[], viewpoints: ViewpointEnt
|
|
|
638
692
|
mappedToOverview: mapped.length,
|
|
639
693
|
withVpCodeRatio: scenarios.length ? withCode.length / scenarios.length : 0,
|
|
640
694
|
mappedRatio: scenarios.length ? mapped.length / scenarios.length : 0,
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
695
|
+
overviewIds: overviewIds.size,
|
|
696
|
+
// With no ID read there is nothing to match against: telling the author to re-tag sends them
|
|
697
|
+
// to change scenarios that may be right (#832). Name the file and the sections read instead.
|
|
698
|
+
note: traceNote(scenarios.length, withCode.length, mapped.length, overviewIds.size, viewpoints[0]?.id),
|
|
644
699
|
};
|
|
645
700
|
}
|
|
646
701
|
|
|
@@ -702,6 +757,31 @@ const CLAIM_RULES: ClaimRule[] = [
|
|
|
702
757
|
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
758
|
severity: 'warn',
|
|
704
759
|
},
|
|
760
|
+
{
|
|
761
|
+
// An ORDER claim ("sorted by date descending", "ties broken by last-updated") is invisible to
|
|
762
|
+
// a presence check: `see [List] table` passes whatever order the rows are in. It sits ahead of
|
|
763
|
+
// the generic rules so "all results sorted …" is judged as an ordering claim, not an all-claim.
|
|
764
|
+
// A tiebreaker is a Tier-1 guard in the tc-generation skill, yet no rule recognised it (#810).
|
|
765
|
+
claim: 'ordering/sort',
|
|
766
|
+
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/,
|
|
767
|
+
proof: /match data|is sorted|sorted (?:ascending|descending)|\brow \d+\b|\.first\.|\.last\.|\[\d+\]\.|order by/,
|
|
768
|
+
need: 'an order-sensitive assertion (`table match data:` rows in the expected order, `is sorted ascending|descending`, or positional `row N` checks)',
|
|
769
|
+
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.',
|
|
770
|
+
severity: 'fail',
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
// A LIMIT claim ("never exceeds 10", "at most 5 files") is a count claim in disguise.
|
|
774
|
+
// Ahead of 'no-side-effect', whose negation+effect pattern would otherwise swallow
|
|
775
|
+
// "never exceeds 10 records" and ask for a duplicate-count proof.
|
|
776
|
+
claim: 'limit/cap',
|
|
777
|
+
// The quantity must be ≥2 ("at most ONE order" is a no-duplicate claim — left to the rules
|
|
778
|
+
// below) and not a duration ("up to 30 seconds" is a timing claim, not a cap).
|
|
779
|
+
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/,
|
|
780
|
+
proof: /\bcount\b|tohavecount|table with|row with \{\{|match data|is at most|\bexactly\b|field with \{\{|\berror\b|invalid|is disabled/,
|
|
781
|
+
need: 'a count/boundary assertion (count is N, `is at most`, the N+1th input rejected or truncated)',
|
|
782
|
+
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.',
|
|
783
|
+
severity: 'fail',
|
|
784
|
+
},
|
|
705
785
|
{
|
|
706
786
|
claim: 'all/every/each',
|
|
707
787
|
title: /\b(all|every|each)\b/,
|
|
@@ -869,7 +949,14 @@ export interface TaxonomyResult {
|
|
|
869
949
|
|
|
870
950
|
const TAXONOMY_KNOWN = new Set(['LIST', 'PRODUCT', 'CART', 'FILTER', 'NAV', 'VAL', 'SEC']);
|
|
871
951
|
|
|
872
|
-
|
|
952
|
+
/**
|
|
953
|
+
* Words of ACCESS — a title about who may reach something. They make the robustness/routing words
|
|
954
|
+
* below security evidence ("deep link without authentication"); alone those words describe routing.
|
|
955
|
+
*/
|
|
956
|
+
const ACCESS_CONTEXT = /\b(xss|sql|injection|without authentication|unauthenticated|unauthori[sz]ed|without an? (?:authenticated|valid|active) \w+|without (?:permission|login|logging in|signing in|an? account|access)|not (?:logged|signed) in|logged[- ]out|direct(?:ly)? access\w*|auth[- ]?guard|forbidden|\b403\b|(?:no|insufficient|missing) permissions?|permission required|session expired|expired session|tamper\w*)\b/;
|
|
957
|
+
|
|
958
|
+
/** `requires`: the signal counts only when the title ALSO matches it (#888). */
|
|
959
|
+
const CATEGORY_SIGNALS: { cat: string; re: RegExp; requires?: RegExp }[] = [
|
|
873
960
|
{ cat: 'FILTER', re: /\b(categor(y|ies)|sub-?categor\w*|expand\w*|collaps\w*|\bfacet\b|filter\w*|belongs? to)\b/ },
|
|
874
961
|
{ cat: 'CART', re: /\b(add(ed|ing)? to cart|added (dialog|modal|message|confirmation)|cart (page|table|line|item)|view cart|continue shopping|quantit)\b/ },
|
|
875
962
|
{ cat: 'PRODUCT', re: /\b(product detail|product-detail|view product|product page|same product|product name and price|product identity|detail product)\b/ },
|
|
@@ -881,9 +968,16 @@ const CATEGORY_SIGNALS: { cat: string; re: RegExp }[] = [
|
|
|
881
968
|
// SEC, so the linter told QA to re-tag genuine auth-gate tests as navigation — diluting the
|
|
882
969
|
// visibility of the security suite (QA field report, 3.2.23). The guard vocabulary below is
|
|
883
970
|
// what makes the INTENT matchable, and GUARD_INTENT then wins over a mere observable.
|
|
884
|
-
{ cat: 'SEC', re: /\b(xss|sql|injection|payload|without authentication|unauthenticated|without an? (?:authenticated|valid|active) \w+|direct(?:ly)? access\w*|
|
|
971
|
+
{ cat: 'SEC', re: /\b(xss|sql|injection|payload|without authentication|unauthenticated|without an? (?:authenticated|valid|active) \w+|direct(?:ly)? access\w*|auth[- ]?guard|not skippable|without having (?:completed|submitted|logged)|remains? unauthenticated|\binert\b|tamper\w*|malformed|special character|abuse)\b/ },
|
|
972
|
+
// Robustness and routing words are SEC evidence only beside an ACCESS word. Alone they describe
|
|
973
|
+
// routing — "Unknown route shows the 404 page", "Deep link opens the item", "the tab from the url
|
|
974
|
+
// parameter" — and the linter told QA to re-tag those navigation tests as VP-SEC (#888).
|
|
975
|
+
{ cat: 'SEC', re: /\b(deep[- ]link\w*|url parameter|query param\w*|falls? back|gracefull?y|without a crash|not[- ]found|\b404\b)\b/, requires: ACCESS_CONTEXT },
|
|
885
976
|
];
|
|
886
977
|
|
|
978
|
+
const signalMatches = (c: { re: RegExp; requires?: RegExp }, title: string): boolean =>
|
|
979
|
+
c.re.test(title) && (!c.requires || c.requires.test(title));
|
|
980
|
+
|
|
887
981
|
/**
|
|
888
982
|
* Categories whose signal describes the test's INTENT/risk rather than the observable action.
|
|
889
983
|
* When one of these matches, a competing observable-only category (NAV: "redirects", "navigates")
|
|
@@ -901,14 +995,19 @@ export function taxonomyLint(scenarios: ScenarioInfo[]): TaxonomyResult {
|
|
|
901
995
|
if (!cat || !TAXONOMY_KNOWN.has(cat)) continue;
|
|
902
996
|
checked++;
|
|
903
997
|
const title = s.name.toLowerCase();
|
|
904
|
-
const matched = [...new Set(CATEGORY_SIGNALS.filter((c) => c
|
|
998
|
+
const matched = [...new Set(CATEGORY_SIGNALS.filter((c) => signalMatches(c, title)).map((c) => c.cat))];
|
|
905
999
|
if (matched.includes(cat)) continue; // title supports its own category → fine
|
|
1000
|
+
// A gate (`requires`) only stops a word from SUGGESTING its category. An author who already
|
|
1001
|
+
// chose that category is supported by the word alone: a VP-SEC "Deep link to the filtered
|
|
1002
|
+
// list …" stays SEC instead of being pushed toward FILTER (#888 review).
|
|
1003
|
+
if (CATEGORY_SIGNALS.some((c) => c.cat === cat && c.requires && c.re.test(title))) continue;
|
|
906
1004
|
// The declared category may be an INTENT category whose signal this title spells
|
|
907
1005
|
// differently; never "correct" it toward a category that only names the observable.
|
|
908
1006
|
const suppressed = INTENT_OVER_OBSERVABLE[cat];
|
|
909
1007
|
const others = matched.filter((c) => c !== cat && !(suppressed && suppressed.has(c)));
|
|
910
1008
|
if (others.length === 1) { // unambiguous mismatch
|
|
911
|
-
|
|
1009
|
+
// The entry that MATCHED — a category can have several (SEC: guard words, gated robustness words).
|
|
1010
|
+
const sig = CATEGORY_SIGNALS.find((c) => c.cat === others[0] && signalMatches(c, title))!;
|
|
912
1011
|
const m = title.match(sig.re);
|
|
913
1012
|
mislabeled.push({ name: s.name, current: cat, suggested: others[0], signal: m ? m[0] : others[0] });
|
|
914
1013
|
}
|
|
@@ -980,9 +1079,8 @@ export function numericClaimGaps(scenarios: ScenarioInfo[]): Array<{ name: strin
|
|
|
980
1079
|
if (!m) continue;
|
|
981
1080
|
const claimed = NUMBER_WORDS[m[1]] ?? Number(m[1]);
|
|
982
1081
|
if (!Number.isFinite(claimed) || claimed < 2) continue;
|
|
983
|
-
//
|
|
984
|
-
|
|
985
|
-
const asserted = (s.stepsText.match(/\bsee \[[^\]]+\][^|]*?\b(?:with|contains) \{\{/g) ?? []).length;
|
|
1082
|
+
// Values, positive states and indicator presence count; plain text visibility does not (#866).
|
|
1083
|
+
const asserted = countedAssertions(s.stepsText);
|
|
986
1084
|
if (asserted > 0 && asserted < claimed) out.push({ name: s.name, claimed, asserted, noun: m[2] });
|
|
987
1085
|
}
|
|
988
1086
|
return out;
|
|
@@ -994,17 +1092,17 @@ export function numericClaimGaps(scenarios: ScenarioInfo[]): Array<{ name: strin
|
|
|
994
1092
|
* The Priority-Viewpoints table is where release selection is supposed to come from. When the
|
|
995
1093
|
* table says a flow is `Low` and its scenario is tagged `@high`, a smoke run picks up work the QA
|
|
996
1094
|
* deliberately deprioritised — and nobody can tell which of the two was the decision (#622).
|
|
1095
|
+
*
|
|
1096
|
+
* Reads the SAME parse traceability does (`parseViewpointOverview`). It used to scan the raw text
|
|
1097
|
+
* for any `| VP-… | high |` row, so one audit run "saw" an id for this check and not for
|
|
1098
|
+
* traceability (#833), and a row in a Known Issues table could raise a contradiction.
|
|
997
1099
|
*/
|
|
998
1100
|
export function priorityMismatches(
|
|
999
|
-
|
|
1101
|
+
viewpoints: ViewpointEntry[], scenarios: ScenarioInfo[],
|
|
1000
1102
|
): Array<{ id: string; declared: string; tagged: string; scenario: string }> {
|
|
1001
1103
|
const declared = new Map<string, string>();
|
|
1002
|
-
for (const
|
|
1003
|
-
declared.set(m[1].toUpperCase(), m[2].toLowerCase());
|
|
1004
|
-
}
|
|
1104
|
+
for (const v of viewpoints) if (v.priorityWord) declared.set(v.id.toUpperCase(), v.priorityWord);
|
|
1005
1105
|
if (declared.size === 0) return [];
|
|
1006
|
-
// `medium` and `normal` are the same rank under different names in real documents.
|
|
1007
|
-
const rank = (p: string): string => (p === 'medium' ? 'normal' : p);
|
|
1008
1106
|
const out: Array<{ id: string; declared: string; tagged: string; scenario: string }> = [];
|
|
1009
1107
|
for (const s of scenarios) {
|
|
1010
1108
|
const id = (s.vpId ?? '').toUpperCase();
|
|
@@ -1012,11 +1110,14 @@ export function priorityMismatches(
|
|
|
1012
1110
|
// The most specific declared row that prefixes this id.
|
|
1013
1111
|
const key = [...declared.keys()].filter((k) => id.startsWith(k)).sort((a, b) => b.length - a.length)[0];
|
|
1014
1112
|
if (!key) continue;
|
|
1015
|
-
|
|
1016
|
-
const
|
|
1017
|
-
// `
|
|
1018
|
-
if (want === '
|
|
1019
|
-
|
|
1113
|
+
// Both sides fold through the one priority reading (`critical` ≡ high, `medium`/`med` ≡ normal).
|
|
1114
|
+
const want = parsePriority(declared.get(key)!);
|
|
1115
|
+
// `deferred` and `skip` (declared not applicable, #853) have no tag equivalent — nothing to contradict.
|
|
1116
|
+
if (want === 'deferred' || want === 'skip') continue;
|
|
1117
|
+
// An untagged scenario states no priority, so it cannot contradict one (#861).
|
|
1118
|
+
const tagged = s.priorityTag ?? (s.priority === 'unknown' ? undefined : String(s.priority));
|
|
1119
|
+
if (!tagged) continue;
|
|
1120
|
+
if (want !== parsePriority(tagged)) out.push({ id: key, declared: declared.get(key)!, tagged, scenario: s.name });
|
|
1020
1121
|
}
|
|
1021
1122
|
return out;
|
|
1022
1123
|
}
|
|
@@ -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;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Generic: no project data.
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from 'fs';
|
|
12
|
+
import { keywordHit } from './keyword-match';
|
|
12
13
|
import { ScenarioInfo } from './parse';
|
|
13
14
|
import { readTextFile } from './read-text';
|
|
14
15
|
|
|
@@ -139,7 +140,11 @@ function directionCovered(block: string, dir: FrDirection, subject: string[]): b
|
|
|
139
140
|
const pool = lines.length ? lines : [block];
|
|
140
141
|
// Narrow to the lines about the clause's element when the suite names it the same way the spec
|
|
141
142
|
// does; a spec in one language and selector keys in another must not read as "no evidence".
|
|
142
|
-
|
|
143
|
+
// Word-aware (#779), or the narrowing lets in a line about something else and then trusts it:
|
|
144
|
+
// `under·score` is not a mention of `Score`, but it selected the only absence assertion in the
|
|
145
|
+
// block and so proved a MUST NOT clause about the Score. (`keywordHit` keeps substring semantics
|
|
146
|
+
// for a non-ASCII element name, which is what the cross-language case above depends on.)
|
|
147
|
+
const about = subject.length ? pool.filter((l) => subject.some((x) => keywordHit(l, x))) : [];
|
|
143
148
|
const scan = about.length ? about : pool;
|
|
144
149
|
return scan.some((l) => NEGATIVE_ASSERTION.test(l) === (dir.polarity === 'negate'));
|
|
145
150
|
}
|
|
@@ -269,7 +274,12 @@ export function specCoverage(specPath: string, scenarios: ScenarioInfo[], featur
|
|
|
269
274
|
const cited = featLower.includes(idLower) || cites.has(fr.id.toUpperCase());
|
|
270
275
|
const words = [...new Set((fr.text.toLowerCase().match(/[a-z][a-z-]{4,}/g) || []))]
|
|
271
276
|
.filter((w) => !/must|should|system|screen|users?|value|input|field/.test(w));
|
|
272
|
-
|
|
277
|
+
// Word-aware, not substring (#779). `haystack.includes('format')` is satisfied by
|
|
278
|
+
// in·format·ion and `includes('length')` by wave·length, and two such collisions clear the
|
|
279
|
+
// `>= 2 words` bar as easily as one — so a MUST clause nobody tested read as covered. The
|
|
280
|
+
// threshold is a separate judgement (is one word enough evidence?) and is unchanged; see
|
|
281
|
+
// keyword-match.ts for why the boundary is leading-only.
|
|
282
|
+
const kwHit = words.length > 0 && scenarios.some((s) => words.filter((w) => keywordHit(s.haystack, w)).length >= Math.min(2, words.length));
|
|
273
283
|
if (!cited && !kwHit) {
|
|
274
284
|
if (fr.modality === 'MUST') uncoveredMust.push({ id: fr.id, text: fr.text.slice(0, 90) });
|
|
275
285
|
continue;
|
|
@@ -279,7 +289,7 @@ export function specCoverage(specPath: string, scenarios: ScenarioInfo[], featur
|
|
|
279
289
|
// Which scenarios speak for this requirement — by its id (tags and comments sit in the block)
|
|
280
290
|
// or by the same keyword rule used above.
|
|
281
291
|
const owning = blocks.filter((b) => b.includes(idLower)
|
|
282
|
-
|| (words.length > 0 && words.filter((w) => b
|
|
292
|
+
|| (words.length > 0 && words.filter((w) => keywordHit(b, w)).length >= Math.min(2, words.length)));
|
|
283
293
|
// Cited from outside any scenario (a header comment): nothing to judge a direction against, so
|
|
284
294
|
// report nothing rather than invent a gap.
|
|
285
295
|
if (!owning.length) { frCovered += units; continue; }
|
|
@@ -299,7 +309,7 @@ export function specCoverage(specPath: string, scenarios: ScenarioInfo[], featur
|
|
|
299
309
|
// blocks that belong to this constraint: cite its msg code (primary), or mention a
|
|
300
310
|
// DISTINCTIVE constraint word (generic words filtered to avoid matching happy paths).
|
|
301
311
|
const own = blocks.filter((b) =>
|
|
302
|
-
(row.code && b.includes(row.code.toLowerCase())) || kw.some((w) => b
|
|
312
|
+
(row.code && b.includes(row.code.toLowerCase())) || kw.some((w) => keywordHit(b, w)),
|
|
303
313
|
);
|
|
304
314
|
if (!own.length) continue; // constraint has no scenarios at all — FR/viewpoint gate covers that
|
|
305
315
|
const found = [...new Set(own.flatMap((b) => actionTriggersIn(b)))];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Triage signal library — the ordered message patterns `sungen triage` matches a red scenario's
|
|
3
|
+
* error against. Seeded from the repair rules (`harness/repair.ts`, driver-mobile `mobile-repair.ts`);
|
|
4
|
+
* the patterns are COPIED, not imported, because core never loads a driver at runtime. The parity
|
|
5
|
+
* check in tests/harness/triage-classification.run.ts keeps the environment rules in step.
|
|
6
|
+
*
|
|
7
|
+
* Order matters and is the classification order: environment first (nothing about the test or the
|
|
8
|
+
* app can be judged when the session never started), then test defects (the test never reached the
|
|
9
|
+
* app). Assertions are decided in triage.ts from the parsed Expected/Received, not here.
|
|
10
|
+
*/
|
|
11
|
+
export type SignalClass = 'environment' | 'test-defect';
|
|
12
|
+
|
|
13
|
+
export interface TriageSignal {
|
|
14
|
+
id: string;
|
|
15
|
+
class: SignalClass;
|
|
16
|
+
match: RegExp;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const TRIAGE_SIGNALS: TriageSignal[] = [
|
|
20
|
+
// --- environment: the run could not reach a usable app/session -----------------------------------
|
|
21
|
+
{ id: 'env-network', class: 'environment', match: /net::ERR_[A-Z_]+|\bECONNREFUSED\b|\bENOTFOUND\b|\bEAI_AGAIN\b|\bETIMEDOUT\b/ },
|
|
22
|
+
// mirrors mobile-session-not-created
|
|
23
|
+
{ id: 'env-session-not-created', class: 'environment', match: /A new session could not be created|session not created/i },
|
|
24
|
+
// mirrors mobile-driver-proxy-lost
|
|
25
|
+
{ id: 'env-driver-proxy-lost', class: 'environment', match: /Could not proxy command to (?:the )?remote server|socket hang up|ECONNRESET/ },
|
|
26
|
+
// mirrors mobile-instrumentation-failed
|
|
27
|
+
{ id: 'env-instrumentation-failed', class: 'environment', match: /instrumentation process (?:cannot|could not) be (?:initialized|started)|Cannot start the instrumentation process/ },
|
|
28
|
+
// mirrors mobile-chromedriver-mismatch
|
|
29
|
+
{ id: 'env-chromedriver-mismatch', class: 'environment', match: /No Chromedriver found|chromedriver.*(?:version|mismatch)|Chrome version must be/i },
|
|
30
|
+
// mirrors mobile-webview-context-absent
|
|
31
|
+
{ id: 'env-webview-context-absent', class: 'environment', match: /no WEBVIEW\/CHROMIUM context appeared within/ },
|
|
32
|
+
{ id: 'env-auth-state-missing', class: 'environment', match: /storageState|\.auth\/[\w.-]+\.json|ENOENT[^\n]*\.auth/ },
|
|
33
|
+
{ id: 'env-base-url-unresolved', class: 'environment', match: /Cannot navigate to invalid URL|baseURL (?:is )?(?:not set|undefined)|Invalid URL/ },
|
|
34
|
+
{ id: 'env-browser-closed', class: 'environment', match: /Target page, context or browser has been closed|browserType\.launch/ },
|
|
35
|
+
|
|
36
|
+
// --- test defect: the test could not reach the element or its data -------------------------------
|
|
37
|
+
{ id: 'test-strict-mode', class: 'test-defect', match: /strict mode violation/ },
|
|
38
|
+
{ id: 'test-locator-unresolved', class: 'test-defect', match: /locator\.\w+: [^\n]*(?:Timeout|timeout)[^\n]*exceeded[\s\S]*waiting for |resolved to 0 elements/ },
|
|
39
|
+
{ id: 'test-element-absent', class: 'test-defect', match: /no such element|Element not found:|An element could not be located/ },
|
|
40
|
+
{ id: 'test-data-missing', class: 'test-defect', match: /Test data key not found:|@cases dataset "[^"]+" not found/ },
|
|
41
|
+
{ id: 'test-step-unmatched', class: 'test-defect', match: /\[sungen\] step "[^"]*" matched no pattern|\bSG\d{3}\b/ },
|
|
42
|
+
{ id: 'test-tap-intercepted', class: 'test-defect', match: /element click intercepted|is not clickable at point|element is obscured|intercepts pointer events/i },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/** A failure that is only Playwright's test-level timeout — no locator, no assertion detail (→ unclassified). */
|
|
46
|
+
export const BARE_TIMEOUT = /^(?:Error:\s*)?Test timeout of \d+ms exceeded\.?$/;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sungen triage` IO: find a unit's feature(s) and result file(s), join them on the same seam
|
|
3
|
+
* delivery and the dashboard use (`mergeFeatureAndSpec` + `resolveResultVariants`), classify each
|
|
4
|
+
* scenario, and persist `.sungen/triage/<unit>.json`. Read-only apart from that one report file.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { parseFeatureMetadata } from '../exporters/feature-parser';
|
|
9
|
+
import { loadPlaywrightReport, resolveResultsPath } from '../exporters/playwright-report-parser';
|
|
10
|
+
import { collectQaEnvSecretDenylist } from '../exporters/qa-env-secret-denylist';
|
|
11
|
+
import { isSensitiveKey } from '../exporters/sensitive-key';
|
|
12
|
+
import { resolveResultVariants } from '../exporters/result-variants';
|
|
13
|
+
import { mergeFeatureAndSpec } from '../exporters/scenario-merger';
|
|
14
|
+
import { parseSpecFile } from '../exporters/spec-parser';
|
|
15
|
+
import { loadTestData } from '../exporters/test-data-resolver';
|
|
16
|
+
import type { ResolvedUnit } from '../cli/resolve-unit';
|
|
17
|
+
import { classifyScenario, TriageClass, TriageRow } from './triage';
|
|
18
|
+
|
|
19
|
+
export interface TriageReport {
|
|
20
|
+
unit: string;
|
|
21
|
+
generatedAt: string;
|
|
22
|
+
/** Result files read, relative to the project root. Empty → nothing has run yet. */
|
|
23
|
+
resultFiles: string[];
|
|
24
|
+
counts: Partial<Record<TriageClass, number>>;
|
|
25
|
+
rows: TriageRow[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Values that must never reach a triage evidence line or a defect record: the resolved `${QA_*}`
|
|
30
|
+
* secrets AND the literal values of credential-named test-data keys — an assertion on a password
|
|
31
|
+
* field prints that value in its Expected/Received.
|
|
32
|
+
*/
|
|
33
|
+
export function unitSecrets(testData: Record<string, string>, cwd: string): string[] {
|
|
34
|
+
const literal = Object.entries(testData)
|
|
35
|
+
.filter(([k, v]) => isSensitiveKey(k) && typeof v === 'string' && v.length >= 3 && !v.includes('${'))
|
|
36
|
+
.map(([, v]) => String(v));
|
|
37
|
+
return [...new Set([...collectQaEnvSecretDenylist(testData, cwd).values, ...literal])].sort((a, b) => b.length - a.length);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function generatedDir(cwd: string, u: ResolvedUnit): string {
|
|
41
|
+
return path.join(cwd, 'specs', 'generated', u.unitId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Every result file of one feature, labelled by variant: `<base>-test-result.json` (no label),
|
|
46
|
+
* `<base>-test-result.<os|env>[.<env>].json` (label = the middle part). A mobile unit has only
|
|
47
|
+
* per-OS files, so all of them are read rather than the first one found. Falls back to the shared
|
|
48
|
+
* lookup (incl. `test-results/results.json`) when the generated dir holds none.
|
|
49
|
+
*/
|
|
50
|
+
function resultFilesFor(cwd: string, genDir: string, base: string): Array<{ file: string; variant?: string }> {
|
|
51
|
+
const prefix = `${base}-test-result`;
|
|
52
|
+
const own = fs.existsSync(genDir)
|
|
53
|
+
? fs.readdirSync(genDir).filter((f) => f.startsWith(prefix) && f.endsWith('.json')).sort()
|
|
54
|
+
: [];
|
|
55
|
+
if (own.length > 0) {
|
|
56
|
+
return own.map((f) => {
|
|
57
|
+
const variant = f.slice(prefix.length, -'.json'.length).replace(/^\./, '');
|
|
58
|
+
return { file: path.join(genDir, f), ...(variant ? { variant } : {}) };
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const shared = resolveResultsPath(cwd, genDir, base);
|
|
62
|
+
return shared ? [{ file: shared }] : [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function triageUnit(cwd: string, u: ResolvedUnit): TriageReport {
|
|
66
|
+
const genDir = generatedDir(cwd, u);
|
|
67
|
+
const featuresDir = path.join(u.dir, 'features');
|
|
68
|
+
const features = fs.existsSync(featuresDir) ? fs.readdirSync(featuresDir).filter((f) => f.endsWith('.feature')).sort() : [];
|
|
69
|
+
const rows: TriageRow[] = [];
|
|
70
|
+
const resultFiles: string[] = [];
|
|
71
|
+
|
|
72
|
+
for (const f of features) {
|
|
73
|
+
const base = f.replace(/\.feature$/, '');
|
|
74
|
+
const files = resultFilesFor(cwd, genDir, base);
|
|
75
|
+
if (files.length === 0) continue;
|
|
76
|
+
const merged = mergeFeatureAndSpec(parseFeatureMetadata(path.join(featuresDir, f)), parseSpecFile(path.join(genDir, `${base}.spec.ts`)));
|
|
77
|
+
const dataFile = path.join(u.dir, 'test-data', `${base}.yaml`);
|
|
78
|
+
const testData = fs.existsSync(dataFile) ? loadTestData(dataFile) : {};
|
|
79
|
+
const secrets = unitSecrets(testData, cwd);
|
|
80
|
+
|
|
81
|
+
for (const { file, variant } of files) {
|
|
82
|
+
resultFiles.push(path.relative(cwd, file));
|
|
83
|
+
const results = loadPlaywrightReport(file);
|
|
84
|
+
for (const m of merged) {
|
|
85
|
+
for (const v of resolveResultVariants(m, results, testData)) {
|
|
86
|
+
const r = classifyScenario({ unit: u.unitId, title: `${m.feature.name}${v.nameSuffix}`, tags: m.feature.tags, result: v.result, variant }, secrets);
|
|
87
|
+
if (r) rows.push(r);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const counts: Partial<Record<TriageClass, number>> = {};
|
|
94
|
+
for (const r of rows) counts[r.class] = (counts[r.class] ?? 0) + 1;
|
|
95
|
+
return { unit: u.unitId, generatedAt: new Date().toISOString(), resultFiles, counts, rows };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function triageReportPath(cwd: string, unitId: string): string {
|
|
99
|
+
return path.join(cwd, '.sungen', 'triage', `${unitId.replace(/\//g, '-')}.json`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The last triage of a unit, or null when `sungen triage` has not run for it. */
|
|
103
|
+
export function readTriageReport(cwd: string, unitId: string): TriageReport | null {
|
|
104
|
+
try { return JSON.parse(fs.readFileSync(triageReportPath(cwd, unitId), 'utf-8')) as TriageReport; } catch { return null; }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Persist under `.sungen/triage/<unit>.json` (a flow `flows/x` → `flows-x.json`); returns the path. */
|
|
108
|
+
export function writeTriageReport(cwd: string, report: TriageReport): string {
|
|
109
|
+
const out = triageReportPath(cwd, report.unit);
|
|
110
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
111
|
+
fs.writeFileSync(out, `${JSON.stringify(report, null, 2)}\n`, 'utf-8');
|
|
112
|
+
return out;
|
|
113
|
+
}
|