@sun-asterisk/sungen 3.2.24 → 3.2.26
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/capabilities/context-router.d.ts.map +1 -1
- package/dist/capabilities/context-router.js +15 -7
- package/dist/capabilities/context-router.js.map +1 -1
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +23 -4
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/capability.d.ts.map +1 -1
- package/dist/cli/commands/capability.js +6 -48
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/changed.d.ts +14 -0
- package/dist/cli/commands/changed.d.ts.map +1 -0
- package/dist/cli/commands/changed.js +128 -0
- package/dist/cli/commands/changed.js.map +1 -0
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +25 -1
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/generate.js +1 -1
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/results-merge.d.ts +19 -0
- package/dist/cli/commands/results-merge.d.ts.map +1 -0
- package/dist/cli/commands/results-merge.js +191 -0
- package/dist/cli/commands/results-merge.js.map +1 -0
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +100 -12
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/exporters/api-catalog-loader.d.ts.map +1 -1
- package/dist/exporters/api-catalog-loader.js +10 -3
- package/dist/exporters/api-catalog-loader.js.map +1 -1
- package/dist/exporters/api-testcase-formatter.d.ts +9 -0
- package/dist/exporters/api-testcase-formatter.d.ts.map +1 -1
- package/dist/exporters/api-testcase-formatter.js +55 -3
- package/dist/exporters/api-testcase-formatter.js.map +1 -1
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +10 -1
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/json-exporter.d.ts.map +1 -1
- package/dist/exporters/json-exporter.js +6 -1
- package/dist/exporters/json-exporter.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +14 -1
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/export.js +1 -1
- package/dist/exporters/matrix/export.js.map +1 -1
- package/dist/exporters/matrix/map-loader.d.ts.map +1 -1
- package/dist/exporters/matrix/map-loader.js +5 -0
- package/dist/exporters/matrix/map-loader.js.map +1 -1
- package/dist/exporters/matrix/render-csv.d.ts.map +1 -1
- package/dist/exporters/matrix/render-csv.js +5 -1
- package/dist/exporters/matrix/render-csv.js.map +1 -1
- package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
- package/dist/exporters/matrix/render-xlsx.js +4 -1
- package/dist/exporters/matrix/render-xlsx.js.map +1 -1
- package/dist/exporters/matrix/types.d.ts +18 -0
- package/dist/exporters/matrix/types.d.ts.map +1 -1
- package/dist/exporters/matrix/types.js.map +1 -1
- package/dist/exporters/playwright-report-parser.d.ts +17 -2
- package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
- package/dist/exporters/playwright-report-parser.js +68 -8
- package/dist/exporters/playwright-report-parser.js.map +1 -1
- package/dist/exporters/results-merge.d.ts +46 -0
- package/dist/exporters/results-merge.d.ts.map +1 -0
- package/dist/exporters/results-merge.js +148 -0
- package/dist/exporters/results-merge.js.map +1 -0
- package/dist/exporters/selector-key-resolver.d.ts +4 -0
- package/dist/exporters/selector-key-resolver.d.ts.map +1 -1
- package/dist/exporters/selector-key-resolver.js +101 -1
- package/dist/exporters/selector-key-resolver.js.map +1 -1
- package/dist/exporters/types.d.ts +8 -1
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
- package/dist/exporters/xlsx-report-builder.js +5 -4
- package/dist/exporters/xlsx-report-builder.js.map +1 -1
- package/dist/generators/gherkin-parser/index.d.ts +2 -0
- package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
- package/dist/generators/gherkin-parser/index.js +52 -1
- package/dist/generators/gherkin-parser/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +20 -10
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +2 -11
- package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/appium-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/imports.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/date-picker-unsupported.hbs +4 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/lifecycle-missing-app-id.hbs +5 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/matches-format-assertion.hbs +15 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +5 -2
- package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +185 -26
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +2 -11
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +17 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +6 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +18 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +5 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/matches-format-assertion.hbs +5 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +4 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +4 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -1
- package/dist/generators/test-generator/code-generator.d.ts +6 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +80 -7
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +60 -6
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +59 -5
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/patterns/expect-patterns.d.ts +7 -0
- package/dist/generators/test-generator/patterns/expect-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/expect-patterns.js +36 -2
- package/dist/generators/test-generator/patterns/expect-patterns.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts +19 -12
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +58 -38
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/patterns/types.d.ts +23 -0
- package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/unsupported-step-code.d.ts +18 -0
- package/dist/generators/test-generator/patterns/unsupported-step-code.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/unsupported-step-code.js +23 -0
- package/dist/generators/test-generator/patterns/unsupported-step-code.js.map +1 -0
- package/dist/generators/test-generator/recipe-expander.js +9 -1
- package/dist/generators/test-generator/recipe-expander.js.map +1 -1
- package/dist/generators/test-generator/reset-tier.d.ts +68 -0
- package/dist/generators/test-generator/reset-tier.d.ts.map +1 -0
- package/dist/generators/test-generator/reset-tier.js +94 -0
- package/dist/generators/test-generator/reset-tier.js.map +1 -0
- package/dist/generators/test-generator/step-mapper.d.ts +39 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +144 -5
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +2 -10
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +16 -2
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/format-pattern.d.ts +35 -0
- package/dist/generators/test-generator/utils/format-pattern.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/format-pattern.js +82 -0
- package/dist/generators/test-generator/utils/format-pattern.js.map +1 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.d.ts +42 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.js +142 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.js.map +1 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.d.ts +18 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.js +25 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.js.map +1 -0
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +9 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.js +124 -63
- package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +20 -1
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/generators/test-generator/utils/spec-syntax-check.d.ts +14 -0
- package/dist/generators/test-generator/utils/spec-syntax-check.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/spec-syntax-check.js +26 -0
- package/dist/generators/test-generator/utils/spec-syntax-check.js.map +1 -0
- package/dist/harness/annotation-overrides.d.ts +0 -12
- package/dist/harness/annotation-overrides.d.ts.map +1 -1
- package/dist/harness/annotation-overrides.js +9 -5
- package/dist/harness/annotation-overrides.js.map +1 -1
- package/dist/harness/audit.d.ts +7 -0
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +78 -10
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability-plan.d.ts +2 -1
- package/dist/harness/capability-plan.d.ts.map +1 -1
- package/dist/harness/capability-plan.js +8 -3
- package/dist/harness/capability-plan.js.map +1 -1
- package/dist/harness/capability.d.ts +11 -0
- package/dist/harness/capability.d.ts.map +1 -1
- package/dist/harness/capability.js +8 -0
- package/dist/harness/capability.js.map +1 -1
- package/dist/harness/catalog/drivers.yaml +5 -0
- package/dist/harness/flow-contract.d.ts +18 -1
- package/dist/harness/flow-contract.d.ts.map +1 -1
- package/dist/harness/flow-contract.js +72 -9
- package/dist/harness/flow-contract.js.map +1 -1
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +5 -1
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/quality-gates.d.ts +12 -1
- package/dist/harness/quality-gates.d.ts.map +1 -1
- package/dist/harness/quality-gates.js +62 -7
- package/dist/harness/quality-gates.js.map +1 -1
- package/dist/harness/run-manifest.d.ts +58 -0
- package/dist/harness/run-manifest.d.ts.map +1 -0
- package/dist/harness/run-manifest.js +264 -0
- package/dist/harness/run-manifest.js.map +1 -0
- package/dist/harness/spec-branches.d.ts +88 -0
- package/dist/harness/spec-branches.d.ts.map +1 -0
- package/dist/harness/spec-branches.js +280 -0
- package/dist/harness/spec-branches.js.map +1 -0
- package/dist/harness/spec-coverage.d.ts +1 -1
- package/dist/harness/spec-coverage.js +4 -4
- package/dist/harness/spec-coverage.js.map +1 -1
- package/dist/harness/viewpoint-baseline.d.ts +9 -0
- package/dist/harness/viewpoint-baseline.d.ts.map +1 -1
- package/dist/harness/viewpoint-baseline.js +33 -3
- package/dist/harness/viewpoint-baseline.js.map +1 -1
- package/dist/harness/viewpoint-ledger.d.ts +1 -1
- package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
- package/dist/harness/viewpoint-ledger.js +88 -9
- package/dist/harness/viewpoint-ledger.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/lockstep-version.d.ts +22 -0
- package/dist/orchestrator/lockstep-version.d.ts.map +1 -0
- package/dist/orchestrator/lockstep-version.js +58 -0
- package/dist/orchestrator/lockstep-version.js.map +1 -0
- package/dist/orchestrator/project-deps-sync.d.ts +39 -0
- package/dist/orchestrator/project-deps-sync.d.ts.map +1 -0
- package/dist/orchestrator/project-deps-sync.js +175 -0
- package/dist/orchestrator/project-deps-sync.js.map +1 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +5 -2
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/add-flow.md +16 -0
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +9 -0
- package/dist/orchestrator/templates/ai-src/commands/delivery.md +9 -2
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +109 -0
- package/dist/orchestrator/templates/ai-src/config/claude.md +1 -1
- package/dist/orchestrator/templates/ai-src/config/copilot.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +49 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -3
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +110 -6
- package/dist/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +8 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +40 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
- package/dist/orchestrator/templates/env.appium.example +10 -0
- package/dist/orchestrator/templates/specs-api.d.ts +4 -2
- package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-api.js +233 -10
- package/dist/orchestrator/templates/specs-api.js.map +1 -1
- package/dist/orchestrator/templates/specs-api.ts +214 -11
- package/dist/orchestrator/templates/specs-regex-escape.d.ts +13 -0
- package/dist/orchestrator/templates/specs-regex-escape.d.ts.map +1 -0
- package/dist/orchestrator/templates/specs-regex-escape.js +19 -0
- package/dist/orchestrator/templates/specs-regex-escape.js.map +1 -0
- package/dist/orchestrator/templates/specs-regex-escape.ts +16 -0
- package/dist/orchestrator/templates/specs-test-data.ts +53 -6
- package/dist/orchestrator/templates/wdio.conf.ts +28 -9
- package/package.json +8 -6
- package/src/capabilities/context-router.ts +13 -5
- package/src/cli/commands/audit.ts +22 -3
- package/src/cli/commands/capability.ts +4 -38
- package/src/cli/commands/changed.ts +104 -0
- package/src/cli/commands/delivery.ts +27 -2
- package/src/cli/commands/generate.ts +1 -1
- package/src/cli/commands/results-merge.ts +153 -0
- package/src/cli/commands/update.ts +72 -13
- package/src/cli/index.ts +4 -0
- package/src/exporters/api-catalog-loader.ts +10 -2
- package/src/exporters/api-testcase-formatter.ts +59 -3
- package/src/exporters/csv-exporter.ts +11 -2
- package/src/exporters/json-exporter.ts +7 -2
- package/src/exporters/matrix/build.ts +13 -1
- package/src/exporters/matrix/export.ts +1 -1
- package/src/exporters/matrix/map-loader.ts +5 -0
- package/src/exporters/matrix/render-csv.ts +5 -1
- package/src/exporters/matrix/render-xlsx.ts +4 -1
- package/src/exporters/matrix/types.ts +18 -0
- package/src/exporters/playwright-report-parser.ts +68 -9
- package/src/exporters/results-merge.ts +150 -0
- package/src/exporters/selector-key-resolver.ts +81 -1
- package/src/exporters/types.ts +8 -1
- package/src/exporters/xlsx-report-builder.ts +5 -4
- package/src/generators/gherkin-parser/index.ts +55 -1
- package/src/generators/test-generator/adapters/adapter-interface.ts +23 -1
- package/src/generators/test-generator/adapters/appium/appium-adapter.ts +2 -2
- package/src/generators/test-generator/adapters/appium/templates/imports.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/date-picker-unsupported.hbs +4 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/lifecycle-missing-app-id.hbs +5 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/matches-format-assertion.hbs +15 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +5 -2
- package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +185 -26
- package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +2 -2
- package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +17 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +6 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +18 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +5 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/matches-format-assertion.hbs +5 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +4 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +4 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -1
- package/src/generators/test-generator/code-generator.ts +86 -7
- package/src/generators/test-generator/diagnostics.ts +60 -6
- package/src/generators/test-generator/patterns/expect-patterns.ts +37 -2
- package/src/generators/test-generator/patterns/index.ts +58 -33
- package/src/generators/test-generator/patterns/types.ts +36 -0
- package/src/generators/test-generator/patterns/unsupported-step-code.ts +19 -0
- package/src/generators/test-generator/recipe-expander.ts +9 -1
- package/src/generators/test-generator/reset-tier.ts +93 -0
- package/src/generators/test-generator/step-mapper.ts +149 -6
- package/src/generators/test-generator/template-engine.ts +18 -3
- package/src/generators/test-generator/utils/format-pattern.ts +79 -0
- package/src/generators/test-generator/utils/js-literal-scanner.ts +132 -0
- package/src/generators/test-generator/utils/mask-refs-for-matching.ts +21 -0
- package/src/generators/test-generator/utils/runtime-data-transformer.ts +126 -76
- package/src/generators/test-generator/utils/selector-resolver.ts +22 -1
- package/src/generators/test-generator/utils/spec-syntax-check.ts +22 -0
- package/src/harness/annotation-overrides.ts +10 -5
- package/src/harness/audit.ts +82 -12
- package/src/harness/capability-plan.ts +10 -5
- package/src/harness/capability.ts +16 -0
- package/src/harness/catalog/drivers.yaml +5 -0
- package/src/harness/flow-contract.ts +87 -9
- package/src/harness/parse.ts +5 -1
- package/src/harness/quality-gates.ts +64 -6
- package/src/harness/run-manifest.ts +254 -0
- package/src/harness/spec-branches.ts +346 -0
- package/src/harness/spec-coverage.ts +4 -4
- package/src/harness/viewpoint-baseline.ts +41 -6
- package/src/harness/viewpoint-ledger.ts +82 -9
- package/src/index.ts +1 -0
- package/src/orchestrator/lockstep-version.ts +46 -0
- package/src/orchestrator/project-deps-sync.ts +175 -0
- package/src/orchestrator/project-initializer.ts +5 -2
- package/src/orchestrator/templates/ai-src/commands/add-flow.md +16 -0
- package/src/orchestrator/templates/ai-src/commands/create-test.md +9 -0
- package/src/orchestrator/templates/ai-src/commands/delivery.md +9 -2
- package/src/orchestrator/templates/ai-src/commands/run-test.md +109 -0
- package/src/orchestrator/templates/ai-src/config/claude.md +1 -1
- package/src/orchestrator/templates/ai-src/config/copilot.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +49 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -3
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +110 -6
- package/src/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +8 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +40 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
- package/src/orchestrator/templates/env.appium.example +10 -0
- package/src/orchestrator/templates/specs-api.ts +214 -11
- package/src/orchestrator/templates/specs-regex-escape.ts +16 -0
- package/src/orchestrator/templates/specs-test-data.ts +53 -6
- package/src/orchestrator/templates/wdio.conf.ts +28 -9
|
@@ -9,43 +9,9 @@ import {
|
|
|
9
9
|
import { buildPlan, MANUAL_REASONS } from '../../harness/capability-plan';
|
|
10
10
|
import { adapterRegistry } from '../../generators/test-generator/adapters';
|
|
11
11
|
import { emitMobileRuntime, installMobileDriver } from '../../orchestrator/mobile-runtime-scaffolder';
|
|
12
|
+
import { coreVersion, resolveDriverVersion } from '../../orchestrator/lockstep-version';
|
|
12
13
|
import { findUnitDir, resolveUnit } from '../resolve-unit';
|
|
13
14
|
|
|
14
|
-
/** The running CLI's (core) version — opt-in drivers install in lockstep with it (not `@latest`). */
|
|
15
|
-
function coreVersion(): string {
|
|
16
|
-
try { return require('../../../package.json').version || 'latest'; } catch { return 'latest'; }
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Resolve the driver version to install for the running core `core`.
|
|
21
|
-
*
|
|
22
|
-
* Lockstep wants the EXACT `core` version. But the beta channel publishes `<core>-beta.N` before a
|
|
23
|
-
* stable `<core>` ever exists — so a released core (e.g. 3.2.10) asking for `@3.2.10` hits ETARGET
|
|
24
|
-
* out-of-the-box when npm only has `3.2.10-beta.2` (`sungen capability add` then fails for a fresh
|
|
25
|
-
* user). Fall back on the SAME version line: prefer exact `core`, else the highest `core-beta.N`,
|
|
26
|
-
* else the `beta` dist-tag as a last resort. Keeps the driver's pinned core-dep on the same line.
|
|
27
|
-
*/
|
|
28
|
-
function resolveDriverVersion(pkg: string, core: string): string {
|
|
29
|
-
if (core === 'latest') return 'beta';
|
|
30
|
-
try {
|
|
31
|
-
const out = spawnSync('npm', ['view', pkg, 'versions', '--json'], { encoding: 'utf-8', shell: true });
|
|
32
|
-
if (out.status !== 0 || !out.stdout) return core; // can't query → let the exact install try/fail
|
|
33
|
-
let versions: string[] = [];
|
|
34
|
-
const parsed = JSON.parse(out.stdout);
|
|
35
|
-
versions = Array.isArray(parsed) ? parsed : [parsed];
|
|
36
|
-
if (versions.includes(core)) return core; // exact exists → lockstep
|
|
37
|
-
const prefix = `${core}-beta.`;
|
|
38
|
-
const betas = versions
|
|
39
|
-
.filter((v) => typeof v === 'string' && v.startsWith(prefix))
|
|
40
|
-
.map((v) => parseInt(v.slice(prefix.length), 10))
|
|
41
|
-
.filter((n) => Number.isInteger(n));
|
|
42
|
-
if (betas.length) return `${core}-beta.${Math.max(...betas)}`; // highest same-line prerelease
|
|
43
|
-
return 'beta'; // nothing on this line → newest beta dist-tag
|
|
44
|
-
} catch {
|
|
45
|
-
return core;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
15
|
/**
|
|
50
16
|
* Add `test:mobile` to the project's package.json scripts (if absent).
|
|
51
17
|
* Idempotent — skips if the script is already set.
|
|
@@ -140,7 +106,7 @@ export function registerCapabilityCommand(program: Command): void {
|
|
|
140
106
|
const resolved = resolveUnit(o.screen);
|
|
141
107
|
if (!resolved) throw new Error(`Screen/flow not found: qa/screens/${o.screen} or qa/flows/${o.screen}`);
|
|
142
108
|
// The catalog unit id carries the unit KIND, which gates which drivers can serve it.
|
|
143
|
-
const plan = buildPlan(resolved.dir, o.screen, resolved.unitId);
|
|
109
|
+
const plan = buildPlan(resolved.dir, o.screen, resolved.unitId, readCapabilities(process.cwd()).platform);
|
|
144
110
|
|
|
145
111
|
const outDir = path.join(process.cwd(), '.sungen', 'reports');
|
|
146
112
|
fs.mkdirSync(outDir, { recursive: true });
|
|
@@ -166,9 +132,9 @@ export function registerCapabilityCommand(program: Command): void {
|
|
|
166
132
|
}
|
|
167
133
|
}
|
|
168
134
|
if (plan.unavailable.length) {
|
|
169
|
-
L(' Cannot help here (driver
|
|
135
|
+
L(' Cannot help here (driver cannot serve this unit kind or platform — do NOT install):');
|
|
170
136
|
for (const r of plan.unavailable) {
|
|
171
|
-
L(` • ${r.count} @manual:${r.reason} → \`${r.driver}\`
|
|
137
|
+
L(` • ${r.count} @manual:${r.reason} → \`${r.driver}\` ${r.why}`);
|
|
172
138
|
}
|
|
173
139
|
}
|
|
174
140
|
if (plan.keep.length) {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sungen changed` — which scenarios does the current edit oblige a re-run of?
|
|
3
|
+
*
|
|
4
|
+
* Backs `run-test --change-only`. The selection is computed here rather than in the AI workflow so
|
|
5
|
+
* it is deterministic and testable: the same edit always yields the same list, and the list can be
|
|
6
|
+
* asserted in CI. The workflow's job is only to run it, merge, and record.
|
|
7
|
+
*
|
|
8
|
+
* Bias: when an edit cannot be attributed to specific scenarios, EVERY scenario is selected and the
|
|
9
|
+
* output says so. Over-running costs minutes; under-running reports a scenario green that never
|
|
10
|
+
* re-ran after the change that broke it.
|
|
11
|
+
*/
|
|
12
|
+
import { Command } from 'commander';
|
|
13
|
+
import { buildRunManifest, readRunManifest, writeRunManifest, selectChanged } from '../../harness/run-manifest';
|
|
14
|
+
import { findUnitDir } from '../resolve-unit';
|
|
15
|
+
import { execFileSync } from 'child_process';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Run-affecting files git reports changed for this unit, or [] when git has no usable opinion.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately narrow and fail-quiet:
|
|
22
|
+
* - not a repo, or the unit is UNTRACKED → [] (git knows nothing; the dogfood project is exactly
|
|
23
|
+
* this case, and treating "untracked" as "changed" would widen on every single run);
|
|
24
|
+
* - only `features/`, `selectors/`, `test-data/` count — a spec.md or screenshot edit is
|
|
25
|
+
* regeneration territory (`sungen manifest --diff`), not a reason to re-run;
|
|
26
|
+
* - `diff HEAD` so staged and unstaged edits both count.
|
|
27
|
+
*/
|
|
28
|
+
function gitChangedFiles(unitDir: string): string[] {
|
|
29
|
+
const rel = path.relative(process.cwd(), unitDir) || '.';
|
|
30
|
+
const git = (args: string[]): string =>
|
|
31
|
+
execFileSync('git', args, { cwd: process.cwd(), encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
32
|
+
try {
|
|
33
|
+
if (!git(['ls-files', rel]).trim()) return []; // untracked unit → git has no baseline
|
|
34
|
+
return git(['diff', 'HEAD', '--name-only', '--', rel])
|
|
35
|
+
.split('\n')
|
|
36
|
+
.map((l) => l.trim())
|
|
37
|
+
.filter((l) => /\/(features|selectors|test-data)\//.test(l));
|
|
38
|
+
} catch {
|
|
39
|
+
return []; // no git, no repo, detached weirdness — the fingerprint still decides
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Escape a scenario title for a `-g` / `--mochaOpts.grep` regex. */
|
|
44
|
+
const toGrep = (titles: string[]): string =>
|
|
45
|
+
titles.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
|
|
46
|
+
|
|
47
|
+
export function registerChangedCommand(program: Command): void {
|
|
48
|
+
program
|
|
49
|
+
.command('changed')
|
|
50
|
+
.description('List the scenarios a selectors/test-data/feature edit obliges you to re-run (backs run-test --change-only)')
|
|
51
|
+
.option('-s, --screen <name>', 'Screen or flow name')
|
|
52
|
+
.option('--api <name>', 'API-first area or api flow (qa/api/<name>)')
|
|
53
|
+
.option('--area <name>', 'Alias of --api')
|
|
54
|
+
.option('--json', 'Output the raw selection as JSON')
|
|
55
|
+
.option('--grep', 'Output ONLY a regex of the selected titles, for -g / --mochaOpts.grep')
|
|
56
|
+
.option('--record', 'Record the current inputs as the new baseline (run this AFTER a successful run)')
|
|
57
|
+
.action((options) => {
|
|
58
|
+
try {
|
|
59
|
+
const name = options.screen || options.api || options.area;
|
|
60
|
+
if (!name) throw new Error('Provide --screen <name> (or --api <area>)');
|
|
61
|
+
const dir = findUnitDir(name);
|
|
62
|
+
if (!dir) throw new Error(`Not found: qa/screens|flows|api/${name}`);
|
|
63
|
+
|
|
64
|
+
const cwd = process.cwd();
|
|
65
|
+
const after = buildRunManifest(cwd, dir, name);
|
|
66
|
+
|
|
67
|
+
if (options.record) {
|
|
68
|
+
const p = writeRunManifest(cwd, name, after);
|
|
69
|
+
console.log(`✓ recorded run baseline for ${name} → ${p.replace(`${cwd}/`, '')} (${Object.keys(after.scenarios).length} scenarios)`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const selection = selectChanged(readRunManifest(cwd, name), after, gitChangedFiles(dir));
|
|
74
|
+
|
|
75
|
+
if (options.grep) {
|
|
76
|
+
// Empty output is meaningful: nothing to run. A caller must treat "" as "skip the run",
|
|
77
|
+
// never as "match everything" — an empty grep would run the whole suite.
|
|
78
|
+
console.log(toGrep(selection.scenarios));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (options.json) { console.log(JSON.stringify(selection, null, 2)); return; }
|
|
82
|
+
|
|
83
|
+
const { total, selected, firstRun } = selection.summary;
|
|
84
|
+
console.log(`\n━━━ Change selection: ${name} ━━━`);
|
|
85
|
+
if (!selected) {
|
|
86
|
+
console.log(` ✓ Nothing changed since the last recorded run — ${total} scenario(s) still current.\n`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (selection.all) {
|
|
90
|
+
console.log(` ⚠ ALL ${total} scenario(s) selected — ${selection.reasons[selection.scenarios[0]]}.`);
|
|
91
|
+
console.log(firstRun
|
|
92
|
+
? ' Record a baseline after this run so the next one can narrow: sungen changed --screen ' + name + ' --record\n'
|
|
93
|
+
: ' An unattributable edit widens to the whole unit on purpose — a missed scenario would report green without re-running.\n');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
console.log(` ${selected} of ${total} scenario(s) need a re-run:\n`);
|
|
97
|
+
for (const t of selection.scenarios) console.log(` ↻ ${t}\n ${selection.reasons[t]}`);
|
|
98
|
+
console.log('');
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
101
|
+
process.exitCode = 1;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -436,7 +436,8 @@ async function exportTarget(
|
|
|
436
436
|
const specLink = fs.existsSync(specMdFile) ? path.relative(cwd, specMdFile) : '';
|
|
437
437
|
const explicitEnv = process.env.SUNGEN_ENV;
|
|
438
438
|
|
|
439
|
-
//
|
|
439
|
+
// Load the endpoint catalog for every kind: api-kind gets the API detail sheet;
|
|
440
|
+
// screen/flow rows name their @api precondition endpoints from it.
|
|
440
441
|
const apiCatalog = loadApiCatalog(cwd, target.kind, target.screen);
|
|
441
442
|
const hasApiCatalog = target.kind === 'api' && Object.keys(apiCatalog).length > 0;
|
|
442
443
|
|
|
@@ -672,8 +673,21 @@ async function exportMatrixTarget(
|
|
|
672
673
|
cwd: string,
|
|
673
674
|
target: DeliveryTarget,
|
|
674
675
|
opts: { check?: boolean; preview?: boolean; format?: MatrixFormat },
|
|
675
|
-
): Promise<MatrixRunSummary | null> {
|
|
676
|
+
): Promise<MatrixRunSummary | 'skipped' | null> {
|
|
676
677
|
const paths = matrixPathsFor(cwd, target);
|
|
678
|
+
|
|
679
|
+
// A feature whose scenarios are ALL @exploration probes has nothing to deliver —
|
|
680
|
+
// skip it quietly (no map required) instead of demanding a reviewed grouping for
|
|
681
|
+
// probes that are, by contract, outside the official suite.
|
|
682
|
+
{
|
|
683
|
+
const featureMeta = parseFeatureMetadata(paths.featureFile);
|
|
684
|
+
const deliverable = featureMeta.scenarios.filter((s) => !s.tags.some((t) => /^@exploration\b/i.test(t)));
|
|
685
|
+
if (featureMeta.scenarios.length > 0 && deliverable.length === 0) {
|
|
686
|
+
log(`${COLOR.gray}${paths.label}: nothing to deliver — all ${featureMeta.scenarios.length} scenario(s) are @exploration probes (outside the official suite).${COLOR.reset}`);
|
|
687
|
+
return 'skipped';
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
677
691
|
const { model, mapMissing, mapErrors } = loadMatrixModel(paths);
|
|
678
692
|
|
|
679
693
|
if (mapMissing) {
|
|
@@ -937,6 +951,16 @@ export function registerDeliveryCommand(program: Command): void {
|
|
|
937
951
|
let blocked = false;
|
|
938
952
|
for (const target of toExport) {
|
|
939
953
|
const paths = matrixPathsFor(cwd, target);
|
|
954
|
+
// Same contract as the export path: an all-@exploration feature has no
|
|
955
|
+
// deliverable variants, so there is no map to approve — skip, not an error.
|
|
956
|
+
{
|
|
957
|
+
const featureMeta = parseFeatureMetadata(paths.featureFile);
|
|
958
|
+
if (featureMeta.scenarios.length > 0
|
|
959
|
+
&& featureMeta.scenarios.every((s) => s.tags.some((t) => /^@exploration\b/i.test(t)))) {
|
|
960
|
+
log(`${COLOR.gray}${paths.label}: nothing to approve — all scenario(s) are @exploration probes.${COLOR.reset}`);
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
940
964
|
try {
|
|
941
965
|
const { findings, approved } = approveMatrix(paths, ids);
|
|
942
966
|
if (approved.length === 0) {
|
|
@@ -961,6 +985,7 @@ export function registerDeliveryCommand(program: Command): void {
|
|
|
961
985
|
const s = await exportMatrixTarget(cwd, target, {
|
|
962
986
|
check: options.check, preview: options.preview, format: format as MatrixFormat,
|
|
963
987
|
});
|
|
988
|
+
if (s === 'skipped') continue; // all-@exploration feature — neither exported nor a failure
|
|
964
989
|
if (s) matrixSummaries.push(s);
|
|
965
990
|
else notExported.push(target.isFlow ? `flow/${target.featureBaseName}` : target.featureBaseName);
|
|
966
991
|
}
|
|
@@ -200,7 +200,7 @@ export function registerGenerateCommand(program: Command): void {
|
|
|
200
200
|
console.log(`\n⚠️ Compile diagnostics (${diagnostics.count}):`);
|
|
201
201
|
for (const line of diagnostics.formatLines().slice(0, 20)) console.log(` ${line}`);
|
|
202
202
|
if (diagnostics.count > 20) console.log(` … and ${diagnostics.count - 20} more`);
|
|
203
|
-
console.log(`
|
|
203
|
+
console.log(` Unsupported steps compile to a runtime throw — the test fails loudly instead of passing having checked nothing.`);
|
|
204
204
|
if (options.strict) {
|
|
205
205
|
console.error(`\n--strict: failing generation on ${diagnostics.count} diagnostic(s).`);
|
|
206
206
|
process.exit(1);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sungen results merge` — fold a subset re-run's Playwright JSON report into
|
|
3
|
+
* the existing per-target result file instead of overwriting it. See
|
|
4
|
+
* exporters/results-merge.ts for the title-keyed semantics; this command owns
|
|
5
|
+
* path resolution, validation, ghost warnings, and the atomic write.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
import { mergeResultsReports, findGhostTitles } from '../../exporters/results-merge';
|
|
12
|
+
import { resolveResultsPath } from '../../exporters/playwright-report-parser';
|
|
13
|
+
import { parseSpecFile } from '../../exporters/spec-parser';
|
|
14
|
+
|
|
15
|
+
interface MergeCliOptions {
|
|
16
|
+
out?: string;
|
|
17
|
+
screen?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readReport(file: string, label: string): any {
|
|
21
|
+
if (!fs.existsSync(file)) {
|
|
22
|
+
throw new Error(`results merge: ${label} file not found: ${file}`);
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
26
|
+
} catch (err) {
|
|
27
|
+
throw new Error(`results merge: ${label} file is not valid JSON (${file}): ${err instanceof Error ? err.message : String(err)}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the generated dir that owns FEATURE `name` — by its spec file, not by
|
|
33
|
+
* unit-dir name: a multi-feature screen keeps `home.spec.ts` AND
|
|
34
|
+
* `home-modal.spec.ts` in specs/generated/home/, and each feature has its OWN
|
|
35
|
+
* per-target result file. Kind order mirrors delivery's target resolution
|
|
36
|
+
* (flows → screens → api → api flows); the same basename found in more than one
|
|
37
|
+
* place is an ambiguity error, never a silent pick.
|
|
38
|
+
*/
|
|
39
|
+
function resolveFeatureDir(cwd: string, name: string): string {
|
|
40
|
+
const roots = [
|
|
41
|
+
path.join(cwd, 'specs', 'generated', 'flows'),
|
|
42
|
+
path.join(cwd, 'specs', 'generated'),
|
|
43
|
+
path.join(cwd, 'specs', 'generated', 'api'),
|
|
44
|
+
path.join(cwd, 'specs', 'generated', 'api', 'flows'),
|
|
45
|
+
];
|
|
46
|
+
const hits: string[] = [];
|
|
47
|
+
for (const root of roots) {
|
|
48
|
+
if (!fs.existsSync(root)) continue;
|
|
49
|
+
for (const unit of fs.readdirSync(root)) {
|
|
50
|
+
const dir = path.join(root, unit);
|
|
51
|
+
// the middle root also contains flows/ and api/ themselves — units only
|
|
52
|
+
if (['flows', 'api'].includes(unit) && root.endsWith(path.join('specs', 'generated'))) continue;
|
|
53
|
+
if (fs.existsSync(path.join(dir, `${name}.spec.ts`)) && !hits.includes(dir)) hits.push(dir);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (hits.length === 0) {
|
|
57
|
+
throw new Error(`results merge: no generated spec named "${name}.spec.ts" found under specs/generated/ — pass the feature basename (a multi-feature screen has one result file PER feature), and run sungen generate first.`);
|
|
58
|
+
}
|
|
59
|
+
if (hits.length > 1) {
|
|
60
|
+
throw new Error(`results merge: "${name}" is ambiguous — found in ${hits.map((h) => path.relative(cwd, h)).join(' and ')}. Use the explicit two-file form.`);
|
|
61
|
+
}
|
|
62
|
+
return hits[0];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Runs the merge. Exported for tests; the commander wiring below is a thin
|
|
67
|
+
* shell around it. Returns the path written.
|
|
68
|
+
*/
|
|
69
|
+
export function runResultsMerge(baseArg: string | undefined, newArg: string | undefined, opts: MergeCliOptions, cwd: string = process.cwd()): string {
|
|
70
|
+
let baseFile = baseArg;
|
|
71
|
+
let newFile = newArg;
|
|
72
|
+
let specTitles: string[] | null = null;
|
|
73
|
+
|
|
74
|
+
if (opts.screen) {
|
|
75
|
+
// Convenience form: `results merge --screen <name> <fresh.json>` — <name> is
|
|
76
|
+
// the FEATURE basename; its per-target file lives next to its spec, with the
|
|
77
|
+
// same in-dir candidate order delivery reads.
|
|
78
|
+
if (baseArg && newArg) throw new Error('results merge: --screen takes ONE report file (the fresh subset run).');
|
|
79
|
+
newFile = baseArg ?? newArg;
|
|
80
|
+
if (!newFile) throw new Error('results merge: --screen needs the fresh subset report file as the argument.');
|
|
81
|
+
const dir = resolveFeatureDir(cwd, opts.screen);
|
|
82
|
+
baseFile = resolveResultsPath(cwd, dir, opts.screen);
|
|
83
|
+
// resolveResultsPath falls back to the cwd-global test-results/results.json —
|
|
84
|
+
// a file shared by EVERY unit. Merging one unit's subset into it would report
|
|
85
|
+
// success while delivery keeps reading the stale per-target file: refuse.
|
|
86
|
+
if (!baseFile || path.dirname(baseFile) !== dir) {
|
|
87
|
+
throw new Error(`results merge: no per-target result file for "${opts.screen}" in ${path.relative(cwd, dir)} — nothing to merge into (a first full run writes it directly).`);
|
|
88
|
+
}
|
|
89
|
+
// Ghost/foreign-title checks need the CURRENT spec titles.
|
|
90
|
+
const specFile = path.join(dir, `${opts.screen}.spec.ts`);
|
|
91
|
+
try { specTitles = parseSpecFile(specFile).tests.map((t) => t.testTitle); } catch { specTitles = null; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!baseFile || !newFile) {
|
|
95
|
+
throw new Error('results merge: expected <base.json> <new.json>, or --screen <name> <new.json>.');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const base = readReport(baseFile, 'base');
|
|
99
|
+
const fresh = readReport(newFile, 'new');
|
|
100
|
+
const { merged, replaced, added, kept } = mergeResultsReports(base, fresh);
|
|
101
|
+
|
|
102
|
+
if (specTitles) {
|
|
103
|
+
// A fresh report whose ADDED titles are unknown to this feature's spec was
|
|
104
|
+
// produced by a DIFFERENT feature — writing it here would silently corrupt
|
|
105
|
+
// this file while the right one stays stale. Refuse before writing.
|
|
106
|
+
const foreign = findGhostTitles(added, specTitles);
|
|
107
|
+
if (foreign.length > 0) {
|
|
108
|
+
throw new Error(`results merge: ${foreign.length} title(s) in ${path.basename(newFile)} match no scenario of "${opts.screen}" (e.g. ${foreign[0]}) — is this another feature's report? Base file untouched.`);
|
|
109
|
+
}
|
|
110
|
+
const ghosts = findGhostTitles(kept, specTitles);
|
|
111
|
+
if (ghosts.length > 0) {
|
|
112
|
+
console.warn(`⚠ ${ghosts.length} result title(s) no longer match any current scenario (renamed by a spec edit?) — kept, not deleted:`);
|
|
113
|
+
for (const g of ghosts.slice(0, 10)) console.warn(` - ${g}`);
|
|
114
|
+
if (ghosts.length > 10) console.warn(` … and ${ghosts.length - 10} more`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Atomic write: a crash mid-write must never leave a truncated result file —
|
|
119
|
+
// that file is the single source of execution truth for delivery/dashboard.
|
|
120
|
+
// (Same-dir rename = same device; concurrent merges are last-rename-wins.)
|
|
121
|
+
const outFile = opts.out ?? baseFile;
|
|
122
|
+
const tmp = `${outFile}.tmp-${process.pid}`;
|
|
123
|
+
try {
|
|
124
|
+
fs.writeFileSync(tmp, JSON.stringify(merged, null, 2));
|
|
125
|
+
fs.renameSync(tmp, outFile);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
try { fs.rmSync(tmp, { force: true }); } catch { /* best-effort cleanup */ }
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log(`✓ merged ${path.basename(newFile)} into ${path.relative(cwd, outFile)} — ${replaced.length} updated, ${added.length} added, ${kept.length} kept`);
|
|
132
|
+
return outFile;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function registerResultsCommand(program: Command): void {
|
|
136
|
+
const results = program
|
|
137
|
+
.command('results')
|
|
138
|
+
.description('Operate on per-target Playwright result files');
|
|
139
|
+
|
|
140
|
+
results
|
|
141
|
+
.command('merge [base] [new]')
|
|
142
|
+
.description('Merge a subset re-run\'s report into an existing result file (same title → new wins; others kept)')
|
|
143
|
+
.option('-o, --out <file>', 'write the merged report here instead of overwriting the base file')
|
|
144
|
+
.option('--screen <name>', 'resolve the base file from this FEATURE basename (a multi-feature screen has one result file per feature); pass only the fresh report as the argument')
|
|
145
|
+
.action((baseArg: string | undefined, newArg: string | undefined, opts: MergeCliOptions) => {
|
|
146
|
+
try {
|
|
147
|
+
runResultsMerge(baseArg, newArg, opts);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
150
|
+
process.exitCode = 1;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { spawnSync } from 'child_process';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { coreVersion } from '../../orchestrator/lockstep-version';
|
|
6
|
+
import { syncProjectSungenDeps } from '../../orchestrator/project-deps-sync';
|
|
3
7
|
|
|
4
8
|
/**
|
|
5
9
|
* `sungen update` does two jobs in sequence:
|
|
@@ -7,8 +11,12 @@ import { spawnSync } from 'child_process';
|
|
|
7
11
|
* 1. Reinstall `@sun-asterisk/sungen@latest` globally via npm so the bundled
|
|
8
12
|
* AI templates are refreshed.
|
|
9
13
|
* 2. Re-execute `sungen update` with the env var `SUNGEN_UPDATE_SKIP_NPM=1`
|
|
10
|
-
* so the
|
|
11
|
-
*
|
|
14
|
+
* so the *new* binary (a) reconciles the project's own sungen packages onto
|
|
15
|
+
* its version line and (b) overwrites the AI rules / commands / skills
|
|
16
|
+
* inside the project from the new templates.
|
|
17
|
+
*
|
|
18
|
+
* Step 2 runs under the NEW binary on purpose: the version the project should be
|
|
19
|
+
* pinned to is the one doing the pinning, so the two can never disagree.
|
|
12
20
|
*
|
|
13
21
|
* We use an environment variable (not a CLI flag) for the internal hand-off
|
|
14
22
|
* because Commander throws "unknown option" if the currently-installed
|
|
@@ -23,6 +31,12 @@ import { spawnSync } from 'child_process';
|
|
|
23
31
|
|
|
24
32
|
const SKIP_NPM_ENV = 'SUNGEN_UPDATE_SKIP_NPM';
|
|
25
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Carried to the re-exec as an env var for the same reason as SKIP_NPM_ENV: a flag would crash an
|
|
36
|
+
* older published binary with "unknown option", an env var is ignored by it.
|
|
37
|
+
*/
|
|
38
|
+
const SKIP_PROJECT_ENV = 'SUNGEN_UPDATE_SKIP_PROJECT';
|
|
39
|
+
|
|
26
40
|
export function registerUpdateCommand(program: Command): void {
|
|
27
41
|
program
|
|
28
42
|
.command('update')
|
|
@@ -48,13 +62,20 @@ export function registerUpdateCommand(program: Command): void {
|
|
|
48
62
|
)
|
|
49
63
|
.option(
|
|
50
64
|
'--skip-npm-install',
|
|
51
|
-
'Skip the npm install step (refresh project AI assets only)',
|
|
65
|
+
'Skip the global npm install step (refresh project AI assets only)',
|
|
66
|
+
false,
|
|
67
|
+
)
|
|
68
|
+
.option(
|
|
69
|
+
'--skip-project',
|
|
70
|
+
'Skip reinstalling the project-local sungen packages (AI assets only — leaves node_modules stale)',
|
|
52
71
|
false,
|
|
53
72
|
)
|
|
54
|
-
.action(async (options: { dryRun?: boolean; beta?: boolean; tag?: string; skipNpmInstall?: boolean; assistant?: string[]; force?: boolean }) => {
|
|
73
|
+
.action(async (options: { dryRun?: boolean; beta?: boolean; tag?: string; skipNpmInstall?: boolean; skipProject?: boolean; assistant?: string[]; force?: boolean }) => {
|
|
55
74
|
try {
|
|
56
75
|
const skipNpm =
|
|
57
76
|
Boolean(options.skipNpmInstall) || process.env[SKIP_NPM_ENV] === '1';
|
|
77
|
+
const skipProject =
|
|
78
|
+
Boolean(options.skipProject) || process.env[SKIP_PROJECT_ENV] === '1';
|
|
58
79
|
|
|
59
80
|
// Channel resolution via npm dist-tags. Default `latest` = official/stable.
|
|
60
81
|
// `--beta` → beta channel; running plain `sungen update` later switches back to stable.
|
|
@@ -63,10 +84,18 @@ export function registerUpdateCommand(program: Command): void {
|
|
|
63
84
|
if (!skipNpm) {
|
|
64
85
|
reinstallSungen(channel);
|
|
65
86
|
printCurrentVersion(channel);
|
|
66
|
-
reExecUpdateForAIAssets(options.dryRun ?? false, options.assistant ?? [], options.force ?? false);
|
|
87
|
+
reExecUpdateForAIAssets(options.dryRun ?? false, options.assistant ?? [], options.force ?? false, skipProject);
|
|
67
88
|
return;
|
|
68
89
|
}
|
|
69
90
|
|
|
91
|
+
// Running under the NEW binary now: pin the project's own copies to THIS version before
|
|
92
|
+
// refreshing the AI assets, so the compiler the project runs matches the templates it gets.
|
|
93
|
+
if (skipProject) {
|
|
94
|
+
console.log('⏭ Skipping project-local package sync (--skip-project).\n');
|
|
95
|
+
} else {
|
|
96
|
+
syncProjectSungenDeps(process.cwd(), coreVersion(), options.dryRun ?? false);
|
|
97
|
+
}
|
|
98
|
+
|
|
70
99
|
const { resolveAssistants } = require('../../orchestrator/ai-skills/resolve-assistants');
|
|
71
100
|
const { AIRulesUpdater } = require('../../orchestrator/ai-rules-updater');
|
|
72
101
|
const updater = new AIRulesUpdater(process.cwd());
|
|
@@ -94,8 +123,15 @@ function reinstallSungen(channel: string): void {
|
|
|
94
123
|
}
|
|
95
124
|
|
|
96
125
|
function printCurrentVersion(channel: string): void {
|
|
97
|
-
console.log('\n🔎 Installed version:');
|
|
98
|
-
|
|
126
|
+
console.log('\n🔎 Installed version (global):');
|
|
127
|
+
// Same PATH caveat as the re-exec: report the version that was just installed globally, not a
|
|
128
|
+
// project-local copy that happens to shadow it.
|
|
129
|
+
const entry = globalCoreEntry();
|
|
130
|
+
if (entry) {
|
|
131
|
+
spawnSync(process.execPath, [entry, '--version'], { stdio: 'inherit' });
|
|
132
|
+
} else {
|
|
133
|
+
spawnSync('sungen', ['--version'], { stdio: 'inherit', shell: true });
|
|
134
|
+
}
|
|
99
135
|
if (channel === 'latest') {
|
|
100
136
|
console.log(' Channel: stable. To try prereleases: sungen update --beta');
|
|
101
137
|
} else {
|
|
@@ -104,17 +140,40 @@ function printCurrentVersion(channel: string): void {
|
|
|
104
140
|
console.log('');
|
|
105
141
|
}
|
|
106
142
|
|
|
107
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Absolute path to the globally installed core's CLI entry, or null when it can't be located
|
|
145
|
+
* (then the caller falls back to a PATH lookup). Asks npm for the global root instead of guessing
|
|
146
|
+
* a prefix, so nvm / Volta / Windows / custom-prefix installs all resolve correctly.
|
|
147
|
+
*/
|
|
148
|
+
function globalCoreEntry(): string | null {
|
|
149
|
+
try {
|
|
150
|
+
const out = spawnSync('npm', ['root', '-g'], { encoding: 'utf-8', shell: true });
|
|
151
|
+
const root = out.status === 0 ? (out.stdout || '').trim() : '';
|
|
152
|
+
if (!root) return null;
|
|
153
|
+
const entry = path.join(root, '@sun-asterisk', 'sungen', 'bin', 'sungen.js');
|
|
154
|
+
return fs.existsSync(entry) ? entry : null;
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function reExecUpdateForAIAssets(dryRun: boolean, assistants: string[], force: boolean, skipProject: boolean): void {
|
|
108
161
|
const args = ['update'];
|
|
109
162
|
if (dryRun) args.push('--dry-run');
|
|
110
163
|
if (force) args.push('--force');
|
|
111
164
|
for (const a of assistants) args.push('--assistant', a);
|
|
112
165
|
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
166
|
+
const env: NodeJS.ProcessEnv = { ...process.env, [SKIP_NPM_ENV]: '1' };
|
|
167
|
+
if (skipProject) env[SKIP_PROJECT_ENV] = '1';
|
|
168
|
+
|
|
169
|
+
// Run the JUST-INSTALLED global entry directly rather than whatever `sungen` resolves to on PATH.
|
|
170
|
+
// Inside a project, `npx sungen update` / `npm run` put the project's own bin dir FIRST on PATH, so
|
|
171
|
+
// a PATH lookup here re-executes the OLD local copy — step 2 would then refresh nothing, which is
|
|
172
|
+
// exactly the "update ran but the project didn't change" failure this flow exists to prevent.
|
|
173
|
+
const entry = globalCoreEntry();
|
|
174
|
+
const result = entry
|
|
175
|
+
? spawnSync(process.execPath, [entry, ...args], { stdio: 'inherit', env })
|
|
176
|
+
: spawnSync('sungen', args, { stdio: 'inherit', shell: true, env });
|
|
118
177
|
|
|
119
178
|
if (result.status !== 0) {
|
|
120
179
|
console.error(
|
package/src/cli/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { registerGateCommand } from './commands/gate';
|
|
|
22
22
|
import { registerIngestCommand } from './commands/ingest';
|
|
23
23
|
import { registerEvalCommand } from './commands/eval';
|
|
24
24
|
import { registerManifestCommand } from './commands/manifest';
|
|
25
|
+
import { registerChangedCommand } from './commands/changed';
|
|
25
26
|
import { registerLedgerCommand } from './commands/ledger';
|
|
26
27
|
import { registerFeedbackCommand } from './commands/feedback';
|
|
27
28
|
import { registerQaFeedbackCommand } from './commands/qa-feedback';
|
|
@@ -34,6 +35,7 @@ import { registerCapabilityCommand } from './commands/capability';
|
|
|
34
35
|
import { registerFlowCheckCommand } from './commands/flow-check';
|
|
35
36
|
import { registerContextCommand } from './commands/context';
|
|
36
37
|
import { registerRepairCommand } from './commands/repair';
|
|
38
|
+
import { registerResultsCommand } from './commands/results-merge';
|
|
37
39
|
import { registerLoginCommand } from './commands/login';
|
|
38
40
|
import { capabilityRegistry } from '../capabilities/registry';
|
|
39
41
|
import { discoverAndRegisterCapabilities } from '../capabilities/discover';
|
|
@@ -70,6 +72,7 @@ async function main() {
|
|
|
70
72
|
registerJourneyCommand(program);
|
|
71
73
|
registerGateCommand(program);
|
|
72
74
|
registerManifestCommand(program);
|
|
75
|
+
registerChangedCommand(program);
|
|
73
76
|
registerLedgerCommand(program);
|
|
74
77
|
registerFeedbackCommand(program);
|
|
75
78
|
registerQaFeedbackCommand(program);
|
|
@@ -82,6 +85,7 @@ async function main() {
|
|
|
82
85
|
registerFlowCheckCommand(program);
|
|
83
86
|
registerContextCommand(program);
|
|
84
87
|
registerRepairCommand(program);
|
|
88
|
+
registerResultsCommand(program);
|
|
85
89
|
registerIngestCommand(program);
|
|
86
90
|
registerEvalCommand(program);
|
|
87
91
|
registerLoginCommand(program);
|
|
@@ -22,9 +22,17 @@ export function loadApiCatalog(
|
|
|
22
22
|
kind: 'screen' | 'flow' | 'api',
|
|
23
23
|
screen: string,
|
|
24
24
|
): Record<string, ApiCatalogEntry> {
|
|
25
|
-
|
|
25
|
+
// Every kind can carry `@api:` preconditions, and the runtime resolves their
|
|
26
|
+
// endpoints from the UNIT's own catalog first (qa/screens|flows/<u>/api/apis.yaml)
|
|
27
|
+
// then the shared one — this loader mirrors that scheme so the deliverable can
|
|
28
|
+
// name the endpoint a screen/flow scenario actually calls.
|
|
29
|
+
const unitDir = kind === 'api'
|
|
30
|
+
? path.join(cwd, 'qa', 'api', screen)
|
|
31
|
+
: kind === 'flow'
|
|
32
|
+
? path.join(cwd, 'qa', 'flows', screen)
|
|
33
|
+
: path.join(cwd, 'qa', 'screens', screen);
|
|
26
34
|
const shared = readApiCatalogFile(path.join(cwd, 'qa', 'api', 'apis.yaml'));
|
|
27
|
-
const unit = readApiCatalogFile(path.join(
|
|
35
|
+
const unit = readApiCatalogFile(path.join(unitDir, 'api', 'apis.yaml'));
|
|
28
36
|
return { ...shared, ...unit };
|
|
29
37
|
}
|
|
30
38
|
|