@sun-asterisk/sungen 3.2.2-beta.9 → 3.2.3-beta.3
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/discover.d.ts.map +1 -1
- package/dist/capabilities/discover.js +26 -3
- package/dist/capabilities/discover.js.map +1 -1
- package/dist/cli/commands/add-flow.d.ts.map +1 -1
- package/dist/cli/commands/add-flow.js +2 -0
- package/dist/cli/commands/add-flow.js.map +1 -1
- package/dist/cli/commands/add.d.ts.map +1 -1
- package/dist/cli/commands/add.js +4 -0
- package/dist/cli/commands/add.js.map +1 -1
- package/dist/cli/commands/capability.d.ts.map +1 -1
- package/dist/cli/commands/capability.js +61 -0
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +89 -2
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/eval.js +3 -3
- package/dist/cli/commands/eval.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +2 -1
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +32 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/journey.d.ts.map +1 -1
- package/dist/cli/commands/journey.js +24 -2
- package/dist/cli/commands/journey.js.map +1 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +10 -3
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
- package/dist/dashboard/snapshot-builder.js +10 -1
- package/dist/dashboard/snapshot-builder.js.map +1 -1
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +3 -1
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/json-exporter.js +3 -1
- package/dist/exporters/json-exporter.js.map +1 -1
- package/dist/exporters/mobile-app-id.d.ts +22 -0
- package/dist/exporters/mobile-app-id.d.ts.map +1 -0
- package/dist/exporters/mobile-app-id.js +37 -0
- package/dist/exporters/mobile-app-id.js.map +1 -0
- package/dist/exporters/spec-parser.d.ts.map +1 -1
- package/dist/exporters/spec-parser.js +6 -2
- package/dist/exporters/spec-parser.js.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +3 -0
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +7 -4
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +20 -3
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +4 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/not-checked-assertion.hbs +4 -1
- package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +3 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +48 -9
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +3 -0
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +11 -1
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts +11 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +33 -0
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +8 -1
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/catalog/universal-viewpoints.yaml +15 -0
- package/dist/harness/eval/skill-lint.d.ts +3 -3
- package/dist/harness/eval/skill-lint.d.ts.map +1 -1
- package/dist/harness/eval/skill-lint.js +88 -68
- package/dist/harness/eval/skill-lint.js.map +1 -1
- package/dist/harness/journey.d.ts +37 -1
- package/dist/harness/journey.d.ts.map +1 -1
- package/dist/harness/journey.js +146 -19
- package/dist/harness/journey.js.map +1 -1
- package/dist/harness/quality-gates.d.ts +13 -0
- package/dist/harness/quality-gates.d.ts.map +1 -1
- package/dist/harness/quality-gates.js +23 -0
- package/dist/harness/quality-gates.js.map +1 -1
- package/dist/harness/sensors.d.ts +1 -1
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +32 -9
- package/dist/harness/sensors.js.map +1 -1
- package/dist/orchestrator/ai-rules-updater.d.ts +11 -3
- package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
- package/dist/orchestrator/ai-rules-updater.js +44 -180
- package/dist/orchestrator/ai-rules-updater.js.map +1 -1
- package/dist/orchestrator/ai-skills/adapters.d.ts +28 -0
- package/dist/orchestrator/ai-skills/adapters.d.ts.map +1 -0
- package/dist/orchestrator/ai-skills/adapters.js +34 -0
- package/dist/orchestrator/ai-skills/adapters.js.map +1 -0
- package/dist/orchestrator/ai-skills/build.d.ts +10 -0
- package/dist/orchestrator/ai-skills/build.d.ts.map +1 -0
- package/dist/orchestrator/ai-skills/build.js +128 -0
- package/dist/orchestrator/ai-skills/build.js.map +1 -0
- package/dist/orchestrator/ai-skills/cap-stripper.d.ts +27 -0
- package/dist/orchestrator/ai-skills/cap-stripper.d.ts.map +1 -0
- package/dist/orchestrator/ai-skills/cap-stripper.js +78 -0
- package/dist/orchestrator/ai-skills/cap-stripper.js.map +1 -0
- package/dist/orchestrator/ai-skills/commands.d.ts +22 -0
- package/dist/orchestrator/ai-skills/commands.d.ts.map +1 -0
- package/dist/orchestrator/ai-skills/commands.js +109 -0
- package/dist/orchestrator/ai-skills/commands.js.map +1 -0
- package/dist/orchestrator/ai-skills/generator.d.ts +23 -0
- package/dist/orchestrator/ai-skills/generator.d.ts.map +1 -0
- package/dist/orchestrator/ai-skills/generator.js +79 -0
- package/dist/orchestrator/ai-skills/generator.js.map +1 -0
- package/dist/orchestrator/ai-skills/golden-skills.test.d.ts +2 -0
- package/dist/orchestrator/ai-skills/golden-skills.test.d.ts.map +1 -0
- package/dist/orchestrator/ai-skills/golden-skills.test.js +183 -0
- package/dist/orchestrator/ai-skills/golden-skills.test.js.map +1 -0
- package/dist/orchestrator/ai-skills/resolve-assistants.d.ts +8 -0
- package/dist/orchestrator/ai-skills/resolve-assistants.d.ts.map +1 -0
- package/dist/orchestrator/ai-skills/resolve-assistants.js +28 -0
- package/dist/orchestrator/ai-skills/resolve-assistants.js.map +1 -0
- package/dist/orchestrator/ai-skills/writer.d.ts +15 -0
- package/dist/orchestrator/ai-skills/writer.d.ts.map +1 -0
- package/dist/orchestrator/ai-skills/writer.js +129 -0
- package/dist/orchestrator/ai-skills/writer.js.map +1 -0
- package/dist/orchestrator/flow-manager.d.ts +11 -0
- package/dist/orchestrator/flow-manager.d.ts.map +1 -1
- package/dist/orchestrator/flow-manager.js +147 -29
- package/dist/orchestrator/flow-manager.js.map +1 -1
- package/dist/orchestrator/mobile-runtime-scaffolder.d.ts +59 -0
- package/dist/orchestrator/mobile-runtime-scaffolder.d.ts.map +1 -0
- package/dist/orchestrator/mobile-runtime-scaffolder.js +298 -0
- package/dist/orchestrator/mobile-runtime-scaffolder.js.map +1 -0
- package/dist/orchestrator/project-initializer.d.ts +44 -7
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +147 -79
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/screen-manager.d.ts +30 -0
- package/dist/orchestrator/screen-manager.d.ts.map +1 -1
- package/dist/orchestrator/screen-manager.js +205 -22
- package/dist/orchestrator/screen-manager.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/README.md +58 -0
- package/dist/orchestrator/templates/{ai-instructions/claude-cmd-add-flow.md → ai-src/commands/add-flow.md} +63 -2
- package/dist/orchestrator/templates/{ai-instructions/claude-cmd-add-screen.md → ai-src/commands/add-screen.md} +171 -6
- package/dist/orchestrator/templates/{ai-instructions/claude-cmd-create-test.md → ai-src/commands/create-test.md} +118 -5
- package/{src/orchestrator/templates/ai-instructions/claude-cmd-dashboard.md → dist/orchestrator/templates/ai-src/commands/dashboard.md} +25 -3
- package/{src/orchestrator/templates/ai-instructions/claude-cmd-delivery.md → dist/orchestrator/templates/ai-src/commands/delivery.md} +67 -3
- package/dist/orchestrator/templates/ai-src/commands/design.md +29 -0
- package/{src/orchestrator/templates/ai-instructions/claude-cmd-feedback.md → dist/orchestrator/templates/ai-src/commands/feedback.md} +34 -3
- package/dist/orchestrator/templates/{ai-instructions/claude-cmd-locale.md → ai-src/commands/locale.md} +56 -4
- package/{src/orchestrator/templates/ai-instructions/claude-cmd-review.md → dist/orchestrator/templates/ai-src/commands/review.md} +57 -2
- package/dist/orchestrator/templates/{ai-instructions/claude-cmd-run-test.md → ai-src/commands/run-test.md} +116 -8
- package/dist/orchestrator/templates/ai-src/config/codex.md +8 -0
- package/{src/orchestrator/templates/ai-instructions/claude-skill-capture-mobile.md → dist/orchestrator/templates/ai-src/skills/sungen-capture-mobile/SKILL.md} +11 -0
- package/dist/orchestrator/templates/{ai-instructions/github-skill-sungen-error-mapping.md → ai-src/skills/sungen-error-mapping/SKILL.md} +31 -10
- package/{src/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md → dist/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md} +5 -5
- package/{src/orchestrator/templates/ai-instructions/claude-skill-locale.md → dist/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md} +275 -3
- package/{src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md → dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md} +17 -0
- package/{src/orchestrator/templates/ai-instructions/claude-skill-selector-fix-mobile.md → dist/orchestrator/templates/ai-src/skills/sungen-selector-fix-mobile/SKILL.md} +5 -0
- package/dist/orchestrator/templates/{ai-instructions/claude-skill-tc-generation.md → ai-src/skills/sungen-tc-generation/SKILL.md} +5 -0
- package/dist/orchestrator/templates/playwright.config.ts +7 -4
- package/package.json +3 -3
- package/src/capabilities/discover.ts +29 -3
- package/src/cli/commands/add-flow.ts +2 -0
- package/src/cli/commands/add.ts +4 -0
- package/src/cli/commands/capability.ts +64 -1
- package/src/cli/commands/delivery.ts +89 -2
- package/src/cli/commands/eval.ts +3 -3
- package/src/cli/commands/generate.ts +2 -1
- package/src/cli/commands/init.ts +50 -3
- package/src/cli/commands/journey.ts +20 -3
- package/src/cli/commands/update.ts +14 -4
- package/src/dashboard/snapshot-builder.ts +10 -1
- package/src/exporters/csv-exporter.ts +3 -1
- package/src/exporters/json-exporter.ts +3 -1
- package/src/exporters/mobile-app-id.ts +34 -0
- package/src/exporters/spec-parser.ts +6 -2
- package/src/generators/test-generator/adapters/adapter-interface.ts +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +7 -4
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +20 -3
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +4 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/not-checked-assertion.hbs +4 -1
- package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +3 -0
- package/src/generators/test-generator/code-generator.ts +49 -9
- package/src/generators/test-generator/template-engine.ts +14 -1
- package/src/generators/test-generator/utils/selector-resolver.ts +51 -1
- package/src/harness/audit.ts +9 -2
- package/src/harness/catalog/universal-viewpoints.yaml +15 -0
- package/src/harness/eval/skill-lint.ts +71 -60
- package/src/harness/journey.ts +167 -19
- package/src/harness/quality-gates.ts +20 -0
- package/src/harness/sensors.ts +25 -8
- package/src/orchestrator/ai-rules-updater.ts +35 -197
- package/src/orchestrator/ai-skills/adapters.ts +52 -0
- package/src/orchestrator/ai-skills/build.ts +103 -0
- package/src/orchestrator/ai-skills/cap-stripper.ts +85 -0
- package/src/orchestrator/ai-skills/commands.ts +112 -0
- package/src/orchestrator/ai-skills/generator.ts +47 -0
- package/src/orchestrator/ai-skills/golden-skills.test.ts +124 -0
- package/src/orchestrator/ai-skills/resolve-assistants.ts +18 -0
- package/src/orchestrator/ai-skills/writer.ts +92 -0
- package/src/orchestrator/flow-manager.ts +165 -29
- package/src/orchestrator/mobile-runtime-scaffolder.ts +277 -0
- package/src/orchestrator/project-initializer.ts +159 -86
- package/src/orchestrator/screen-manager.ts +237 -22
- package/src/orchestrator/templates/ai-src/README.md +58 -0
- package/src/orchestrator/templates/{ai-instructions/claude-cmd-add-flow.md → ai-src/commands/add-flow.md} +63 -2
- package/src/orchestrator/templates/{ai-instructions/claude-cmd-add-screen.md → ai-src/commands/add-screen.md} +171 -6
- package/src/orchestrator/templates/{ai-instructions/claude-cmd-create-test.md → ai-src/commands/create-test.md} +118 -5
- package/{dist/orchestrator/templates/ai-instructions/claude-cmd-dashboard.md → src/orchestrator/templates/ai-src/commands/dashboard.md} +25 -3
- package/{dist/orchestrator/templates/ai-instructions/claude-cmd-delivery.md → src/orchestrator/templates/ai-src/commands/delivery.md} +67 -3
- package/src/orchestrator/templates/ai-src/commands/design.md +29 -0
- package/{dist/orchestrator/templates/ai-instructions/claude-cmd-feedback.md → src/orchestrator/templates/ai-src/commands/feedback.md} +34 -3
- package/src/orchestrator/templates/{ai-instructions/claude-cmd-locale.md → ai-src/commands/locale.md} +56 -4
- package/{dist/orchestrator/templates/ai-instructions/claude-cmd-review.md → src/orchestrator/templates/ai-src/commands/review.md} +57 -2
- package/src/orchestrator/templates/{ai-instructions/claude-cmd-run-test.md → ai-src/commands/run-test.md} +116 -8
- package/src/orchestrator/templates/ai-src/config/codex.md +8 -0
- package/{dist/orchestrator/templates/ai-instructions/claude-skill-capture-mobile.md → src/orchestrator/templates/ai-src/skills/sungen-capture-mobile/SKILL.md} +11 -0
- package/{dist/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md → src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md} +31 -10
- package/{dist/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md → src/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md} +5 -5
- package/{dist/orchestrator/templates/ai-instructions/claude-skill-locale.md → src/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md} +275 -3
- package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md → src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md} +17 -0
- package/{dist/orchestrator/templates/ai-instructions/claude-skill-selector-fix-mobile.md → src/orchestrator/templates/ai-src/skills/sungen-selector-fix-mobile/SKILL.md} +5 -0
- package/src/orchestrator/templates/{ai-instructions/claude-skill-tc-generation.md → ai-src/skills/sungen-tc-generation/SKILL.md} +5 -0
- package/src/orchestrator/templates/playwright.config.ts +7 -4
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-design.md +0 -12
- package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +0 -347
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +0 -86
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +0 -131
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +0 -94
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-dashboard.md +0 -65
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +0 -83
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-design.md +0 -13
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-feedback.md +0 -24
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-locale.md +0 -70
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-review.md +0 -38
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +0 -158
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mobile.md +0 -184
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +0 -85
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-locale.md +0 -291
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix-mobile.md +0 -316
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +0 -221
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +0 -617
- package/src/orchestrator/templates/ai-instructions/claude-cmd-design.md +0 -12
- package/src/orchestrator/templates/ai-instructions/claude-skill-api-design.md +0 -62
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture-mode-figma-mcp.md +0 -82
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture-mode-figma-pat.md +0 -117
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture-mode-live.md +0 -60
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture-mode-local.md +0 -38
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture.md +0 -35
- package/src/orchestrator/templates/ai-instructions/claude-skill-dashboard.md +0 -121
- package/src/orchestrator/templates/ai-instructions/claude-skill-delivery.md +0 -141
- package/src/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +0 -176
- package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +0 -446
- package/src/orchestrator/templates/ai-instructions/claude-skill-ingest-legacy.md +0 -79
- package/src/orchestrator/templates/ai-instructions/claude-skill-mobile-gestures.md +0 -109
- package/src/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +0 -347
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +0 -320
- package/src/orchestrator/templates/ai-instructions/claude-skill-test-design-techniques.md +0 -118
- package/src/orchestrator/templates/ai-instructions/claude-skill-viewpoint-group-a-data-entry.md +0 -203
- package/src/orchestrator/templates/ai-instructions/claude-skill-viewpoint-group-b-data-ops.md +0 -179
- package/src/orchestrator/templates/ai-instructions/claude-skill-viewpoint-group-c-data-explore.md +0 -233
- package/src/orchestrator/templates/ai-instructions/claude-skill-viewpoint-group-d-display.md +0 -226
- package/src/orchestrator/templates/ai-instructions/claude-skill-viewpoint-group-e-identity.md +0 -177
- package/src/orchestrator/templates/ai-instructions/claude-skill-viewpoint.md +0 -118
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +0 -86
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +0 -131
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +0 -94
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-dashboard.md +0 -65
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +0 -83
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-design.md +0 -13
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-feedback.md +0 -24
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-locale.md +0 -70
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-review.md +0 -38
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +0 -158
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-api-design.md +0 -62
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mobile.md +0 -184
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-figma-mcp.md +0 -82
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-figma-pat.md +0 -117
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-live.md +0 -60
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-local.md +0 -38
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture.md +0 -35
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-dashboard.md +0 -121
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +0 -141
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-error-mapping.md +0 -176
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +0 -446
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +0 -85
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-ingest-legacy.md +0 -79
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-locale.md +0 -291
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-mobile-gestures.md +0 -109
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix-mobile.md +0 -316
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +0 -221
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +0 -617
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +0 -320
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-test-design-techniques.md +0 -118
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint-group-a-data-entry.md +0 -203
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint-group-b-data-ops.md +0 -179
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint-group-c-data-explore.md +0 -233
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint-group-d-display.md +0 -226
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint-group-e-identity.md +0 -177
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint.md +0 -118
- /package/dist/orchestrator/templates/{ai-instructions/claude-agent-challenge.md → ai-src/agents/challenge.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-agent-discovery.md → ai-src/agents/discovery.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-agent-generator.md → ai-src/agents/generator.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-agent-reviewer.md → ai-src/agents/reviewer.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-config.md → ai-src/config/claude.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/copilot-config.md → ai-src/config/copilot.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-skill-api-design.md → ai-src/skills/sungen-api-design/SKILL.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-skill-capture.md → ai-src/skills/sungen-capture/SKILL.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-skill-capture-mode-figma-mcp.md → ai-src/skills/sungen-capture/mode-figma-mcp.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-skill-capture-mode-figma-pat.md → ai-src/skills/sungen-capture/mode-figma-pat.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-skill-capture-mode-live.md → ai-src/skills/sungen-capture/mode-live.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-skill-capture-mode-local.md → ai-src/skills/sungen-capture/mode-local.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-skill-dashboard.md → ai-src/skills/sungen-dashboard/SKILL.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-skill-delivery.md → ai-src/skills/sungen-delivery/SKILL.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-skill-gherkin-syntax.md → ai-src/skills/sungen-gherkin-syntax/SKILL.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-skill-ingest-legacy.md → ai-src/skills/sungen-ingest-legacy/SKILL.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-skill-mobile-gestures.md → ai-src/skills/sungen-mobile-gestures/SKILL.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-skill-selector-keys.md → ai-src/skills/sungen-selector-keys/SKILL.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-skill-tc-review.md → ai-src/skills/sungen-tc-review/SKILL.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-skill-test-design-techniques.md → ai-src/skills/sungen-test-design-techniques/SKILL.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-skill-viewpoint.md → ai-src/skills/sungen-viewpoint/SKILL.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-skill-viewpoint-group-a-data-entry.md → ai-src/skills/sungen-viewpoint/group-a-data-entry.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-skill-viewpoint-group-b-data-ops.md → ai-src/skills/sungen-viewpoint/group-b-data-ops.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-skill-viewpoint-group-c-data-explore.md → ai-src/skills/sungen-viewpoint/group-c-data-explore.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-skill-viewpoint-group-d-display.md → ai-src/skills/sungen-viewpoint/group-d-display.md} +0 -0
- /package/dist/orchestrator/templates/{ai-instructions/claude-skill-viewpoint-group-e-identity.md → ai-src/skills/sungen-viewpoint/group-e-identity.md} +0 -0
- /package/src/orchestrator/templates/{ai-instructions/claude-agent-challenge.md → ai-src/agents/challenge.md} +0 -0
- /package/src/orchestrator/templates/{ai-instructions/claude-agent-discovery.md → ai-src/agents/discovery.md} +0 -0
- /package/src/orchestrator/templates/{ai-instructions/claude-agent-generator.md → ai-src/agents/generator.md} +0 -0
- /package/src/orchestrator/templates/{ai-instructions/claude-agent-reviewer.md → ai-src/agents/reviewer.md} +0 -0
- /package/src/orchestrator/templates/{ai-instructions/claude-config.md → ai-src/config/claude.md} +0 -0
- /package/src/orchestrator/templates/{ai-instructions/copilot-config.md → ai-src/config/copilot.md} +0 -0
- /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-api-design.md → src/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md} +0 -0
- /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture.md → src/orchestrator/templates/ai-src/skills/sungen-capture/SKILL.md} +0 -0
- /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-figma-mcp.md → src/orchestrator/templates/ai-src/skills/sungen-capture/mode-figma-mcp.md} +0 -0
- /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-figma-pat.md → src/orchestrator/templates/ai-src/skills/sungen-capture/mode-figma-pat.md} +0 -0
- /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-live.md → src/orchestrator/templates/ai-src/skills/sungen-capture/mode-live.md} +0 -0
- /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-local.md → src/orchestrator/templates/ai-src/skills/sungen-capture/mode-local.md} +0 -0
- /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-dashboard.md → src/orchestrator/templates/ai-src/skills/sungen-dashboard/SKILL.md} +0 -0
- /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md → src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md} +0 -0
- /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md → src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md} +0 -0
- /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-ingest-legacy.md → src/orchestrator/templates/ai-src/skills/sungen-ingest-legacy/SKILL.md} +0 -0
- /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-mobile-gestures.md → src/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md} +0 -0
- /package/src/orchestrator/templates/{ai-instructions/claude-skill-selector-keys.md → ai-src/skills/sungen-selector-keys/SKILL.md} +0 -0
- /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md → src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md} +0 -0
- /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-test-design-techniques.md → src/orchestrator/templates/ai-src/skills/sungen-test-design-techniques/SKILL.md} +0 -0
- /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint.md → src/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md} +0 -0
- /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint-group-a-data-entry.md → src/orchestrator/templates/ai-src/skills/sungen-viewpoint/group-a-data-entry.md} +0 -0
- /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint-group-b-data-ops.md → src/orchestrator/templates/ai-src/skills/sungen-viewpoint/group-b-data-ops.md} +0 -0
- /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint-group-c-data-explore.md → src/orchestrator/templates/ai-src/skills/sungen-viewpoint/group-c-data-explore.md} +0 -0
- /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint-group-d-display.md → src/orchestrator/templates/ai-src/skills/sungen-viewpoint/group-d-display.md} +0 -0
- /package/{dist/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint-group-e-identity.md → src/orchestrator/templates/ai-src/skills/sungen-viewpoint/group-e-identity.md} +0 -0
|
@@ -23,11 +23,14 @@ export interface TestFileData {
|
|
|
23
23
|
featureFileName?: string; // Feature file name for TestDataLoader.load()
|
|
24
24
|
isParallel?: boolean; // @parallel tag: fresh page per test (opt-out from serial default)
|
|
25
25
|
flowMode?: boolean; // flow: cross-screen → longer per-test timeout (bug #243)
|
|
26
|
+
platform?: string; // mobile @platform:* value (mobile|android|ios) → `// sungen:platform=` marker for wdio per-OS routing
|
|
26
27
|
cleanup?: { overlay?: boolean; forms?: boolean; scroll?: boolean; storage?: boolean };
|
|
27
28
|
backgroundSteps?: Array<{ comment?: string; code: string }>; // Raw background steps for serial beforeAll
|
|
28
29
|
scenarios: string[];
|
|
29
30
|
authGroups?: AuthGroup[]; // Grouped by auth role for nested describes
|
|
30
31
|
singleAuthRole?: string; // Auth role when all scenarios share the same role
|
|
32
|
+
appPackage?: string; // Mobile (appium): Android package from the feature `Path:` → drives the per-scenario relaunch reset (beforeEach terminate+activate)
|
|
33
|
+
iosBundleId?: string; // Mobile (appium): iOS bundle id from a dual-id `Path: <pkg>/<activity> | <bundleId>`
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
export interface ScenarioData {
|
package/src/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
if (driver.isIOS) {
|
|
2
|
-
// iOS (XCUITest) has no UiScrollable. Swipe
|
|
3
|
-
//
|
|
4
|
-
// it
|
|
2
|
+
// iOS (XCUITest) has no UiScrollable. Swipe up in bounded steps until the target is on screen (a
|
|
3
|
+
// below-fold element isn't in the XCUITest tree until scrolled). A settle `pause` between swipes
|
|
4
|
+
// paces the loop so it doesn't flood `execute/sync` (rapid back-to-back swipes are a common source
|
|
5
|
+
// of flakiness; issue #394). Bounded (cap 10) so a missing target can't loop forever. NOTE: this
|
|
6
|
+
// does NOT fix an app that crashes mid-scroll on its own — that's an app-stability issue, not codegen.
|
|
5
7
|
const __t = $({{> appium-selector-expr}});
|
|
6
|
-
for (let __i = 0; __i <
|
|
8
|
+
for (let __i = 0; __i < 10 && !(await __t.isDisplayed().catch(() => false)); __i++) {
|
|
7
9
|
await driver.execute('mobile: swipe', { direction: 'up' });
|
|
10
|
+
await driver.pause(400); // settle: let the app render between swipes (don't flood execute/sync)
|
|
8
11
|
}
|
|
9
12
|
} else {
|
|
10
13
|
{{#if (eq strategy 'accessibility-id')}}
|
package/src/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
-
{{!-- A switch/toggle flips state on tap (
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
{{!-- A switch/toggle flips state on tap. On Flutter (and many native layouts) the row's a11y node
|
|
2
|
+
spans the FULL width and merges the label with the switch, so a centre .click() lands on the
|
|
3
|
+
LABEL and does NOT flip the control — only the trailing visual switch toggles. So tap the
|
|
4
|
+
RIGHT portion (vertical centre) via getElementRect (W3C-standard → Android + iOS). Falls back to
|
|
5
|
+
a plain centre click when the rect is unavailable. Use `check`/`uncheck` to drive a specific
|
|
6
|
+
on/off state idempotently instead. --}}
|
|
7
|
+
await (async () => {
|
|
8
|
+
const e = await {{> locator}};
|
|
9
|
+
const r = await driver.getElementRect(e.elementId);
|
|
10
|
+
if (r && r.width) {
|
|
11
|
+
// Trailing switch: ~right edge, vertical centre. The inset clears the label/track padding while
|
|
12
|
+
// staying on the switch even on a full-width composite row.
|
|
13
|
+
const x = Math.round(r.x + r.width - Math.max(48, r.width * 0.08));
|
|
14
|
+
const y = Math.round(r.y + r.height / 2);
|
|
15
|
+
if (driver.isIOS) { await driver.execute('mobile: tap', { x, y }); }
|
|
16
|
+
else { await driver.execute('mobile: clickGesture', { x, y }); }
|
|
17
|
+
} else {
|
|
18
|
+
await e.click();
|
|
19
|
+
}
|
|
20
|
+
})();
|
|
@@ -1 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// Mobile toggle state lives in the `checked` attribute (Android) or `value` "1"/"0" (iOS) — NOT
|
|
2
|
+
// `selected` (what toBeSelected() reads; always false for a Flutter Switch). The composite row node
|
|
3
|
+
// carries it, so the row selector resolves the state directly.
|
|
4
|
+
await expect({{> locator}}).toHaveAttribute(driver.isIOS ? 'value' : 'checked', driver.isIOS ? '1' : 'true');
|
|
@@ -1 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// Mobile toggle state lives in the `checked` attribute (Android) or `value` "1"/"0" (iOS) — NOT
|
|
2
|
+
// `selected` (what toBeSelected() reads; always false for a Flutter Switch). The composite row node
|
|
3
|
+
// carries it, so the row selector resolves the state directly.
|
|
4
|
+
await expect({{> locator}}).not.toHaveAttribute(driver.isIOS ? 'value' : 'checked', driver.isIOS ? '1' : 'true');
|
|
@@ -152,6 +152,9 @@ describe('{{featureName}}', () => {
|
|
|
152
152
|
const __APP_PKG__ = __resolveAppId();
|
|
153
153
|
try { await driver.terminateApp(__APP_PKG__); } catch { /* app may not be running yet */ }
|
|
154
154
|
await driver.activateApp(__APP_PKG__);
|
|
155
|
+
// Restore portrait so a prior scenario's `rotate to landscape` can't leak into this one — the
|
|
156
|
+
// app relaunch above resets in-app state, but device orientation is OS-level and persists.
|
|
157
|
+
try { await driver.setOrientation('PORTRAIT'); } catch { /* orientation control not supported */ }
|
|
155
158
|
// Settle after a cold (re)launch: a Flutter app first renders a BLANK accessibility skeleton
|
|
156
159
|
// (nested FrameLayout/View, no content-desc) for several seconds before the real tree appears.
|
|
157
160
|
// That skeleton is static, so a "two equal reads" check alone false-positives on it and we'd
|
|
@@ -74,7 +74,7 @@ function extractCleanupFlags(tags: string[]): { overlay?: boolean; forms?: boole
|
|
|
74
74
|
* Any tag not recognized by sungen as functional → pass through.
|
|
75
75
|
*/
|
|
76
76
|
const FUNCTIONAL_TAG_PREFIXES = [
|
|
77
|
-
'@parallel', '@cleanup:', '@auth:', '@manual', '@no-auth',
|
|
77
|
+
'@parallel', '@serial', '@cleanup:', '@auth:', '@manual', '@no-auth',
|
|
78
78
|
'@steps:', '@extend:', '@screenshot:', '@beforeAll', '@afterEach', '@afterAll',
|
|
79
79
|
'@flow', '@cases:', '@deferred:', '@owned-by:',
|
|
80
80
|
];
|
|
@@ -173,8 +173,10 @@ function getEffectiveAuthRole(
|
|
|
173
173
|
* @manual at scenario level → skip that scenario
|
|
174
174
|
*/
|
|
175
175
|
function isManual(tags: string[]): boolean {
|
|
176
|
-
//
|
|
177
|
-
|
|
176
|
+
// Recognize bare `@manual` AND the reason-coded `@manual:Mx` convention (what create-test emits) —
|
|
177
|
+
// must match harness/parse.ts + capability-plan.ts, else an `@manual:Mx` scenario is wrongly
|
|
178
|
+
// GENERATED (and run) instead of skipped. @deferred:flow is owned by a flow → also skips here (H6).
|
|
179
|
+
return tags.some(tag => /^@manual\b/i.test(tag) || tag === '@deferred:flow');
|
|
178
180
|
}
|
|
179
181
|
|
|
180
182
|
/**
|
|
@@ -271,10 +273,13 @@ export class CodeGenerator {
|
|
|
271
273
|
const depth = outputSubdir ? outputSubdir.split('/').length : 0;
|
|
272
274
|
const basePath = depth > 0 ? Array(depth).fill('..').join('/') : '..';
|
|
273
275
|
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
+
// Isolation model: per-scenario fresh page is the DEFAULT (no cascade — one failure can't skip
|
|
277
|
+
// the rest). `@serial` opts INTO the shared-page serial suite (a genuinely stateful, ordered
|
|
278
|
+
// sequence). `@parallel` is a back-compat alias of the default (isolated). See #414.
|
|
279
|
+
const isSerialFeature = (feature.tags || []).includes('@serial');
|
|
280
|
+
const isParallelFeature = !isSerialFeature; // isolated unless explicitly @serial
|
|
276
281
|
const hasCleanupTags = (feature.tags || []).some(t => t.startsWith('@cleanup:'));
|
|
277
|
-
const needsCleanupImport =
|
|
282
|
+
const needsCleanupImport = isSerialFeature && hasCleanupTags; // afterEach cleanup only on the shared-page path
|
|
278
283
|
|
|
279
284
|
// Active capabilities for this feature (registry-driven): the default UI + any whose annotation
|
|
280
285
|
// tags appear (@query) or whose detectsStep matches (declarative DB steps). Each active
|
|
@@ -480,14 +485,23 @@ export class CodeGenerator {
|
|
|
480
485
|
}
|
|
481
486
|
}
|
|
482
487
|
|
|
483
|
-
//
|
|
484
|
-
|
|
488
|
+
// Per-scenario ISOLATION is the default (test.describe + a fresh page each scenario → a failure
|
|
489
|
+
// never skip-cascades). `@serial` opts into the shared-page serial suite for a stateful sequence.
|
|
490
|
+
const isParallel = !(feature.tags || []).includes('@serial');
|
|
485
491
|
|
|
486
492
|
// Generate background if exists
|
|
487
493
|
let background: string | undefined;
|
|
488
494
|
let backgroundSteps: Array<{ comment?: string; code: string }> | undefined;
|
|
489
495
|
if (feature.background) {
|
|
490
|
-
|
|
496
|
+
// Mobile (appium) ALWAYS inlines Background via `backgroundSteps`: the test-file template emits a
|
|
497
|
+
// relaunch-per-scenario beforeEach and renders Background INSIDE it (it has no separate `background`
|
|
498
|
+
// beforeEach slot), and that per-scenario relaunch already gives @parallel its reset semantics.
|
|
499
|
+
// Routing a @parallel *mobile* feature to generateBeforeEach() computes the nav steps into
|
|
500
|
+
// `background`, which the mobile template then ignores → the Background nav (e.g. `tap [Settings]
|
|
501
|
+
// tab`) is silently dropped and every scenario runs on the launcher. So branch on isParallel only
|
|
502
|
+
// for web; for appium always use generateBackgroundSteps().
|
|
503
|
+
const isMobile = this.adapter.frameworkName === 'appium';
|
|
504
|
+
if (isParallel && !isMobile) {
|
|
491
505
|
background = await this.generateBeforeEach(feature.background);
|
|
492
506
|
} else {
|
|
493
507
|
backgroundSteps = await this.generateBackgroundSteps(feature.background);
|
|
@@ -597,6 +611,29 @@ export class CodeGenerator {
|
|
|
597
611
|
const cleanup = extractCleanupFlags(feature.tags || []);
|
|
598
612
|
const screenshotOnFailure = hasScreenshotOnFailure(feature.tags || []);
|
|
599
613
|
|
|
614
|
+
// Mobile: the @platform:* value (mobile|android|ios). Emitted as a `// sungen:platform=` marker
|
|
615
|
+
// so wdio.conf routes the spec per OS — android/ios = that OS only, mobile (default) = both.
|
|
616
|
+
// Only the appium adapter consumes it; web features have no @platform tag → undefined → no marker.
|
|
617
|
+
const platform = this.adapter.frameworkName === 'appium'
|
|
618
|
+
? (feature.tags || []).find(t => t.startsWith('@platform:'))?.split(':')[1]
|
|
619
|
+
: undefined;
|
|
620
|
+
|
|
621
|
+
// Mobile: parse the feature `Path:` (the app entry) into the app id(s) the per-scenario relaunch
|
|
622
|
+
// reset needs. Dual-id form: `<androidPkg>[/<activity>] | <iosBundleId>` — appPackage is the Android
|
|
623
|
+
// package (activity stripped, it isn't needed for terminateApp/activateApp); iosBundleId is the part
|
|
624
|
+
// after `|` when present. Without these the test-file template's `{{#if appPackage}}` stays false and
|
|
625
|
+
// the beforeEach (terminate+activate) reset is never emitted → nav scenarios cascade-fail.
|
|
626
|
+
let appPackage: string | undefined;
|
|
627
|
+
let iosBundleId: string | undefined;
|
|
628
|
+
if (this.adapter.frameworkName === 'appium' && feature.path) {
|
|
629
|
+
const [androidPart, iosPart] = feature.path.split('|').map(s => s.trim());
|
|
630
|
+
// Guard against a web-style URL Path (`/login`): only treat as a package when it isn't a route.
|
|
631
|
+
if (androidPart && !androidPart.startsWith('/')) {
|
|
632
|
+
appPackage = androidPart.split('/')[0].trim() || undefined;
|
|
633
|
+
}
|
|
634
|
+
if (iosPart) iosBundleId = iosPart;
|
|
635
|
+
}
|
|
636
|
+
|
|
600
637
|
// Use adapter to render the complete test file structure
|
|
601
638
|
return this.adapter.renderTestFile({
|
|
602
639
|
imports: '', // Not used in template as it's rendered separately
|
|
@@ -613,11 +650,14 @@ export class CodeGenerator {
|
|
|
613
650
|
featureFileName: featureName,
|
|
614
651
|
isParallel,
|
|
615
652
|
flowMode: isFlowFeature,
|
|
653
|
+
platform,
|
|
616
654
|
cleanup,
|
|
617
655
|
backgroundSteps,
|
|
618
656
|
scenarios: needsGrouping ? [] : scenarios,
|
|
619
657
|
authGroups: needsGrouping ? authGroups : undefined,
|
|
620
658
|
singleAuthRole,
|
|
659
|
+
appPackage,
|
|
660
|
+
iosBundleId,
|
|
621
661
|
});
|
|
622
662
|
}
|
|
623
663
|
|
|
@@ -21,9 +21,19 @@ export class TemplateEngine {
|
|
|
21
21
|
return text.split('\n').map(line => indentation + line).join('\n');
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
+
// Escape a raw string for embedding inside a single-quoted JS string literal. Every caller wraps
|
|
25
|
+
// the result in '…' (selector values, attribute values, scope), so a value containing a newline,
|
|
26
|
+
// backslash, or quote must be escaped or the generated spec is a syntax error (e.g. a Flutter
|
|
27
|
+
// composite content-desc like "Quick Convert\nSelect File\n…" used as an accessibility-id).
|
|
28
|
+
// Order matters: backslash first, then the rest.
|
|
24
29
|
Handlebars.registerHelper('escapeQuotes', function(text: string) {
|
|
25
30
|
if (text == null) return '';
|
|
26
|
-
return String(text)
|
|
31
|
+
return String(text)
|
|
32
|
+
.replace(/\\/g, '\\\\')
|
|
33
|
+
.replace(/'/g, "\\'")
|
|
34
|
+
.replace(/\n/g, '\\n')
|
|
35
|
+
.replace(/\r/g, '\\r')
|
|
36
|
+
.replace(/\t/g, '\\t');
|
|
27
37
|
});
|
|
28
38
|
|
|
29
39
|
Handlebars.registerHelper('escapeRegex', function(text: string) {
|
|
@@ -259,11 +269,14 @@ export class TemplateEngine {
|
|
|
259
269
|
screenName?: string;
|
|
260
270
|
featureFileName?: string;
|
|
261
271
|
isParallel?: boolean;
|
|
272
|
+
platform?: string;
|
|
262
273
|
cleanup?: { overlay?: boolean; forms?: boolean; scroll?: boolean; storage?: boolean };
|
|
263
274
|
backgroundSteps?: Array<{ comment?: string; code: string }>;
|
|
264
275
|
scenarios: string[];
|
|
265
276
|
authGroups?: Array<{ authRole?: string; scenarios: string[] }>;
|
|
266
277
|
singleAuthRole?: string;
|
|
278
|
+
appPackage?: string;
|
|
279
|
+
iosBundleId?: string;
|
|
267
280
|
}): string {
|
|
268
281
|
return this.render('test-file', data);
|
|
269
282
|
}
|
|
@@ -20,6 +20,12 @@ interface SelectorEntry {
|
|
|
20
20
|
scope?: string; // Parent landmark aria-label to scope within (e.g., 'desktop navigation')
|
|
21
21
|
match?: 'exact' | 'partial'; // For getByText matching (default: partial)
|
|
22
22
|
|
|
23
|
+
// === Mobile per-platform variant (issue #392) ===
|
|
24
|
+
// `android:`/`ios:` sub-selectors for one logical element whose locator differs per OS (composite
|
|
25
|
+
// content-desc, partial/dynamic text, native predicate). Each is a full SelectorEntry.
|
|
26
|
+
android?: SelectorEntry;
|
|
27
|
+
ios?: SelectorEntry;
|
|
28
|
+
|
|
23
29
|
// === v2: Complex Elements ===
|
|
24
30
|
variant?: 'native' | 'custom' | 'dragdrop'; // Dropdown/upload subtype
|
|
25
31
|
trigger?: string; // Visible button that triggers hidden file input
|
|
@@ -52,8 +58,17 @@ interface LegacySelectorFile {
|
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
export interface ResolvedSelector {
|
|
55
|
-
|
|
61
|
+
// Web (Playwright) strategies + mobile (Appium) strategies. Mobile types pass through from
|
|
62
|
+
// selectors.yaml so the appium adapter's `appium-selector` switch can emit the right WDIO selector
|
|
63
|
+
// (`~` / raw xpath / `android=` / `-ios predicate string:` / `id=`). See issue #392.
|
|
64
|
+
strategy: 'testid' | 'id' | 'placeholder' | 'label' | 'text' | 'css' | 'role' | 'locator'
|
|
65
|
+
| 'accessibility-id' | 'xpath' | 'android-uiautomator' | 'ios-predicate';
|
|
56
66
|
value: string;
|
|
67
|
+
// Mobile per-platform variant (`android:`/`ios:` in selectors.yaml). When set, the appium codegen
|
|
68
|
+
// emits a runtime `driver.isIOS ? '<ios>' : '<android>'` so one @platform:mobile spec resolves the
|
|
69
|
+
// right native selector per OS. Each side carries its own strategy+value (issue #392).
|
|
70
|
+
androidSelector?: { strategy: ResolvedSelector['strategy']; value: string; name?: string };
|
|
71
|
+
iosSelector?: { strategy: ResolvedSelector['strategy']; value: string; name?: string };
|
|
57
72
|
role?: string; // For role-based selectors (e.g., 'button', 'link')
|
|
58
73
|
name?: string; // For accessible name in role selectors
|
|
59
74
|
locator?: string; // CSS locator for fallback
|
|
@@ -472,6 +487,25 @@ export class SelectorResolver {
|
|
|
472
487
|
return { ...resolved, ...v2Fields };
|
|
473
488
|
};
|
|
474
489
|
|
|
490
|
+
// Mobile per-platform variant (issue #392): `{ android: {...}, ios: {...} }`. Resolve each side
|
|
491
|
+
// and emit androidSelector/iosSelector so the appium codegen renders `driver.isIOS ? ios : android`.
|
|
492
|
+
// The base strategy/value falls back to whichever side is present (a missing side falls back to it).
|
|
493
|
+
// `nth`/`scope` are shared (top-level) — they cannot diverge per-OS (the codegen ternary swaps only
|
|
494
|
+
// strategy+value); a case needing per-OS nth/scope is a sub-feature, not a variant.
|
|
495
|
+
if (entry.android || entry.ios) {
|
|
496
|
+
const av = entry.android ? this.resolveFromEntry(entry.android, originalLabel) : undefined;
|
|
497
|
+
const iv = entry.ios ? this.resolveFromEntry(entry.ios, originalLabel) : undefined;
|
|
498
|
+
const base = (av || iv) as ResolvedSelector;
|
|
499
|
+
return withExtras({
|
|
500
|
+
strategy: base.strategy,
|
|
501
|
+
value: base.value,
|
|
502
|
+
name: base.name,
|
|
503
|
+
nth,
|
|
504
|
+
...(av ? { androidSelector: { strategy: av.strategy, value: av.value, name: av.name } } : {}),
|
|
505
|
+
...(iv ? { iosSelector: { strategy: iv.strategy, value: iv.value, name: iv.name } } : {}),
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
475
509
|
// If type is 'locator', use locator field (or value as fallback) as the CSS locator directly
|
|
476
510
|
if (type === 'locator') {
|
|
477
511
|
const locatorValue = locator && locator.trim() ? locator : value;
|
|
@@ -536,6 +570,22 @@ export class SelectorResolver {
|
|
|
536
570
|
exact,
|
|
537
571
|
});
|
|
538
572
|
|
|
573
|
+
// Mobile (Appium) native strategies — pass through unchanged; the appium adapter's
|
|
574
|
+
// `appium-selector` switch renders the right WDIO selector (`~` / raw xpath / `android=` /
|
|
575
|
+
// `-ios predicate string:` / `id=`). On web these types never appear. See issue #392.
|
|
576
|
+
case 'accessibility-id':
|
|
577
|
+
case 'xpath':
|
|
578
|
+
case 'android-uiautomator':
|
|
579
|
+
case 'ios-predicate':
|
|
580
|
+
case 'id':
|
|
581
|
+
return withExtras({
|
|
582
|
+
strategy: type,
|
|
583
|
+
value,
|
|
584
|
+
name,
|
|
585
|
+
nth,
|
|
586
|
+
exact,
|
|
587
|
+
});
|
|
588
|
+
|
|
539
589
|
default:
|
|
540
590
|
// Fallback to placeholder
|
|
541
591
|
return withExtras({
|
package/src/harness/audit.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { readIntent, projectRootFromScreenDir, IntentProfile } from './intent';
|
|
|
22
22
|
import { getProvenance, Provenance } from './provenance';
|
|
23
23
|
import { specCoverage, SpecCoverageResult, parseSpecClauses } from './spec-coverage';
|
|
24
24
|
import { downstreamScope, manualOracle, readText, DownstreamResult, ManualOracleResult,
|
|
25
|
-
negativeSideEffect, sourceBacked, crossArtifactOwnership, isolationRisk } from './quality-gates';
|
|
25
|
+
negativeSideEffect, sourceBacked, crossArtifactOwnership, isolationRisk, serialCascadeRisk } from './quality-gates';
|
|
26
26
|
import { viewpointLedger, parseViewpointItems, LedgerResult } from './viewpoint-ledger';
|
|
27
27
|
import { capabilityRegistry } from '../capabilities/registry';
|
|
28
28
|
import { discoverAndRegisterCapabilities } from '../capabilities/discover';
|
|
@@ -146,7 +146,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
146
146
|
const capGate = scoringCap?.gateProvider as
|
|
147
147
|
((i: { scenarios: ScenarioInfo[]; viewpoints: ViewpointEntry[]; catalog: Catalog; focus: typeof intent.focus; cwd: string; screenName: string; threshold: number; businessCriticalMethods?: string[] }) => { gate: GateResult; depth: DepthResult }) | undefined;
|
|
148
148
|
const provided = capGate?.({ scenarios, viewpoints, catalog, focus: intent.focus, cwd: projectRootFromScreenDir(screenDir), screenName: catalogScreenName, threshold: depthThresholdFor(intent.focus), businessCriticalMethods: intent.businessCriticalMethods });
|
|
149
|
-
const gate = provided?.gate ?? viewpointGate(scenarios, viewpoints, catalog);
|
|
149
|
+
const gate = provided?.gate ?? viewpointGate(scenarios, viewpoints, catalog, platform === 'mobile');
|
|
150
150
|
const depth = provided?.depth ?? assertionDepth(scenarios, dataThemesFor(catalog, gate.pageType), intent.focus);
|
|
151
151
|
|
|
152
152
|
// TQ-4 — deferral-aware coverage credit: an inherently cross-screen theme (cart / detail /
|
|
@@ -244,6 +244,13 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
244
244
|
if (isoRisk) {
|
|
245
245
|
findings.push('ISOLATION-RISK: this @parallel flow mutates the cart but has no per-scenario isolation → cart/count/quantity asserts can go flaky when scenarios share state. Add `@cleanup:cart` (or `@isolate`, or a `Given User has an empty cart` background) so each scenario starts clean.');
|
|
246
246
|
}
|
|
247
|
+
// #414 — serial-cascade masking: an @serial suite of independent (re-navigating) scenarios hides
|
|
248
|
+
// failures, because Playwright's describe.serial SKIPS everything after the first red. Isolation is
|
|
249
|
+
// the default, so this only fires when @serial was added to a suite that didn't need it.
|
|
250
|
+
const cascadeN = serialCascadeRisk(featureText, scenarios.length);
|
|
251
|
+
if (cascadeN) {
|
|
252
|
+
findings.push(`SERIAL-CASCADE-RISK: this \`@serial\` suite has ${cascadeN} INDEPENDENT scenarios (each re-navigates via Background) that don't need a shared-page sequence. \`describe.serial\` SKIPS every test after the first failure → one early red MASKS the rest as never-run. **Drop \`@serial\`** — the default per-scenario isolation runs every scenario and isolates failures. Keep \`@serial\` only for a genuinely stateful ordered sequence (where scenario N depends on N-1's state).`);
|
|
253
|
+
}
|
|
247
254
|
if (flowDepth.stateful && flowDepth.missing.length) {
|
|
248
255
|
const how: Record<string, string> = {
|
|
249
256
|
'count-proof': 'assert the cart ROW COUNT / item QUANTITY (e.g. `... table with {{two_rows}}`, `Quantity column with {{qty}}`), not just the row presence',
|
|
@@ -91,6 +91,21 @@ page_types:
|
|
|
91
91
|
keywords: ["success message", "saved", "created", "confirmation"]
|
|
92
92
|
template: "User see [Success] message with {{success_text}}"
|
|
93
93
|
|
|
94
|
+
# Mobile launcher / home screen (Appium). Display + navigation only — no web form/validation.
|
|
95
|
+
# A `mobile-*` candidate (considered ONLY for mobile targets; never for web). Keywords are
|
|
96
|
+
# launcher-DISTINCTIVE (a tab-bar landing screen) and deliberately exclude generic tokens like
|
|
97
|
+
# "tab"/"navigation"/"home" that a mobile commerce/list screen also carries — so such a screen
|
|
98
|
+
# still selects ecommerce-list, not mobile-home. Both themes are presence/navigation (no `depth:`)
|
|
99
|
+
# — seeing the content and leaving Home (a destination marker OR the Home-only anchor going
|
|
100
|
+
# `is hidden`) IS the assertion; no data-assertion is forced.
|
|
101
|
+
mobile-home:
|
|
102
|
+
detect_keywords: ["bottom navigation", launcher, greeting, "home screen", "launcher screen"]
|
|
103
|
+
must_cover:
|
|
104
|
+
- theme: content-visibility
|
|
105
|
+
keywords: [displayed, render, loads, exposes, shown, visible]
|
|
106
|
+
- theme: navigation-correctness
|
|
107
|
+
keywords: [tap, navigate, "leaves home", "is hidden", opens, navigation]
|
|
108
|
+
|
|
94
109
|
auth:
|
|
95
110
|
detect_keywords: [login, logout, password, signin, "sign in", credential]
|
|
96
111
|
must_cover:
|
|
@@ -1,87 +1,98 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Static skill-lint (Eval Harness L1) — deterministic quality checks on Sungen's OWN
|
|
3
|
-
*
|
|
4
|
-
* ships. Learned (generically) from the "static validations" tier of an agent-kit evals
|
|
5
|
-
* layer. No project data — this lints the sungen package's own templates.
|
|
3
|
+
* single-source AI templates (`templates/ai-src/`), so a broken skill fails before it ships.
|
|
6
4
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
5
|
+
* Source of truth is now `ai-src/` (one file per skill/command, cap-fenced for per-assistant
|
|
6
|
+
* deltas) — NOT the retired per-assistant `ai-instructions/` duplicates. Checks:
|
|
7
|
+
* - skills: `ai-src/skills/<dir>/SKILL.md` has `name:` + `description:` frontmatter
|
|
8
|
+
* - commands: `ai-src/commands/<name>.md` has `description:` frontmatter
|
|
9
|
+
* - cap-fence validity: every {{#cap}}/{{^cap}} uses a known tag and is closed
|
|
10
|
+
* - line budget: context-cost smell (advisory)
|
|
11
|
+
* Aux fragments (viewpoint group-*.md, capture mode files) load via a parent and need no
|
|
12
|
+
* frontmatter, so only `SKILL.md` is frontmatter-checked.
|
|
13
13
|
*/
|
|
14
14
|
import * as fs from 'fs';
|
|
15
15
|
import * as path from 'path';
|
|
16
|
-
import {
|
|
16
|
+
import { stripCaps, VALID_CAPS } from '../../orchestrator/ai-skills/cap-stripper';
|
|
17
17
|
|
|
18
18
|
export interface SkillLintFinding { level: 'error' | 'warn'; file: string; rule: string; detail: string }
|
|
19
19
|
export interface SkillLintResult { checked: number; findings: SkillLintFinding[]; errors: number }
|
|
20
20
|
|
|
21
|
-
const LINE_BUDGET = 700;
|
|
22
|
-
const SKILL_RE = /^(claude|github)-skill-/;
|
|
21
|
+
const LINE_BUDGET = 700;
|
|
23
22
|
|
|
24
|
-
function
|
|
23
|
+
function frontmatter(text: string): string | null {
|
|
25
24
|
const m = text.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
26
|
-
|
|
27
|
-
return { fm: m[1], body: text.slice(m[0].length) };
|
|
25
|
+
return m ? m[1] : null;
|
|
28
26
|
}
|
|
29
27
|
|
|
30
|
-
/** Lint the
|
|
31
|
-
export function lintSkills(
|
|
28
|
+
/** Lint the single-source templates under `srcRoot` (the `ai-src` dir). */
|
|
29
|
+
export function lintSkills(srcRoot: string): SkillLintResult {
|
|
32
30
|
const findings: SkillLintFinding[] = [];
|
|
33
|
-
|
|
34
|
-
const skillFiles = files.filter((f) => SKILL_RE.test(f));
|
|
31
|
+
let checked = 0;
|
|
35
32
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
const checkCaps = (rel: string, text: string) => {
|
|
34
|
+
try { for (const cap of VALID_CAPS) stripCaps(text, [cap], rel); stripCaps(text, [], rel); }
|
|
35
|
+
catch (e) { findings.push({ level: 'error', file: rel, rule: 'cap-fence', detail: String((e as Error).message) }); }
|
|
36
|
+
};
|
|
37
|
+
const lineBudget = (rel: string, text: string) => {
|
|
38
|
+
const n = text.split('\n').length;
|
|
39
|
+
if (n > LINE_BUDGET) findings.push({ level: 'warn', file: rel, rule: 'line-budget', detail: `${n} lines > ${LINE_BUDGET} (context-cost smell)` });
|
|
40
|
+
};
|
|
39
41
|
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (!
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (!/\bdescription\s*:/.test(fm)) findings.push({ level: 'error', file: f, rule: 'frontmatter-description', detail: 'no `description:` in frontmatter' });
|
|
42
|
+
// Skills
|
|
43
|
+
const skillsDir = path.join(srcRoot, 'skills');
|
|
44
|
+
for (const dir of fs.existsSync(skillsDir) ? fs.readdirSync(skillsDir) : []) {
|
|
45
|
+
const dp = path.join(skillsDir, dir);
|
|
46
|
+
if (!fs.statSync(dp).isDirectory()) continue;
|
|
47
|
+
for (const file of fs.readdirSync(dp)) {
|
|
48
|
+
if (!file.endsWith('.md')) continue;
|
|
49
|
+
const rel = `skills/${dir}/${file}`;
|
|
50
|
+
const text = fs.readFileSync(path.join(dp, file), 'utf8');
|
|
51
|
+
checkCaps(rel, text);
|
|
52
|
+
lineBudget(rel, text);
|
|
53
|
+
if (file === 'SKILL.md') {
|
|
54
|
+
checked++;
|
|
55
|
+
const fm = frontmatter(text);
|
|
56
|
+
if (!fm) { findings.push({ level: 'error', file: rel, rule: 'frontmatter', detail: 'SKILL.md missing --- frontmatter ---' }); continue; }
|
|
57
|
+
if (!/\bname\s*:/.test(fm)) findings.push({ level: 'error', file: rel, rule: 'frontmatter-name', detail: 'no `name:` in frontmatter' });
|
|
58
|
+
if (!/\bdescription\s*:/.test(fm)) findings.push({ level: 'error', file: rel, rule: 'frontmatter-description', detail: 'no `description:` in frontmatter' });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (
|
|
63
|
+
// Agents (Claude-only sub-agents)
|
|
64
|
+
const agentsDir = path.join(srcRoot, 'agents');
|
|
65
|
+
for (const file of fs.existsSync(agentsDir) ? fs.readdirSync(agentsDir) : []) {
|
|
66
|
+
if (!file.endsWith('.md')) continue;
|
|
67
|
+
const rel = `agents/${file}`;
|
|
68
|
+
const text = fs.readFileSync(path.join(agentsDir, file), 'utf8');
|
|
69
|
+
checked++;
|
|
70
|
+
checkCaps(rel, text);
|
|
71
|
+
lineBudget(rel, text);
|
|
72
|
+
const fm = frontmatter(text);
|
|
73
|
+
if (!fm) { findings.push({ level: 'error', file: rel, rule: 'frontmatter', detail: 'agent missing --- frontmatter ---' }); continue; }
|
|
74
|
+
if (!/\bname\s*:/.test(fm)) findings.push({ level: 'error', file: rel, rule: 'frontmatter-name', detail: 'no `name:` in frontmatter' });
|
|
75
|
+
if (!/\bdescription\s*:/.test(fm)) findings.push({ level: 'error', file: rel, rule: 'frontmatter-description', detail: 'no `description:` in frontmatter' });
|
|
66
76
|
}
|
|
67
77
|
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
(
|
|
78
|
+
// Commands
|
|
79
|
+
const cmdsDir = path.join(srcRoot, 'commands');
|
|
80
|
+
for (const file of fs.existsSync(cmdsDir) ? fs.readdirSync(cmdsDir) : []) {
|
|
81
|
+
if (!file.endsWith('.md')) continue;
|
|
82
|
+
const rel = `commands/${file}`;
|
|
83
|
+
const text = fs.readFileSync(path.join(cmdsDir, file), 'utf8');
|
|
84
|
+
checked++;
|
|
85
|
+
checkCaps(rel, text);
|
|
86
|
+
lineBudget(rel, text);
|
|
87
|
+
const fm = frontmatter(text);
|
|
88
|
+
if (!fm) { findings.push({ level: 'error', file: rel, rule: 'frontmatter', detail: 'command missing --- frontmatter ---' }); continue; }
|
|
89
|
+
if (!/\bdescription\s*:/.test(fm)) findings.push({ level: 'error', file: rel, rule: 'frontmatter-description', detail: 'no `description:` in frontmatter' });
|
|
76
90
|
}
|
|
77
|
-
for (const n of claudeSkills) if (!githubSkills.has(n)) findings.push({ level: 'warn', file: `claude .../${n}/SKILL.md`, rule: 'variant-missing', detail: `Claude skill "${n}" has no GitHub (Copilot) variant` });
|
|
78
|
-
for (const n of githubSkills) if (!claudeSkills.has(n)) findings.push({ level: 'warn', file: `github .../${n}/SKILL.md`, rule: 'variant-missing', detail: `GitHub skill "${n}" has no Claude variant` });
|
|
79
91
|
|
|
80
|
-
return { checked
|
|
92
|
+
return { checked, findings, errors: findings.filter((f) => f.level === 'error').length };
|
|
81
93
|
}
|
|
82
94
|
|
|
83
|
-
/** Default
|
|
95
|
+
/** Default source dir, resolved relative to this module (works from src via tsx and dist). */
|
|
84
96
|
export function defaultSkillDir(): string {
|
|
85
|
-
|
|
86
|
-
return path.resolve(__dirname, '..', '..', 'orchestrator', 'templates', 'ai-instructions');
|
|
97
|
+
return path.resolve(__dirname, '..', '..', 'orchestrator', 'templates', 'ai-src');
|
|
87
98
|
}
|