@guanlin4924/devflow-pi 0.1.93 → 0.1.95
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/README.md +10 -10
- package/node_modules/@playwright/test/LICENSE +202 -0
- package/node_modules/@playwright/test/NOTICE +5 -0
- package/node_modules/@playwright/test/README.md +318 -0
- package/node_modules/@playwright/test/cli.js +19 -0
- package/node_modules/@playwright/test/index.d.ts +18 -0
- package/node_modules/@playwright/test/index.js +17 -0
- package/node_modules/@playwright/test/index.mjs +18 -0
- package/node_modules/@playwright/test/package.json +35 -0
- package/node_modules/@playwright/test/reporter.d.ts +17 -0
- package/node_modules/@playwright/test/reporter.js +17 -0
- package/node_modules/@playwright/test/reporter.mjs +17 -0
- package/node_modules/playwright/LICENSE +202 -0
- package/node_modules/playwright/NOTICE +5 -0
- package/node_modules/playwright/README.md +318 -0
- package/node_modules/playwright/ThirdPartyNotices.txt +14 -0
- package/node_modules/playwright/cli.js +19 -0
- package/node_modules/playwright/index.d.ts +17 -0
- package/node_modules/playwright/index.js +17 -0
- package/node_modules/playwright/index.mjs +18 -0
- package/node_modules/playwright/jsx-runtime.js +42 -0
- package/node_modules/playwright/jsx-runtime.mjs +21 -0
- package/node_modules/playwright/lib/agents/agentParser.js +89 -0
- package/node_modules/playwright/lib/agents/copilot-setup-steps.yml +34 -0
- package/node_modules/playwright/lib/agents/generateAgents.js +395 -0
- package/node_modules/playwright/lib/agents/playwright-test-coverage.prompt.md +31 -0
- package/node_modules/playwright/lib/agents/playwright-test-generate.prompt.md +8 -0
- package/node_modules/playwright/lib/agents/playwright-test-generator.agent.md +88 -0
- package/node_modules/playwright/lib/agents/playwright-test-heal.prompt.md +6 -0
- package/node_modules/playwright/lib/agents/playwright-test-healer.agent.md +56 -0
- package/node_modules/playwright/lib/agents/playwright-test-plan.prompt.md +9 -0
- package/node_modules/playwright/lib/agents/playwright-test-planner.agent.md +74 -0
- package/node_modules/playwright/lib/cli/reportActions.js +78 -0
- package/node_modules/playwright/lib/cli/testActions.js +214 -0
- package/node_modules/playwright/lib/common/index.js +2923 -0
- package/node_modules/playwright/lib/common/index.js.txt +34 -0
- package/node_modules/playwright/lib/errorContext.js +129 -0
- package/node_modules/playwright/lib/globals.js +58 -0
- package/node_modules/playwright/lib/index.js +815 -0
- package/node_modules/playwright/lib/isomorphic.js +260 -0
- package/node_modules/playwright/lib/isomorphic.js.txt +9 -0
- package/node_modules/playwright/lib/loader/loaderProcessEntry.js +34 -0
- package/node_modules/playwright/lib/loader/loaderProcessEntry.js.txt +9 -0
- package/node_modules/playwright/lib/matchers/expect.js +13490 -0
- package/node_modules/playwright/lib/matchers/expect.js.LICENSE +640 -0
- package/node_modules/playwright/lib/matchers/expect.js.txt +67 -0
- package/node_modules/playwright/lib/mcp/test/browserBackend.js +135 -0
- package/node_modules/playwright/lib/mcp/test/generatorTools.js +122 -0
- package/node_modules/playwright/lib/mcp/test/plannerTools.js +150 -0
- package/node_modules/playwright/lib/mcp/test/seed.js +82 -0
- package/node_modules/playwright/lib/mcp/test/streams.js +44 -0
- package/node_modules/playwright/lib/mcp/test/testBackend.js +99 -0
- package/node_modules/playwright/lib/mcp/test/testContext.js +306 -0
- package/node_modules/playwright/lib/mcp/test/testTool.js +30 -0
- package/node_modules/playwright/lib/mcp/test/testTools.js +98 -0
- package/node_modules/playwright/lib/package.js +47 -0
- package/node_modules/playwright/lib/program.js +241 -0
- package/node_modules/playwright/lib/runner/index.js +8527 -0
- package/node_modules/playwright/lib/runner/index.js.txt +61 -0
- package/node_modules/playwright/lib/transform/babelBundle.js +72274 -0
- package/node_modules/playwright/lib/transform/babelBundle.js.LICENSE +2384 -0
- package/node_modules/playwright/lib/transform/babelBundle.js.txt +325 -0
- package/node_modules/playwright/lib/transform/esmLoader.js +8054 -0
- package/node_modules/playwright/lib/transform/esmLoader.js.LICENSE +355 -0
- package/node_modules/playwright/lib/transform/esmLoader.js.txt +60 -0
- package/node_modules/playwright/lib/util.js +373 -0
- package/node_modules/playwright/lib/worker/workerProcessEntry.js +1861 -0
- package/node_modules/playwright/lib/worker/workerProcessEntry.js.txt +19 -0
- package/node_modules/playwright/package.json +55 -0
- package/node_modules/playwright/test.d.ts +18 -0
- package/node_modules/playwright/test.js +24 -0
- package/node_modules/playwright/test.mjs +35 -0
- package/node_modules/playwright/types/test.d.ts +11074 -0
- package/node_modules/playwright/types/testReporter.d.ts +950 -0
- package/node_modules/playwright-core/LICENSE +202 -0
- package/node_modules/playwright-core/NOTICE +5 -0
- package/node_modules/playwright-core/README.md +3 -0
- package/node_modules/playwright-core/ThirdPartyNotices.txt +13 -0
- package/node_modules/playwright-core/bin/install_media_pack.ps1 +5 -0
- package/node_modules/playwright-core/bin/install_webkit_wsl.ps1 +33 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh +42 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh +13 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1 +24 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh +42 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh +12 -0
- package/node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1 +24 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh +48 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh +11 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1 +23 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh +48 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh +11 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1 +23 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh +48 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh +11 -0
- package/node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1 +24 -0
- package/node_modules/playwright-core/browsers.json +52 -0
- package/node_modules/playwright-core/cli.js +21 -0
- package/node_modules/playwright-core/index.d.ts +17 -0
- package/node_modules/playwright-core/index.js +17 -0
- package/node_modules/playwright-core/index.mjs +28 -0
- package/node_modules/playwright-core/lib/bootstrap.js +88 -0
- package/node_modules/playwright-core/lib/coreBundle.js +75813 -0
- package/node_modules/playwright-core/lib/entry/cliDaemon.js +5 -0
- package/node_modules/playwright-core/lib/entry/dashboardApp.js +3 -0
- package/node_modules/playwright-core/lib/entry/mcp.js +10 -0
- package/node_modules/playwright-core/lib/entry/oopBrowserDownload.js +3 -0
- package/node_modules/playwright-core/lib/package.js +50 -0
- package/node_modules/playwright-core/lib/server/chromium/appIcon.png +0 -0
- package/node_modules/playwright-core/lib/server/electron/loader.js +116 -0
- package/node_modules/playwright-core/lib/serverRegistry.js +1939 -0
- package/node_modules/playwright-core/lib/serverRegistry.js.LICENSE +65 -0
- package/node_modules/playwright-core/lib/tools/cli-client/channelSessions.js +141 -0
- package/node_modules/playwright-core/lib/tools/cli-client/cli.js +6 -0
- package/node_modules/playwright-core/lib/tools/cli-client/help.json +721 -0
- package/node_modules/playwright-core/lib/tools/cli-client/minimist.js +128 -0
- package/node_modules/playwright-core/lib/tools/cli-client/output.js +351 -0
- package/node_modules/playwright-core/lib/tools/cli-client/program.js +418 -0
- package/node_modules/playwright-core/lib/tools/cli-client/registry.js +180 -0
- package/node_modules/playwright-core/lib/tools/cli-client/session.js +259 -0
- package/node_modules/playwright-core/lib/tools/dashboard/appIcon.png +0 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/SKILL.md +425 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/element-attributes.md +23 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/playwright-tests.md +39 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/request-mocking.md +87 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/running-code.md +241 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/session-management.md +225 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/storage-state.md +275 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/test-generation.md +433 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/tracing.md +139 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/video-recording.md +143 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/SKILL.md +136 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/gallery-spec.md +144 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/migration.md +89 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/react.md +69 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/typing.md +173 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/vue.md +77 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-trace/SKILL.md +174 -0
- package/node_modules/playwright-core/lib/tools/utils/extension.js +112 -0
- package/node_modules/playwright-core/lib/tools/utils/socketConnection.js +108 -0
- package/node_modules/playwright-core/lib/utilsBundle.js +85357 -0
- package/node_modules/playwright-core/lib/utilsBundle.js.LICENSE +1890 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/codeMirrorModule--QdMvsKi.css +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/codeMirrorModule-CQ8RZGBm.js +32 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/codicon-DCmgc-ay.ttf +0 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-1bWoP6pv.svg +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-beta-k3eOH_eK.svg +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-nightly-Cp5nfeDT.svg +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/index-AwgwcYie.css +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/index-hRvO4yU2.js +12 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/safari-na3_-uQk.svg +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/index.html +29 -0
- package/node_modules/playwright-core/lib/vite/dashboard/playwright-logo.svg +24 -0
- package/node_modules/playwright-core/lib/vite/htmlReport/index.html +16 -0
- package/node_modules/playwright-core/lib/vite/htmlReport/report.css +2 -0
- package/node_modules/playwright-core/lib/vite/htmlReport/report.js +32 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css +1 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-CVQWJZAA.js +32 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/codicon-DCmgc-ay.ttf +0 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/index-BZpYJZ-P.css +1 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/index-hrlLqLtq.js +129 -0
- package/node_modules/playwright-core/lib/vite/recorder/index.html +29 -0
- package/node_modules/playwright-core/lib/vite/recorder/playwright-logo.svg +9 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-BbkfBe3n.js +32 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Ds6CBOo0.js +182 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-L3liM589.js +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-DywYcAf8.js +7 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.-QdMvsKi.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/codicon.DCmgc-ay.ttf +0 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.Bqk9acqE.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/index.B4cLoZK3.js +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/index.B_TqY17P.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/index.html +44 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/manifest.webmanifest +16 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/playwright-logo.svg +9 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.B_Jk1wbt.js +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.html +10 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/sw.bundle.js +4 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CU5KtEkS.js +5 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CyMfwkXJ.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +18 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.kHJ-D0s7.css +1 -0
- package/node_modules/playwright-core/lib/webp_codec.LICENSE +173 -0
- package/node_modules/playwright-core/lib/webp_codec.wasm +0 -0
- package/node_modules/playwright-core/lib/xdg-open +1267 -0
- package/node_modules/playwright-core/package.json +34 -0
- package/node_modules/playwright-core/types/protocol.d.ts +24645 -0
- package/node_modules/playwright-core/types/structs.d.ts +52 -0
- package/node_modules/playwright-core/types/types.d.ts +26632 -0
- package/package.json +12 -2
- package/plugins/devflow-core/extensions/devflow-core.ts +518 -40
- package/plugins/devflow-core/runtime/hooks/context_sync_gate.py +3 -3
- package/plugins/devflow-core/runtime/hooks/hook_common.py +334 -11
- package/plugins/devflow-core/runtime/hooks/index_gate.py +29 -16
- package/plugins/devflow-core/runtime/hooks/start.py +13 -13
- package/plugins/devflow-core/skills/wireframe-proposal/SKILL.md +2 -2
- package/plugins/devflow-core/skills/wireframe-validate/SKILL.md +4 -4
- package/plugins/devflow-core/skills/workflow-orchestrator/SKILL.md +19 -19
- package/plugins/devflow-core/skills/workflow-orchestrator/agents/openai.yaml +2 -2
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +412 -76
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +60 -9
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/api_test_matrix.py +2 -2
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/approval_pins.py +433 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/approval_store.py +137 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/architecture_assessment.rb +1 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/architecture_brief.rb +1 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/architecture_feasibility.rb +1 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_invalidation.rb +5 -5
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_commit.rb +4 -4
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_validate.py +1 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_source_resolver.rb +4 -4
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checker_adapter.rb +7 -7
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/control_io.rb +51 -7
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/cross_track_consistency_check.rb +1 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_coverage.py +87 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_v1.py +318 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/environment_profile.py +134 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +7 -6
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_surface.py +138 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_runtime.rb +3 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_state.rb +5 -5
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_workflow.rb +5 -5
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +13 -13
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +3 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/manage_lessons.rb +7 -7
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_recover.rb +2 -2
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_run.rb +3 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/phase_ownership.rb +6 -6
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/playwright_adapter.py +515 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/repair_control_plane.rb +1 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_coverage.rb +1 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph.rb +3 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/review_preflight.py +3 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/review_run.py +18 -18
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/runtime_context_collector.rb +3 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/session_rollover.rb +4 -4
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/static_runtime_context_collector.rb +2 -2
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/sync_artifacts.rb +5 -5
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/tc_hash.py +132 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/tdd_evidence.py +3 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/test_command_policy.py +104 -10
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/test_task_binding_check.py +3 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/update_triage_route.rb +3 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +19 -6
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_commit.rb +5 -5
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_validate.py +1 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/acceptance-remediation.yaml +12 -12
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +201 -222
- package/plugins/devflow-core/skills/workflow-pipeline/SKILL.md +6 -4
- package/plugins/devflow-core/skills/workflow-pipeline/references/pipeline-flow.md +68 -140
- package/plugins/devflow-core/skills/workflow-project-context/SKILL.md +14 -14
- package/plugins/devflow-core/skills/workflow-project-context/assets/agents-md-template.md +7 -7
- package/plugins/devflow-core/skills/workflow-project-context/assets/codebase-backend-spec-template.md +4 -4
- package/plugins/devflow-core/skills/workflow-project-context/assets/codebase-frontend-spec-template.md +4 -4
- package/plugins/devflow-core/skills/workflow-project-context/assets/codebase-navigation-template.md +1 -1
- package/plugins/devflow-core/skills/workflow-project-context/assets/context-domain-index-template.md +4 -4
- package/plugins/devflow-core/skills/workflow-project-context/assets/context-spec-index-template.md +10 -10
- package/plugins/devflow-core/skills/workflow-project-context/assets/execution-contract-template.md +2 -2
- package/plugins/devflow-core/skills/workflow-project-context/assets/navigation-index-template.md +18 -18
- package/plugins/devflow-core/skills/workflow-project-context/assets/project-initialization-template.md +32 -32
- package/plugins/devflow-core/skills/workflow-project-context/assets/rules-index-template.md +30 -30
- package/plugins/devflow-core/skills/workflow-project-context/assets/state-template.md +7 -7
- package/plugins/devflow-core/skills/workflow-project-context/references/navigation-guide.md +37 -37
- package/plugins/devflow-core/skills/workflow-project-context/scripts/validate_initialization.py +16 -16
- package/plugins/devflow-core/skills/workflow-project-context/scripts/validate_initialization_database.py +1 -1
- package/plugins/devflow-design/skills/workflow-api-call-guide/SKILL.md +4 -4
- package/plugins/devflow-design/skills/workflow-api-contract/SKILL.md +8 -8
- package/plugins/devflow-design/skills/workflow-api-supplement/SKILL.md +5 -5
- package/plugins/devflow-design/skills/workflow-architecture-feasibility/SKILL.md +1 -1
- package/plugins/devflow-design/skills/workflow-backend-tech-spec/SKILL.md +39 -39
- package/plugins/devflow-design/skills/workflow-backend-tech-spec/assets/backend-spec-template.md +10 -10
- package/plugins/devflow-design/skills/workflow-backend-tech-spec/references/affected-module-exploration.md +2 -2
- package/plugins/devflow-design/skills/workflow-backend-tech-spec/references/code-explore-guide.md +1 -1
- package/plugins/devflow-design/skills/workflow-backend-tech-spec/references/project-rule-contract.md +20 -20
- package/plugins/devflow-design/skills/workflow-design-rule-pack/SKILL.md +8 -8
- package/plugins/devflow-design/skills/workflow-design-rule-pack/assets/design/index.md +3 -3
- package/plugins/devflow-design/skills/workflow-design-rule-pack/assets/design/recommendations/accessibility.md +2 -2
- package/plugins/devflow-design/skills/workflow-design-rule-pack/assets/design/recommendations/component-library.md +2 -2
- package/plugins/devflow-design/skills/workflow-design-rule-pack/assets/design/recommendations/design-systems.md +1 -1
- package/plugins/devflow-design/skills/workflow-design-rule-pack/assets/design/recommendations/responsive-grid.md +2 -2
- package/plugins/devflow-design/skills/workflow-design-rule-pack/assets/design/recommendations/stack-guidelines.md +6 -6
- package/plugins/devflow-design/skills/workflow-design-rule-pack/assets/design/recommendations/ux-patterns.md +2 -2
- package/plugins/devflow-design/skills/workflow-design-rule-pack/assets/design/tokens/project-tokens.md +1 -1
- package/plugins/devflow-design/skills/workflow-design-rule-pack/assets/design/tools/ui-ux-pro-max/SKILL.md +12 -12
- package/plugins/devflow-design/skills/workflow-design-rule-pack/scripts/install.py +2 -2
- package/plugins/devflow-design/skills/workflow-feature-breakdown/SKILL.md +30 -30
- package/plugins/devflow-design/skills/workflow-feature-breakdown/assets/output-template.md +2 -2
- package/plugins/devflow-design/skills/workflow-frontend-tech-spec/SKILL.md +41 -41
- package/plugins/devflow-design/skills/workflow-frontend-tech-spec/assets/frontend-spec-template.md +15 -15
- package/plugins/devflow-design/skills/workflow-frontend-tech-spec/references/affected-module-exploration.md +2 -2
- package/plugins/devflow-design/skills/workflow-frontend-tech-spec/references/project-rule-contract.md +25 -25
- package/plugins/devflow-design/skills/workflow-integration-verify/SKILL.md +13 -13
- package/plugins/devflow-design/skills/workflow-ui-spec/SKILL.md +23 -23
- package/plugins/devflow-design/skills/workflow-ui-spec/assets/ui-spec-template.md +3 -3
- package/plugins/devflow-implementation/agents/implementation-worker.md +2 -2
- package/plugins/devflow-implementation/skills/workflow-bug-fix/SKILL.md +9 -9
- package/plugins/devflow-implementation/skills/workflow-bug-fix/references/lesson-format.md +2 -2
- package/plugins/devflow-implementation/skills/workflow-bug-fix/references/root-cause-ladder.md +1 -1
- package/plugins/devflow-implementation/skills/workflow-change-plan/SKILL.md +17 -17
- package/plugins/devflow-implementation/skills/workflow-change-plan/references/code-conventions.md +1 -1
- package/plugins/devflow-implementation/skills/workflow-change-plan/references/output-template.md +3 -3
- package/plugins/devflow-implementation/skills/workflow-change-plan/references/project-rule-contract.md +16 -16
- package/plugins/devflow-implementation/skills/workflow-execute-phase/SKILL.md +38 -38
- package/plugins/devflow-implementation/skills/workflow-execute-phase/references/execute-phase.md +4 -2
- package/plugins/devflow-intel/agents/project-scout.md +1 -1
- package/plugins/devflow-intel/extensions/devflow-intel.ts +20 -19
- package/plugins/devflow-intel/skills/code-index-query/SKILL.md +6 -6
- package/plugins/devflow-intel/skills/workflow-code-index/SKILL.md +12 -12
- package/plugins/devflow-intel/skills/workflow-code-index/references/discovery.md +2 -2
- package/plugins/devflow-intel/skills/workflow-code-index/references/query-guide.md +1 -1
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/code_index.py +31 -12
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/core/discovery.py +2 -2
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/core/scanner.py +4 -1
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/validators/project_binding_check.py +4 -4
- package/plugins/devflow-intel/skills/workflow-context-sync/SKILL.md +14 -14
- package/plugins/devflow-intel/skills/workflow-context-sync/scripts/context_sync_cursor.py +2 -2
- package/plugins/devflow-intel/skills/workflow-update-navigation/SKILL.md +26 -26
- package/plugins/devflow-intel/skills/workflow-update-navigation/references/incremental-update-guide.md +2 -2
- package/plugins/devflow-quality/runtime/playwright/devflow-test-dsl-v1.schema.json +872 -0
- package/plugins/devflow-quality/runtime/playwright/playwright.config.mjs +46 -0
- package/plugins/devflow-quality/runtime/playwright/reporter.mjs +311 -0
- package/plugins/devflow-quality/runtime/playwright/runner.spec.mjs +1308 -0
- package/plugins/devflow-quality/runtime/playwright/runtime-manifest.json +11 -0
- package/plugins/devflow-quality/skills/code-review/SKILL.md +12 -12
- package/plugins/devflow-quality/skills/code-review/references/spec-checklist.md +1 -1
- package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +11 -8
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/SKILL.md +6 -6
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/SKILL.md +14 -14
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/SKILL.md +82 -69
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/api-mainflow-rules.md +20 -15
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/codebase-discovery.md +12 -10
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/intermediate-format.md +115 -192
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/mainflow-selection.md +27 -23
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/project-dsl-contract.md +78 -153
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/ui-mainflow-rules.md +36 -83
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/compile_dsl.py +497 -556
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/discover_codebases.py +2 -2
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/export_legacy_testagent.py +647 -0
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/normalize_case_payloads.py +10 -3
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/validate_case_payloads.py +165 -275
- package/plugins/devflow-quality/skills/workflow-human-acceptance/SKILL.md +61 -0
- package/plugins/devflow-quality/skills/workflow-release-closeout/SKILL.md +18 -18
- package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/SKILL.md +20 -14
- package/plugins/devflow-requirements/skills/requirement-feasibility-review/SKILL.md +1 -1
- package/plugins/devflow-requirements/skills/workflow-demand-clarify/SKILL.md +10 -10
- package/plugins/devflow-requirements/skills/workflow-domain-modeling/SKILL.md +25 -25
- package/plugins/devflow-requirements/skills/workflow-requirement-baseline/SKILL.md +49 -49
- package/plugins/devflow-requirements/skills/workflow-requirement-baseline/assets/baseline-template.md +22 -22
- package/plugins/devflow-requirements/skills/workflow-requirement-baseline/references/antipatterns.md +3 -3
- package/plugins/devflow-requirements/skills/workflow-requirement-baseline/references/challenge-review-guide.md +3 -3
- package/plugins/devflow-requirements/skills/workflow-requirement-baseline/references/project-context-guide.md +11 -11
- package/plugins/devflow-requirements/skills/workflow-requirement-baseline/references/questioning.md +1 -1
- package/plugins/devflow-requirements/skills/workflow-requirement-baseline/references/requirement-review-checklist.md +4 -4
- package/plugins/devflow-requirements/skills/workflow-requirement-triage/SKILL.md +16 -16
- package/plugins/devflow-requirements/skills/workflow-wayfinder/SKILL.md +6 -6
- package/src/approval-store.ts +340 -0
- package/src/cli.ts +15 -0
- package/src/constants.ts +1 -1
- package/src/doctor.ts +259 -2
- package/src/initialization-store.ts +51 -3
- package/src/paths.ts +12 -1
- package/src/project-context.ts +60 -15
- package/src/project-documents.ts +61 -61
- package/src/project-initializer.ts +9 -8
- package/src/project-layout.ts +217 -0
- package/src/runtime.ts +3 -3
- package/templates/commands/start.md +4 -4
- package/templates/omp/AGENTS.md +1 -1
- package/templates/omp/hook-config.json +13 -7
- package/templates/omp/index-policy.json +8 -8
package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/compile_dsl.py
CHANGED
|
@@ -1,627 +1,568 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Compile AI-friendly case descriptions into
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"""Compile AI-friendly case descriptions into devflow-test-dsl/v1 files.
|
|
3
|
+
|
|
4
|
+
Input: ``.devflow/planning/generated-cases/<feature>/case-descriptions.json``
|
|
5
|
+
(format: ``references/intermediate-format.md``).
|
|
6
|
+
Output: one DSL file per frozen TC at
|
|
7
|
+
``.devflow/planning/generated-cases/<feature>/TC-xxxx.json``.
|
|
8
|
+
|
|
9
|
+
This script is the hard-constraint layer between AI judgement and the gate:
|
|
10
|
+
|
|
11
|
+
- every case must reference a frozen, confirmed, ``automated`` TC whose
|
|
12
|
+
``execution_surface`` is ``ui`` or ``api_flow`` (legacy page TCs derive to
|
|
13
|
+
``ui`` through the single Core implementation ``execution_surface.derive``);
|
|
14
|
+
- ``tc_sha256`` is always computed here with Core ``tc_hash`` (never copied
|
|
15
|
+
from the AI input);
|
|
16
|
+
- locators are compiled to v1 targets, ``testid`` first; ``xpath``, ``near``,
|
|
17
|
+
``ai_only``, Midscene, prompts, scripts and token injection are rejected;
|
|
18
|
+
- every generated document is validated with Core ``dsl_v1`` (Schema +
|
|
19
|
+
semantics) and ``dsl_coverage`` (``covers`` structural coverage) and nothing
|
|
20
|
+
is written unless every case passes.
|
|
21
|
+
|
|
22
|
+
Output bytes are deterministic (no timestamps), so re-compiling an unchanged
|
|
23
|
+
description keeps ``dsl_sha256`` and therefore an existing execution_plan
|
|
24
|
+
approval valid. The legacy TestAgent import payload lives in
|
|
25
|
+
``export_legacy_testagent.py`` and is not part of the gate.
|
|
7
26
|
"""
|
|
8
|
-
|
|
9
27
|
from __future__ import annotations
|
|
10
28
|
|
|
11
29
|
import argparse
|
|
12
30
|
import json
|
|
31
|
+
import os
|
|
13
32
|
import re
|
|
14
|
-
|
|
15
|
-
from datetime import datetime, timezone
|
|
33
|
+
import sys
|
|
16
34
|
from pathlib import Path
|
|
17
35
|
from typing import Any
|
|
18
36
|
|
|
19
|
-
# ---------------------------------------------------------------------------
|
|
20
|
-
# 硬约束常量 — 唯一权威定义
|
|
21
|
-
# ---------------------------------------------------------------------------
|
|
22
|
-
|
|
23
|
-
VALID_TARGET_KEYS = {"css", "xpath", "label", "placeholder", "role", "name", "near_text", "exact"}
|
|
24
|
-
EXACT_LOCATOR_KEYS = {"css", "xpath", "role"} # 精确匹配,必须有 source evidence
|
|
25
|
-
SEMANTIC_LOCATOR_KEYS = {"label", "placeholder", "near_text"} # 语义匹配,需检查唯一性
|
|
26
37
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
def _core_scripts() -> Path:
|
|
39
|
+
candidates = [Path(__file__).resolve().parents[4] / "devflow-core" / "skills" / "workflow-orchestrator" / "scripts"]
|
|
40
|
+
configured = os.environ.get("DEVFLOW_KIT_ROOT")
|
|
41
|
+
if configured:
|
|
42
|
+
candidates.append(Path(configured).expanduser() / "workflow-orchestrator" / "scripts")
|
|
43
|
+
for candidate in candidates:
|
|
44
|
+
if (candidate / "dsl_v1.py").is_file():
|
|
45
|
+
return candidate.resolve()
|
|
46
|
+
raise RuntimeError("Devflow Core scripts (dsl_v1.py) not found")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
sys.path.insert(0, str(_core_scripts()))
|
|
50
|
+
from dsl_coverage import check as coverage_check # noqa: E402
|
|
51
|
+
from dsl_v1 import SCHEMA_VERSION, validate as validate_dsl # noqa: E402
|
|
52
|
+
from execution_surface import derive as derive_surface # noqa: E402
|
|
53
|
+
from tc_hash import tc_sha256 # noqa: E402
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
FEATURE_RE = re.compile(r"^[^/\\\x00-\x1f\x7f]+$")
|
|
57
|
+
TC_ID_RE = re.compile(r"^TC-[0-9]{4,}$")
|
|
58
|
+
GENERATABLE_SURFACES = frozenset({"ui", "api_flow"})
|
|
59
|
+
|
|
60
|
+
UI_ACTIONS = frozenset({"goto", "click", "fill", "select", "check", "uncheck", "wait_for",
|
|
61
|
+
"assert_visible", "assert_text", "assert_url", "screenshot"})
|
|
62
|
+
TARGET_ACTIONS = UI_ACTIONS - {"goto", "assert_url", "screenshot"}
|
|
63
|
+
HTTP_METHODS = ("GET", "POST", "PUT", "PATCH", "DELETE")
|
|
64
|
+
API_SHORTHAND = {method.lower(): method for method in HTTP_METHODS}
|
|
65
|
+
|
|
66
|
+
COMMON_KEYS = {"id", "name", "action", "covers", "timeout_ms"}
|
|
67
|
+
UI_KEYS = {
|
|
68
|
+
"goto": {"url"},
|
|
69
|
+
"click": {"locator", "exact"},
|
|
70
|
+
"fill": {"locator", "exact", "value"},
|
|
71
|
+
"select": {"locator", "exact", "value"},
|
|
72
|
+
"check": {"locator", "exact"},
|
|
73
|
+
"uncheck": {"locator", "exact"},
|
|
74
|
+
"wait_for": {"locator", "exact", "state"},
|
|
75
|
+
"assert_visible": {"locator", "exact"},
|
|
76
|
+
"assert_text": {"locator", "exact", "expected", "match", "normalize_whitespace", "case_sensitive"},
|
|
77
|
+
"assert_url": {"expected", "match"},
|
|
78
|
+
"screenshot": {"full_page"},
|
|
33
79
|
}
|
|
80
|
+
API_KEYS = {"method", "url", "headers", "query", "json", "expect_status", "assertions", "extract", "retry", "idempotent"}
|
|
81
|
+
CASE_KEYS = {"tc_id", "name", "description", "variables", "data", "steps", "source_evidence", "tags"}
|
|
82
|
+
DATA_KEYS = {"namespace", "owner", "accounts", "repeatable", "setup", "cleanup"}
|
|
83
|
+
|
|
84
|
+
# Keys and values of the retired TestAgent/Midscene format. They are rejected
|
|
85
|
+
# with a pointed message instead of being silently dropped.
|
|
86
|
+
FORBIDDEN_KEYS = {
|
|
87
|
+
"strategy": "strategies (structured_first/ai_only/Midscene) do not exist in devflow-test-dsl/v1",
|
|
88
|
+
"prompt": "AI prompts are not executable in the gate; write a deterministic assert_* step",
|
|
89
|
+
"scope": "row/dialog scopes are not supported; use a data-testid or another unique locator",
|
|
90
|
+
"auth": "auth shortcuts are not supported; write explicit data.setup API steps or UI login steps",
|
|
91
|
+
"script": "scripts are not allowed in the DSL",
|
|
92
|
+
"params": "legacy params{} is not supported; put url/json/query/headers directly on the step",
|
|
93
|
+
"extract_vars": "use extract: {name: \"/json/pointer\"}",
|
|
94
|
+
"depends_on": "steps run in order; depends_on is not supported",
|
|
95
|
+
"on_failure": "a failed step always stops the case",
|
|
96
|
+
"driver": "the compiler derives driver from action",
|
|
97
|
+
"target": "write locator: \"testid:...\" (the compiler builds the target)",
|
|
98
|
+
}
|
|
99
|
+
FORBIDDEN_ACTIONS = {
|
|
100
|
+
"verify": "use assert_text / assert_visible / assert_url with a deterministic expectation",
|
|
101
|
+
"act": "ai_only actions are not allowed; use a deterministic UI action",
|
|
102
|
+
"navigate": "use goto",
|
|
103
|
+
"wait": "fixed or load waits are not allowed; use wait_for with a locator and state",
|
|
104
|
+
"inject_token": "token injection scripts are not allowed; log in via UI or data.setup API steps",
|
|
105
|
+
"evaluate_js": "JavaScript is not allowed in the DSL",
|
|
106
|
+
"hover": "hover is not in devflow-test-dsl/v1",
|
|
107
|
+
"assert_url_contains": "use assert_url with match: \"contains\"",
|
|
108
|
+
"assert_any_visible": "use assert_visible on one unique target",
|
|
109
|
+
}
|
|
110
|
+
FORBIDDEN_TEXT_RE = re.compile(r"\b(ai_only|midscene)\b", re.I)
|
|
34
111
|
|
|
112
|
+
LOCATOR_TYPES = ("testid", "role", "label", "placeholder", "text", "css")
|
|
113
|
+
REJECTED_LOCATOR_TYPES = {
|
|
114
|
+
"xpath": "XPath is not supported in devflow-test-dsl/v1; use testid/role/label/placeholder/text/css",
|
|
115
|
+
"near": "near_text is not supported; ask for a data-testid or use label/placeholder",
|
|
116
|
+
}
|
|
117
|
+
# Row-repeated action texts: with a non-testid locator these almost always
|
|
118
|
+
# match several elements, which fails under strict mode.
|
|
119
|
+
HIGH_RISK_TEXTS = {
|
|
120
|
+
"编辑", "删除", "查看", "详情", "启用", "禁用", "停用", "新增", "添加", "修改", "复制", "移动", "导出", "导入", "下载",
|
|
121
|
+
"上传", "设置", "配置", "操作", "更多", "取消", "确定", "保存", "提交", "重置", "刷新", "搜索", "查询", "筛选", "排序",
|
|
122
|
+
}
|
|
35
123
|
MIN_CSS_LENGTH = 10
|
|
36
|
-
|
|
37
|
-
DEFAULT_UI_STRATEGY = {"mode": "structured_first", "fallback": "midscene"}
|
|
38
|
-
AI_ONLY_STRATEGY = {"mode": "ai_only"}
|
|
39
|
-
|
|
40
|
-
# ---------------------------------------------------------------------------
|
|
41
|
-
# Locator 字符串解析器
|
|
42
|
-
# ---------------------------------------------------------------------------
|
|
43
|
-
|
|
44
|
-
LOCATOR_RE = re.compile(
|
|
45
|
-
r"^(?P<type>text|placeholder|css|xpath|role|near)"
|
|
46
|
-
r":(?P<value>.+)$"
|
|
47
|
-
)
|
|
48
|
-
ROLE_LOCATOR_RE = re.compile(r"^role:(?P<role>[^:]+):(?P<name>.+)$")
|
|
49
|
-
SCOPE_RE = re.compile(r"^row:(?P<row>\d+)$")
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def parse_locator(raw: str) -> dict[str, str]:
|
|
53
|
-
"""Parse 'type:value' string into flat target dict.
|
|
54
|
-
|
|
55
|
-
Returns {'label': '...'} for text:, {'placeholder': '...'} for placeholder:, etc.
|
|
56
|
-
Raises ValueError on unrecognized format.
|
|
57
|
-
"""
|
|
58
|
-
raw = raw.strip()
|
|
59
|
-
|
|
60
|
-
# role:button:确定
|
|
61
|
-
m = ROLE_LOCATOR_RE.match(raw)
|
|
62
|
-
if m:
|
|
63
|
-
return {"role": m.group("role"), "name": m.group("name")}
|
|
64
|
-
|
|
65
|
-
m = LOCATOR_RE.match(raw)
|
|
66
|
-
if not m:
|
|
67
|
-
raise ValueError(
|
|
68
|
-
f"Invalid locator format: '{raw}'. "
|
|
69
|
-
f"Expected: text:<text> | placeholder:<text> | css:<selector> | "
|
|
70
|
-
f"xpath:<expr> | role:<role>:<name> | near:<text>"
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
loc_type = m.group("type")
|
|
74
|
-
value = m.group("value").strip()
|
|
75
|
-
|
|
76
|
-
if not value:
|
|
77
|
-
raise ValueError(f"Locator '{raw}' has empty value.")
|
|
78
|
-
|
|
79
|
-
if loc_type == "text":
|
|
80
|
-
return {"label": value}
|
|
81
|
-
if loc_type == "placeholder":
|
|
82
|
-
return {"placeholder": value}
|
|
83
|
-
if loc_type == "css":
|
|
84
|
-
return {"css": value}
|
|
85
|
-
if loc_type == "xpath":
|
|
86
|
-
return {"xpath": value}
|
|
87
|
-
if loc_type == "near":
|
|
88
|
-
return {"near_text": value}
|
|
89
|
-
|
|
90
|
-
raise ValueError(f"Unknown locator type: {loc_type}")
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
# ---------------------------------------------------------------------------
|
|
94
|
-
# CSS 编造检测 — 零容忍
|
|
95
|
-
# ---------------------------------------------------------------------------
|
|
96
|
-
|
|
97
|
-
# 明显是 AI 编造的 CSS class 特征
|
|
98
124
|
HALLUCINATED_CSS_PATTERNS: list[tuple[str, str]] = [
|
|
99
|
-
|
|
100
|
-
(r"
|
|
101
|
-
# 裸的百分比值
|
|
102
|
-
(r"%", "contains '%' — not valid in CSS selector without escaping"),
|
|
103
|
-
# 纯表单容器通用类,不指定具体字段
|
|
104
|
-
(r"^\.el-form-item(__content)?$", "matches all form items, not a specific field"),
|
|
105
|
-
# 明显编造的随机 class 名模式
|
|
125
|
+
(r"%", "contains '%' (invented utility class such as w-100%)"),
|
|
126
|
+
(r"^\.el-form-item(__content)?$", "matches every form item, not a specific field"),
|
|
106
127
|
(r"\b[a-z]{1,3}-\d{3,}\b", "looks like a made-up class (random letters+numbers)"),
|
|
128
|
+
(r"^(tr|td|li|div|span|button)?:nth-(child|of-type)\(", "position-only selector is not stable"),
|
|
107
129
|
]
|
|
130
|
+
GENERIC_FORM_CLASSES = (".el-form-item", ".el-form-item__content", ".el-form-item__label", ".ant-form-item",
|
|
131
|
+
".ant-form-item-control", ".form-item", ".form-group")
|
|
108
132
|
|
|
109
|
-
# fill/select 动作在非 placeholder 情况下,CSS 禁止匹配纯容器类
|
|
110
|
-
GENERIC_FORM_CLASSES = [
|
|
111
|
-
".el-form-item",
|
|
112
|
-
".el-form-item__content",
|
|
113
|
-
".el-form-item__label",
|
|
114
|
-
".ant-form-item",
|
|
115
|
-
".ant-form-item-control",
|
|
116
|
-
".form-item",
|
|
117
|
-
".form-group",
|
|
118
|
-
]
|
|
119
133
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
"""Reject CSS selectors that show signs of AI hallucination. Hard gate."""
|
|
123
|
-
errors: list[str] = []
|
|
124
|
-
|
|
125
|
-
# Check against hallucination patterns
|
|
126
|
-
for pattern, description in HALLUCINATED_CSS_PATTERNS:
|
|
127
|
-
if re.search(pattern, css_value):
|
|
128
|
-
errors.append(
|
|
129
|
-
f"CSS selector '{css_value}' rejected: {description}. "
|
|
130
|
-
f"Step: {step_name}"
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
# Check if CSS is purely generic form classes (no specific target)
|
|
134
|
-
stripped = css_value.strip()
|
|
135
|
-
parts = [p.strip() for p in stripped.split() if p.strip()]
|
|
136
|
-
if parts and all(
|
|
137
|
-
any(p == g or p.startswith(g) for g in GENERIC_FORM_CLASSES)
|
|
138
|
-
for p in parts
|
|
139
|
-
):
|
|
140
|
-
errors.append(
|
|
141
|
-
f"CSS selector '{css_value}' only matches generic form containers, "
|
|
142
|
-
f"not a specific field. Use 'near:<label文本>' instead. "
|
|
143
|
-
f"Step: {step_name}"
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
# Too short = probably made up
|
|
147
|
-
if len(css_value) < MIN_CSS_LENGTH:
|
|
148
|
-
errors.append(
|
|
149
|
-
f"CSS selector '{css_value}' is too generic ({len(css_value)} chars, min {MIN_CSS_LENGTH}). "
|
|
150
|
-
f"Step: {step_name}"
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
if errors:
|
|
154
|
-
raise ValueError("CSS HALLUCINATION DETECTED:\n" + "\n".join(f" - {e}" for e in errors))
|
|
134
|
+
class CompileError(ValueError):
|
|
135
|
+
pass
|
|
155
136
|
|
|
156
137
|
|
|
157
138
|
# ---------------------------------------------------------------------------
|
|
158
|
-
#
|
|
139
|
+
# Suite
|
|
159
140
|
# ---------------------------------------------------------------------------
|
|
160
141
|
|
|
161
|
-
|
|
162
|
-
|
|
142
|
+
def current_feature(root: Path) -> str | None:
|
|
143
|
+
state = root / ".devflow/planning" / "STATE.md"
|
|
144
|
+
if not state.is_file():
|
|
145
|
+
return None
|
|
146
|
+
match = re.search(r"^current_feature:\s*['\"]?([^'\"\n]+)", state.read_text(encoding="utf-8"), re.MULTILINE)
|
|
147
|
+
return match.group(1).strip() if match else None
|
|
163
148
|
|
|
164
|
-
def _validate_fill_locator(step_name: str, locator_raw: str) -> None:
|
|
165
|
-
"""Hard decision chain for fill actions: placeholder → near → ai_only. CSS/xpath forbidden."""
|
|
166
|
-
if not locator_raw:
|
|
167
|
-
raise ValueError(
|
|
168
|
-
f"Fill step '{step_name}' has no locator. "
|
|
169
|
-
f"Use 'placeholder:<text>', 'near:<label文本>', or change action to 'act' with ai_only."
|
|
170
|
-
)
|
|
171
149
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
150
|
+
def load_suite(path: Path) -> dict[str, Any]:
|
|
151
|
+
try:
|
|
152
|
+
suite = json.loads(path.read_text(encoding="utf-8"))
|
|
153
|
+
except FileNotFoundError as error:
|
|
154
|
+
raise CompileError(f"frozen TC suite not found: {path}") from error
|
|
155
|
+
except json.JSONDecodeError as error:
|
|
156
|
+
raise CompileError(f"frozen TC suite is not valid JSON: {error}") from error
|
|
157
|
+
if not isinstance(suite, dict) or not isinstance(suite.get("nodes"), list):
|
|
158
|
+
raise CompileError("TC suite must be an object with nodes[]")
|
|
159
|
+
if suite.get("frozen") is not True:
|
|
160
|
+
raise CompileError("TC suite is not frozen: DSL can only be generated for a CP-3 frozen suite")
|
|
161
|
+
return suite
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def eligible_cases(suite: dict[str, Any]) -> tuple[dict[str, dict[str, Any]], dict[str, str]]:
|
|
165
|
+
"""Return ({tc_id: data} generatable, {tc_id: reason} for known but ineligible TCs)."""
|
|
166
|
+
eligible: dict[str, dict[str, Any]] = {}
|
|
167
|
+
rejected: dict[str, str] = {}
|
|
168
|
+
for node in suite["nodes"]:
|
|
169
|
+
if not isinstance(node, dict) or node.get("kind") != "test_case" or not isinstance(node.get("id"), str):
|
|
170
|
+
continue
|
|
171
|
+
tc_id, data = node["id"], node.get("data")
|
|
172
|
+
if not isinstance(data, dict):
|
|
173
|
+
rejected[tc_id] = "has no data"
|
|
174
|
+
elif node.get("status") != "confirmed":
|
|
175
|
+
rejected[tc_id] = f"status is {node.get('status')!r}, not confirmed"
|
|
176
|
+
elif data.get("automation_mode") != "automated":
|
|
177
|
+
rejected[tc_id] = f"automation_mode is {data.get('automation_mode')!r}; only automated TCs get a DSL"
|
|
178
|
+
else:
|
|
179
|
+
surface, legacy = derive_surface(data)
|
|
180
|
+
if surface not in GENERATABLE_SURFACES:
|
|
181
|
+
suffix = " (legacy_unclassified)" if legacy else ""
|
|
182
|
+
rejected[tc_id] = f"execution_surface is {surface!r}{suffix}; DSL is only generated for ui/api_flow"
|
|
183
|
+
else:
|
|
184
|
+
eligible[tc_id] = data
|
|
185
|
+
return eligible, rejected
|
|
182
186
|
|
|
183
187
|
|
|
184
188
|
# ---------------------------------------------------------------------------
|
|
185
|
-
#
|
|
189
|
+
# Locators
|
|
186
190
|
# ---------------------------------------------------------------------------
|
|
187
191
|
|
|
188
|
-
def
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
# role: → ensure name is present (already handled by parse_locator)
|
|
242
|
-
|
|
243
|
-
if errors:
|
|
244
|
-
raise ValueError("\n".join(errors))
|
|
245
|
-
|
|
192
|
+
def _css_errors(css: str) -> list[str]:
|
|
193
|
+
errors = [f"css '{css}' rejected: {reason}" for pattern, reason in HALLUCINATED_CSS_PATTERNS if re.search(pattern, css)]
|
|
194
|
+
parts = [part for part in css.split() if part]
|
|
195
|
+
if parts and all(any(part.startswith(generic) for generic in GENERIC_FORM_CLASSES) for part in parts):
|
|
196
|
+
errors.append(f"css '{css}' only matches generic form containers, not a specific field")
|
|
197
|
+
if len(css) < MIN_CSS_LENGTH:
|
|
198
|
+
errors.append(f"css '{css}' is too generic ({len(css)} chars, min {MIN_CSS_LENGTH})")
|
|
199
|
+
return errors
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def compile_target(raw: Any, exact: Any, label: str, warnings: list[str]) -> dict[str, Any]:
|
|
203
|
+
"""'type:value' → one v1 target. testid first; role/label/placeholder/text default to exact."""
|
|
204
|
+
if not isinstance(raw, str) or ":" not in raw:
|
|
205
|
+
raise CompileError(f"{label}.locator must be 'testid:<id>' | 'role:<role>:<name>' | 'label:<text>' | "
|
|
206
|
+
"'placeholder:<text>' | 'text:<text>' | 'css:<selector>'")
|
|
207
|
+
kind, _, value = raw.strip().partition(":")
|
|
208
|
+
kind = kind.strip().lower()
|
|
209
|
+
if kind in REJECTED_LOCATOR_TYPES:
|
|
210
|
+
raise CompileError(f"{label}.locator: {REJECTED_LOCATOR_TYPES[kind]}")
|
|
211
|
+
if kind not in LOCATOR_TYPES:
|
|
212
|
+
raise CompileError(f"{label}.locator type '{kind}' is unknown; use one of {', '.join(LOCATOR_TYPES)}")
|
|
213
|
+
if exact is not None and not isinstance(exact, bool):
|
|
214
|
+
raise CompileError(f"{label}.exact must be a boolean")
|
|
215
|
+
if kind == "testid":
|
|
216
|
+
if exact is not None:
|
|
217
|
+
raise CompileError(f"{label}.exact does not apply to testid locators")
|
|
218
|
+
if not value.strip():
|
|
219
|
+
raise CompileError(f"{label}.locator has an empty value")
|
|
220
|
+
return {"testid": value.strip()}
|
|
221
|
+
if kind == "css":
|
|
222
|
+
if exact is not None:
|
|
223
|
+
raise CompileError(f"{label}.exact does not apply to css locators")
|
|
224
|
+
errors = _css_errors(value.strip())
|
|
225
|
+
if errors:
|
|
226
|
+
raise CompileError(f"{label}: " + "; ".join(errors))
|
|
227
|
+
warnings.append(f"{label} uses a css locator; prefer a data-testid and cite the selector source in source-map.md")
|
|
228
|
+
return {"css": value.strip()}
|
|
229
|
+
if kind == "role":
|
|
230
|
+
role, _, name = value.partition(":")
|
|
231
|
+
if not re.fullmatch(r"[a-z]+", role.strip()) or not name.strip():
|
|
232
|
+
raise CompileError(f"{label}.locator role form is 'role:<aria-role>:<accessible name>'")
|
|
233
|
+
target: dict[str, Any] = {"role": role.strip(), "name": name.strip()}
|
|
234
|
+
text = name.strip()
|
|
235
|
+
else:
|
|
236
|
+
if not value.strip():
|
|
237
|
+
raise CompileError(f"{label}.locator has an empty value")
|
|
238
|
+
target = {kind: value.strip()}
|
|
239
|
+
text = value.strip()
|
|
240
|
+
target["exact"] = True if exact is None else exact
|
|
241
|
+
if text in HIGH_RISK_TEXTS:
|
|
242
|
+
warnings.append(f"{label} locates '{text}' by {kind}; this text usually repeats per row/dialog and fails strict mode — use a data-testid")
|
|
243
|
+
else:
|
|
244
|
+
warnings.append(f"{label} uses {kind} instead of a data-testid")
|
|
246
245
|
return target
|
|
247
246
|
|
|
248
247
|
|
|
249
248
|
# ---------------------------------------------------------------------------
|
|
250
|
-
#
|
|
249
|
+
# covers
|
|
251
250
|
# ---------------------------------------------------------------------------
|
|
252
251
|
|
|
253
|
-
|
|
252
|
+
def _indexes(value: str, label: str) -> list[int]:
|
|
253
|
+
match = re.fullmatch(r"(\d+)(?:-(\d+))?", value.strip())
|
|
254
|
+
if not match:
|
|
255
|
+
raise CompileError(f"{label}: '{value}' must be N or N-M")
|
|
256
|
+
start, end = int(match.group(1)), int(match.group(2) or match.group(1))
|
|
257
|
+
if start < 1 or end < start:
|
|
258
|
+
raise CompileError(f"{label}: '{value}' is not a valid 1-based range")
|
|
259
|
+
return list(range(start, end + 1))
|
|
254
260
|
|
|
255
261
|
|
|
256
|
-
def
|
|
257
|
-
"""
|
|
258
|
-
if
|
|
259
|
-
return
|
|
260
|
-
if
|
|
261
|
-
|
|
262
|
-
|
|
262
|
+
def compile_covers(raw: Any, label: str) -> dict[str, Any] | None:
|
|
263
|
+
"""Accept {preconditions, steps, oracle} or shorthand 'pre:1' / 'step:2-3' / 'oracle' (string or list)."""
|
|
264
|
+
if raw is None:
|
|
265
|
+
return None
|
|
266
|
+
if isinstance(raw, dict):
|
|
267
|
+
unknown = set(raw) - {"preconditions", "steps", "oracle"}
|
|
268
|
+
if unknown:
|
|
269
|
+
raise CompileError(f"{label}.covers has unknown keys {sorted(unknown)}")
|
|
270
|
+
covers: dict[str, Any] = {}
|
|
271
|
+
for key in ("preconditions", "steps"):
|
|
272
|
+
if key in raw:
|
|
273
|
+
values = raw[key]
|
|
274
|
+
if not isinstance(values, list) or not values or not all(isinstance(item, int) and not isinstance(item, bool) for item in values):
|
|
275
|
+
raise CompileError(f"{label}.covers.{key} must be a non-empty list of 1-based integers")
|
|
276
|
+
covers[key] = sorted(set(values))
|
|
277
|
+
if "oracle" in raw:
|
|
278
|
+
if raw["oracle"] is not True:
|
|
279
|
+
raise CompileError(f"{label}.covers.oracle may only be true")
|
|
280
|
+
covers["oracle"] = True
|
|
281
|
+
if not covers:
|
|
282
|
+
raise CompileError(f"{label}.covers is empty")
|
|
283
|
+
return covers
|
|
284
|
+
items = [raw] if isinstance(raw, str) else raw
|
|
285
|
+
if not isinstance(items, list) or not items or not all(isinstance(item, str) for item in items):
|
|
286
|
+
raise CompileError(f"{label}.covers must be an object, a string, or a list of strings")
|
|
287
|
+
found: dict[str, set[int]] = {"preconditions": set(), "steps": set()}
|
|
288
|
+
oracle = False
|
|
289
|
+
for item in items:
|
|
290
|
+
text = item.strip().lower()
|
|
291
|
+
if text == "oracle":
|
|
292
|
+
oracle = True
|
|
293
|
+
continue
|
|
294
|
+
prefix, _, rest = text.partition(":")
|
|
295
|
+
key = {"pre": "preconditions", "precondition": "preconditions", "preconditions": "preconditions",
|
|
296
|
+
"step": "steps", "steps": "steps"}.get(prefix)
|
|
297
|
+
if key is None or not rest:
|
|
298
|
+
raise CompileError(f"{label}.covers item '{item}' must be 'pre:N', 'step:N', 'step:N-M' or 'oracle'")
|
|
299
|
+
for part in rest.split(","):
|
|
300
|
+
found[key].update(_indexes(part, f"{label}.covers"))
|
|
301
|
+
covers = {key: sorted(values) for key, values in found.items() if values}
|
|
302
|
+
if oracle:
|
|
303
|
+
covers["oracle"] = True
|
|
304
|
+
return covers
|
|
263
305
|
|
|
264
306
|
|
|
265
307
|
# ---------------------------------------------------------------------------
|
|
266
|
-
#
|
|
308
|
+
# Steps
|
|
267
309
|
# ---------------------------------------------------------------------------
|
|
268
310
|
|
|
269
|
-
def
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
"depends_on": [],
|
|
278
|
-
"timeout_ms": int(step.get("timeout_ms", 30_000)),
|
|
279
|
-
"on_failure": step.get("on_failure", "stop"),
|
|
280
|
-
}
|
|
311
|
+
def _check_forbidden(step: dict[str, Any], label: str) -> None:
|
|
312
|
+
for key, reason in FORBIDDEN_KEYS.items():
|
|
313
|
+
if key in step:
|
|
314
|
+
raise CompileError(f"{label}.{key} is not allowed: {reason}")
|
|
315
|
+
for key, value in step.items():
|
|
316
|
+
if isinstance(value, str) and FORBIDDEN_TEXT_RE.search(value):
|
|
317
|
+
raise CompileError(f"{label}.{key} mentions ai_only/Midscene, which is not allowed in the gate DSL")
|
|
318
|
+
|
|
281
319
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
compiled["action"] = "goto"
|
|
286
|
-
compiled["params"] = {"url": step.get("url", "")}
|
|
287
|
-
return compiled
|
|
288
|
-
|
|
289
|
-
# --- Wait ---
|
|
290
|
-
if action == "wait":
|
|
291
|
-
compiled["driver"] = "web"
|
|
292
|
-
compiled["action"] = "wait"
|
|
293
|
-
compiled["params"] = {"wait_for": step.get("wait_for", "load")}
|
|
294
|
-
compiled["timeout_ms"] = int(step.get("timeout_ms", 15_000))
|
|
295
|
-
return compiled
|
|
296
|
-
|
|
297
|
-
# --- Inject token ---
|
|
298
|
-
if action == "inject_token":
|
|
299
|
-
compiled["driver"] = "web"
|
|
300
|
-
compiled["action"] = "evaluate_js"
|
|
301
|
-
compiled["params"] = {"script": step.get("script", "")}
|
|
302
|
-
return compiled
|
|
303
|
-
|
|
304
|
-
# --- API step ---
|
|
305
|
-
if action in {"get", "post", "put", "patch", "delete"}:
|
|
306
|
-
compiled["driver"] = "api"
|
|
307
|
-
compiled["action"] = action
|
|
308
|
-
compiled["params"] = deepcopy(step.get("params", {}))
|
|
309
|
-
compiled["assertions"] = _compile_api_assertions(step.get("assertions", []))
|
|
310
|
-
compiled["extract_vars"] = deepcopy(step.get("extract_vars", {}))
|
|
311
|
-
return compiled
|
|
312
|
-
|
|
313
|
-
# --- UI action steps ---
|
|
314
|
-
if action in {"click", "fill", "select", "hover"}:
|
|
315
|
-
compiled["driver"] = "ui"
|
|
316
|
-
compiled["action"] = action
|
|
317
|
-
locator_raw = step.get("locator", "")
|
|
318
|
-
if locator_raw:
|
|
319
|
-
compiled["target"] = compile_target(
|
|
320
|
-
locator_raw,
|
|
321
|
-
scope=step.get("scope"),
|
|
322
|
-
step_name=name,
|
|
323
|
-
)
|
|
324
|
-
compiled["strategy"] = compile_strategy(action)
|
|
325
|
-
if action == "fill":
|
|
326
|
-
compiled["params"] = {"value": step.get("value", "")}
|
|
327
|
-
_validate_fill_locator(name, locator_raw)
|
|
328
|
-
return compiled
|
|
329
|
-
|
|
330
|
-
# --- UI verification step ---
|
|
331
|
-
if action == "verify":
|
|
332
|
-
compiled["driver"] = "ui"
|
|
333
|
-
compiled["action"] = "act"
|
|
334
|
-
locator_raw = step.get("locator", "")
|
|
335
|
-
if locator_raw:
|
|
336
|
-
compiled["target"] = compile_target(locator_raw, scope=step.get("scope"), step_name=name)
|
|
337
|
-
compiled["strategy"] = deepcopy(AI_ONLY_STRATEGY)
|
|
338
|
-
expect = step.get("expect", "")
|
|
339
|
-
compiled["prompt"] = _build_verify_prompt(name, expect, step)
|
|
340
|
-
compiled["timeout_ms"] = int(step.get("timeout_ms", 15_000))
|
|
341
|
-
return compiled
|
|
342
|
-
|
|
343
|
-
# --- AI act step (with prompt) ---
|
|
344
|
-
if action == "act":
|
|
345
|
-
compiled["driver"] = "ui"
|
|
346
|
-
compiled["action"] = "act"
|
|
347
|
-
locator_raw = step.get("locator", "")
|
|
348
|
-
if locator_raw:
|
|
349
|
-
compiled["target"] = compile_target(locator_raw, scope=step.get("scope"), step_name=name)
|
|
350
|
-
compiled["strategy"] = deepcopy(AI_ONLY_STRATEGY)
|
|
351
|
-
compiled["prompt"] = step.get("prompt", "")
|
|
352
|
-
compiled["timeout_ms"] = int(step.get("timeout_ms", 15_000))
|
|
353
|
-
return compiled
|
|
354
|
-
|
|
355
|
-
# Fallback
|
|
356
|
-
raise ValueError(f"Unknown action '{action}' in step '{name}'")
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
def _compile_api_assertions(raw_assertions: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
360
|
-
"""Compile simplified API assertions into strict format."""
|
|
361
|
-
compiled: list[dict[str, Any]] = []
|
|
362
|
-
for a in raw_assertions:
|
|
363
|
-
atype = str(a.get("type", "status_code"))
|
|
364
|
-
compiled.append({
|
|
365
|
-
"type": atype,
|
|
366
|
-
"expected": a.get("expected"),
|
|
367
|
-
"path": a.get("path"),
|
|
368
|
-
})
|
|
320
|
+
def _base(step: dict[str, Any], default_id: str, label: str) -> dict[str, Any]:
|
|
321
|
+
step_id = step.get("id", default_id)
|
|
322
|
+
compiled: dict[str, Any] = {"id": step_id}
|
|
369
323
|
return compiled
|
|
370
324
|
|
|
371
325
|
|
|
372
|
-
def
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
return
|
|
326
|
+
def _extract(raw: Any, label: str) -> dict[str, Any]:
|
|
327
|
+
if not isinstance(raw, dict):
|
|
328
|
+
raise CompileError(f"{label}.extract must map variable names to JSON Pointers")
|
|
329
|
+
compiled: dict[str, Any] = {}
|
|
330
|
+
for name, spec in raw.items():
|
|
331
|
+
if isinstance(spec, str):
|
|
332
|
+
compiled[name] = {"pointer": spec}
|
|
333
|
+
elif isinstance(spec, dict):
|
|
334
|
+
compiled[name] = dict(spec)
|
|
335
|
+
else:
|
|
336
|
+
raise CompileError(f"{label}.extract.{name} must be a JSON Pointer string or {{pointer, sensitive}}")
|
|
337
|
+
return compiled
|
|
384
338
|
|
|
385
339
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
340
|
+
def compile_api_step(step: dict[str, Any], method: str | None, default_id: str, label: str) -> dict[str, Any]:
|
|
341
|
+
allowed = COMMON_KEYS | API_KEYS
|
|
342
|
+
unknown = set(step) - allowed
|
|
343
|
+
if unknown:
|
|
344
|
+
raise CompileError(f"{label} has unsupported keys {sorted(unknown)} for an API request (allowed: {sorted(allowed)})")
|
|
345
|
+
raw_method = method or step.get("method")
|
|
346
|
+
if not isinstance(raw_method, str) or raw_method.upper() not in HTTP_METHODS:
|
|
347
|
+
raise CompileError(f"{label}.method must be one of {', '.join(HTTP_METHODS)}")
|
|
348
|
+
if method and "method" in step and str(step["method"]).upper() != method:
|
|
349
|
+
raise CompileError(f"{label}.method conflicts with action {step.get('action')}")
|
|
350
|
+
compiled = _base(step, default_id, label)
|
|
351
|
+
compiled.update({"driver": "api", "action": "request", "method": raw_method.upper(), "url": step.get("url")})
|
|
352
|
+
for key in ("headers", "query", "json", "expect_status", "assertions", "retry", "idempotent", "timeout_ms"):
|
|
353
|
+
if key in step:
|
|
354
|
+
compiled[key] = step[key]
|
|
355
|
+
if "extract" in step:
|
|
356
|
+
compiled["extract"] = _extract(step["extract"], label)
|
|
357
|
+
covers = compile_covers(step.get("covers"), label)
|
|
358
|
+
if covers:
|
|
359
|
+
compiled["covers"] = covers
|
|
360
|
+
return compiled
|
|
389
361
|
|
|
390
|
-
def compile_auth_steps(auth: dict[str, Any] | None, base_url: str = "${base_url}") -> list[dict[str, Any]]:
|
|
391
|
-
"""Compile auth config into API login + web inject steps."""
|
|
392
|
-
if not auth:
|
|
393
|
-
return []
|
|
394
|
-
|
|
395
|
-
method = str(auth.get("method", "")).lower()
|
|
396
|
-
steps: list[dict[str, Any]] = []
|
|
397
|
-
|
|
398
|
-
if method == "api_login":
|
|
399
|
-
login_url = auth.get("login_url", f"{base_url}/auth/login_by_id")
|
|
400
|
-
login_body = auth.get("login_body", {})
|
|
401
|
-
token_path = auth.get("token_path", "headers.X-Jwt-Token")
|
|
402
|
-
app_id = auth.get("app_id")
|
|
403
|
-
|
|
404
|
-
steps.append({
|
|
405
|
-
"id": "api_auth_login",
|
|
406
|
-
"name": "获取JWT令牌",
|
|
407
|
-
"driver": "api",
|
|
408
|
-
"action": "post",
|
|
409
|
-
"params": {"url": login_url, "json": login_body},
|
|
410
|
-
"assertions": [
|
|
411
|
-
{"type": "status_code", "expected": 200},
|
|
412
|
-
{"type": "json_path_exists", "path": token_path},
|
|
413
|
-
],
|
|
414
|
-
"extract_vars": {"jwt_token": {"path": token_path, "source": "jsonpath"}},
|
|
415
|
-
"depends_on": [],
|
|
416
|
-
"timeout_ms": 15_000,
|
|
417
|
-
"on_failure": "stop",
|
|
418
|
-
})
|
|
419
|
-
|
|
420
|
-
if app_id:
|
|
421
|
-
steps.append({
|
|
422
|
-
"id": "api_app_token",
|
|
423
|
-
"name": "获取应用访问Token",
|
|
424
|
-
"driver": "api",
|
|
425
|
-
"action": "post",
|
|
426
|
-
"params": {
|
|
427
|
-
"url": auth.get("app_token_url", f"{base_url}/admin/app/access"),
|
|
428
|
-
"json": {"id": app_id},
|
|
429
|
-
"headers": {"Authorization": "Bearer ${jwt_token}"},
|
|
430
|
-
},
|
|
431
|
-
"assertions": [{"type": "status_code", "expected": 200}],
|
|
432
|
-
"extract_vars": {"token_url": {"path": "body.data.url", "source": "jsonpath"}},
|
|
433
|
-
"depends_on": ["api_auth_login"],
|
|
434
|
-
"timeout_ms": 15_000,
|
|
435
|
-
"on_failure": "stop",
|
|
436
|
-
})
|
|
437
|
-
|
|
438
|
-
steps.append({
|
|
439
|
-
"id": "web_auth_inject",
|
|
440
|
-
"name": "Token注入",
|
|
441
|
-
"driver": "web",
|
|
442
|
-
"action": "goto",
|
|
443
|
-
"params": {"url": "${token_url}"},
|
|
444
|
-
"depends_on": ["api_app_token"],
|
|
445
|
-
"timeout_ms": 30_000,
|
|
446
|
-
"on_failure": "stop",
|
|
447
|
-
})
|
|
448
|
-
|
|
449
|
-
return steps
|
|
450
362
|
|
|
363
|
+
def compile_step(step: Any, default_id: str, label: str, warnings: list[str], api_only: bool = False) -> dict[str, Any]:
|
|
364
|
+
if not isinstance(step, dict):
|
|
365
|
+
raise CompileError(f"{label} must be an object")
|
|
366
|
+
_check_forbidden(step, label)
|
|
367
|
+
action = str(step.get("action") or "").strip()
|
|
368
|
+
lowered = action.lower()
|
|
369
|
+
if lowered in FORBIDDEN_ACTIONS:
|
|
370
|
+
raise CompileError(f"{label}.action '{action}' is not allowed: {FORBIDDEN_ACTIONS[lowered]}")
|
|
371
|
+
if lowered == "request" or lowered in API_SHORTHAND:
|
|
372
|
+
return compile_api_step(step, API_SHORTHAND.get(lowered), default_id, label)
|
|
373
|
+
if api_only:
|
|
374
|
+
raise CompileError(f"{label}: data.setup/cleanup may only contain API request steps")
|
|
375
|
+
if lowered not in UI_ACTIONS:
|
|
376
|
+
raise CompileError(f"{label}.action '{action}' is unknown; UI actions: {', '.join(sorted(UI_ACTIONS))}; "
|
|
377
|
+
f"API: request or {'/'.join(API_SHORTHAND)}")
|
|
378
|
+
allowed = COMMON_KEYS | UI_KEYS[lowered]
|
|
379
|
+
unknown = set(step) - allowed
|
|
380
|
+
if unknown:
|
|
381
|
+
raise CompileError(f"{label} has unsupported keys {sorted(unknown)} for action {lowered} (allowed: {sorted(allowed)})")
|
|
382
|
+
compiled = _base(step, default_id, label)
|
|
383
|
+
compiled.update({"driver": "ui", "action": lowered})
|
|
384
|
+
if lowered in TARGET_ACTIONS:
|
|
385
|
+
if "locator" not in step:
|
|
386
|
+
raise CompileError(f"{label}.locator is required for {lowered}")
|
|
387
|
+
compiled["target"] = compile_target(step["locator"], step.get("exact"), label, warnings)
|
|
388
|
+
if lowered == "goto":
|
|
389
|
+
compiled["url"] = step.get("url")
|
|
390
|
+
elif lowered in {"fill", "select"}:
|
|
391
|
+
compiled["value"] = step.get("value")
|
|
392
|
+
elif lowered == "wait_for":
|
|
393
|
+
compiled["state"] = step.get("state", "visible")
|
|
394
|
+
elif lowered == "assert_text":
|
|
395
|
+
compiled["match"] = step.get("match", "exact")
|
|
396
|
+
compiled["expected"] = step.get("expected")
|
|
397
|
+
for key in ("normalize_whitespace", "case_sensitive"):
|
|
398
|
+
if key in step:
|
|
399
|
+
compiled[key] = step[key]
|
|
400
|
+
elif lowered == "assert_url":
|
|
401
|
+
compiled["match"] = step.get("match", "contains")
|
|
402
|
+
compiled["expected"] = step.get("expected")
|
|
403
|
+
elif lowered == "screenshot":
|
|
404
|
+
compiled["name"] = step.get("id", default_id) if isinstance(step.get("id", default_id), str) else default_id
|
|
405
|
+
if "full_page" in step:
|
|
406
|
+
compiled["full_page"] = step["full_page"]
|
|
407
|
+
if "timeout_ms" in step:
|
|
408
|
+
compiled["timeout_ms"] = step["timeout_ms"]
|
|
409
|
+
covers = compile_covers(step.get("covers"), label)
|
|
410
|
+
if covers:
|
|
411
|
+
compiled["covers"] = covers
|
|
412
|
+
return compiled
|
|
451
413
|
|
|
452
|
-
# ---------------------------------------------------------------------------
|
|
453
|
-
# Main compile entry
|
|
454
|
-
# ---------------------------------------------------------------------------
|
|
455
414
|
|
|
456
|
-
def
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
("
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
(
|
|
464
|
-
|
|
465
|
-
(
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
locator_raw = str(step.get("_locator_raw", ""))
|
|
472
|
-
for ai_term, src_term in TERM_PAIRS:
|
|
473
|
-
if ai_term in name and src_term in locator_raw:
|
|
474
|
-
warnings.append(
|
|
475
|
-
f"Step '{name}' uses '{ai_term}' but locator contains '{src_term}'. "
|
|
476
|
-
f"Verify the button text in source code — use the exact source text."
|
|
477
|
-
)
|
|
478
|
-
return warnings
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
def compile_case(case_desc: dict[str, Any]) -> dict[str, Any]:
|
|
482
|
-
"""Compile one simplified case description into strict DSL."""
|
|
483
|
-
name = str(case_desc.get("name") or "Unnamed Case")
|
|
484
|
-
tags = list(case_desc.get("tags") or ["ui-mainflow"])
|
|
485
|
-
variables = dict(case_desc.get("variables") or {})
|
|
486
|
-
base_url = variables.get("base_url", "${base_url}")
|
|
487
|
-
|
|
488
|
-
# Compile auth
|
|
489
|
-
auth_steps = compile_auth_steps(case_desc.get("auth"), base_url)
|
|
490
|
-
|
|
491
|
-
# Compile business steps
|
|
492
|
-
raw_steps = list(case_desc.get("steps") or [])
|
|
493
|
-
business_steps: list[dict[str, Any]] = []
|
|
494
|
-
for i, raw_step in enumerate(raw_steps):
|
|
495
|
-
compiled_step = compile_step(raw_step, i + 1)
|
|
496
|
-
# Preserve original locator string for terminology checking
|
|
497
|
-
compiled_step["_locator_raw"] = raw_step.get("locator", "")
|
|
498
|
-
business_steps.append(compiled_step)
|
|
499
|
-
|
|
500
|
-
# Terminology check (warning, not error)
|
|
501
|
-
term_warnings = _check_terminology(business_steps)
|
|
502
|
-
if term_warnings:
|
|
503
|
-
for w in term_warnings:
|
|
504
|
-
print(f"TERMINOLOGY WARNING: {w}", file=__import__("sys").stderr)
|
|
505
|
-
|
|
506
|
-
# Merge: auth steps first, then business steps
|
|
507
|
-
all_steps = auth_steps + business_steps
|
|
508
|
-
|
|
509
|
-
# Wire dependencies: auth steps are already wired; business steps depend on previous
|
|
510
|
-
first_business_idx = len(auth_steps)
|
|
511
|
-
for i, step in enumerate(all_steps):
|
|
512
|
-
if i >= first_business_idx and i > 0:
|
|
513
|
-
prev_id = all_steps[i - 1]["id"]
|
|
514
|
-
if not step.get("depends_on"):
|
|
515
|
-
step["depends_on"] = [prev_id]
|
|
415
|
+
def compile_variables(raw: Any, label: str) -> dict[str, Any]:
|
|
416
|
+
if raw is None:
|
|
417
|
+
return {}
|
|
418
|
+
if not isinstance(raw, dict):
|
|
419
|
+
raise CompileError(f"{label}.variables must be an object")
|
|
420
|
+
compiled: dict[str, Any] = {}
|
|
421
|
+
for name, value in raw.items():
|
|
422
|
+
if isinstance(value, dict):
|
|
423
|
+
compiled[name] = dict(value)
|
|
424
|
+
elif isinstance(value, (str, int, float, bool)):
|
|
425
|
+
compiled[name] = {"value": value}
|
|
426
|
+
else:
|
|
427
|
+
raise CompileError(f"{label}.variables.{name} must be a scalar, {{value, sensitive}} or {{secret}}")
|
|
428
|
+
return compiled
|
|
429
|
+
|
|
516
430
|
|
|
431
|
+
def compile_case(desc: Any, index: int, eligible: dict[str, dict[str, Any]], rejected: dict[str, str]) -> tuple[str, dict[str, Any], list[str]]:
|
|
432
|
+
label = f"cases[{index}]"
|
|
433
|
+
if not isinstance(desc, dict):
|
|
434
|
+
raise CompileError(f"{label} must be an object")
|
|
435
|
+
unknown = set(desc) - CASE_KEYS
|
|
436
|
+
for key in sorted(unknown):
|
|
437
|
+
raise CompileError(f"{label}.{key} is not allowed: {FORBIDDEN_KEYS.get(key, 'unknown case field')}")
|
|
438
|
+
tc_id = desc.get("tc_id")
|
|
439
|
+
if not isinstance(tc_id, str) or not TC_ID_RE.fullmatch(tc_id):
|
|
440
|
+
raise CompileError(f"{label}.tc_id must reference a frozen TC (TC-xxxx)")
|
|
441
|
+
if tc_id not in eligible:
|
|
442
|
+
reason = rejected.get(tc_id, "is not in the frozen TC suite")
|
|
443
|
+
raise CompileError(f"{label}.tc_id {tc_id} {reason}")
|
|
444
|
+
label = f"{label}({tc_id})"
|
|
445
|
+
warnings: list[str] = []
|
|
446
|
+
data_raw = desc.get("data") or {}
|
|
447
|
+
if not isinstance(data_raw, dict):
|
|
448
|
+
raise CompileError(f"{label}.data must be an object")
|
|
449
|
+
unknown_data = set(data_raw) - DATA_KEYS
|
|
450
|
+
if unknown_data:
|
|
451
|
+
raise CompileError(f"{label}.data has unsupported keys {sorted(unknown_data)}")
|
|
452
|
+
data: dict[str, Any] = {"namespace": data_raw.get("namespace", "${execution_id}")}
|
|
453
|
+
for key in ("owner", "accounts", "repeatable"):
|
|
454
|
+
if key in data_raw:
|
|
455
|
+
data[key] = data_raw[key]
|
|
456
|
+
for phase in ("setup", "cleanup"):
|
|
457
|
+
steps = data_raw.get(phase, [])
|
|
458
|
+
if not isinstance(steps, list):
|
|
459
|
+
raise CompileError(f"{label}.data.{phase} must be an array")
|
|
460
|
+
data[phase] = [compile_step(step, f"{phase}-{i:02d}", f"{label}.data.{phase}[{i - 1}]", warnings, api_only=True)
|
|
461
|
+
for i, step in enumerate(steps, start=1)]
|
|
462
|
+
raw_steps = desc.get("steps")
|
|
463
|
+
if not isinstance(raw_steps, list) or not raw_steps:
|
|
464
|
+
raise CompileError(f"{label}.steps must be a non-empty array")
|
|
465
|
+
steps = [compile_step(step, f"s{i:02d}", f"{label}.steps[{i - 1}]", warnings) for i, step in enumerate(raw_steps, start=1)]
|
|
466
|
+
source = eligible[tc_id]
|
|
467
|
+
name = desc.get("name")
|
|
517
468
|
dsl = {
|
|
518
|
-
"
|
|
519
|
-
"
|
|
520
|
-
"
|
|
521
|
-
"
|
|
522
|
-
"
|
|
469
|
+
"schema_version": SCHEMA_VERSION,
|
|
470
|
+
"tc_id": tc_id,
|
|
471
|
+
"tc_sha256": tc_sha256(source),
|
|
472
|
+
"name": name if isinstance(name, str) and name.strip() else tc_id,
|
|
473
|
+
"variables": compile_variables(desc.get("variables"), label),
|
|
474
|
+
"data": data,
|
|
475
|
+
"steps": steps,
|
|
523
476
|
}
|
|
477
|
+
errors = [f"{label}: {error}" for error in validate_dsl(dsl)]
|
|
478
|
+
if not errors:
|
|
479
|
+
errors = [f"{label}: coverage: {error}" for error in coverage_check(source, dsl)]
|
|
480
|
+
if errors:
|
|
481
|
+
raise CompileError("\n".join(errors))
|
|
482
|
+
return tc_id, dsl, [f"{label}: {warning}" for warning in warnings]
|
|
524
483
|
|
|
525
|
-
return {
|
|
526
|
-
"project_id": int(case_desc.get("project_id", 1)),
|
|
527
|
-
"name": name,
|
|
528
|
-
"description": case_desc.get("description", ""),
|
|
529
|
-
"tags": tags,
|
|
530
|
-
"dsl_json": dsl,
|
|
531
|
-
}
|
|
532
484
|
|
|
485
|
+
def compile_all(payload: Any, suite: dict[str, Any]) -> tuple[dict[str, dict[str, Any]], list[str], list[str]]:
|
|
486
|
+
"""Return ({tc_id: dsl}, warnings, errors). Errors are collected per case."""
|
|
487
|
+
cases = payload.get("cases") if isinstance(payload, dict) else payload
|
|
488
|
+
if not isinstance(cases, list):
|
|
489
|
+
raise CompileError("case descriptions must be {\"cases\": [...]} or an array")
|
|
490
|
+
eligible, rejected = eligible_cases(suite)
|
|
491
|
+
compiled: dict[str, dict[str, Any]] = {}
|
|
492
|
+
warnings: list[str] = []
|
|
493
|
+
errors: list[str] = []
|
|
494
|
+
for index, desc in enumerate(cases):
|
|
495
|
+
try:
|
|
496
|
+
tc_id, dsl, case_warnings = compile_case(desc, index, eligible, rejected)
|
|
497
|
+
except CompileError as error:
|
|
498
|
+
errors.append(str(error))
|
|
499
|
+
continue
|
|
500
|
+
if tc_id in compiled:
|
|
501
|
+
errors.append(f"cases[{index}]: duplicate case for {tc_id}; each frozen TC has exactly one DSL file")
|
|
502
|
+
continue
|
|
503
|
+
compiled[tc_id] = dsl
|
|
504
|
+
warnings.extend(case_warnings)
|
|
505
|
+
return compiled, warnings, errors
|
|
533
506
|
|
|
534
|
-
def compile_all(payload: dict[str, Any], frozen_mainflow_tc_ids: set[str] | None = None) -> dict[str, Any]:
|
|
535
|
-
"""Compile all cases in the payload."""
|
|
536
|
-
metadata = dict(payload.get("metadata") or {})
|
|
537
|
-
descriptions = list(payload.get("cases", []))
|
|
538
|
-
tc_ids: list[str] = []
|
|
539
|
-
if frozen_mainflow_tc_ids is not None:
|
|
540
|
-
for index, description in enumerate(descriptions):
|
|
541
|
-
tc_id = description.get("tc_id") if isinstance(description, dict) else None
|
|
542
|
-
if not isinstance(tc_id, str) or tc_id not in frozen_mainflow_tc_ids:
|
|
543
|
-
raise ValueError(f"cases[{index}].tc_id must reference a frozen api/page TC that is positive")
|
|
544
|
-
tc_ids.append(tc_id)
|
|
545
|
-
if len(tc_ids) != len(set(tc_ids)):
|
|
546
|
-
raise ValueError("each frozen positive api/page TC may generate at most one mainflow case")
|
|
547
|
-
cases = [compile_case(c) for c in descriptions]
|
|
548
|
-
|
|
549
|
-
return {
|
|
550
|
-
"metadata": {
|
|
551
|
-
**metadata,
|
|
552
|
-
**({"frozen_tc_ids": tc_ids} if frozen_mainflow_tc_ids is not None else {}),
|
|
553
|
-
"compiled_at": datetime.now(timezone.utc).isoformat(),
|
|
554
|
-
"compiled_by": "compile_dsl.py (hard-constraint layer)",
|
|
555
|
-
},
|
|
556
|
-
"cases": cases,
|
|
557
|
-
}
|
|
558
507
|
|
|
508
|
+
def render(dsl: dict[str, Any]) -> bytes:
|
|
509
|
+
return (json.dumps(dsl, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
|
|
559
510
|
|
|
560
|
-
def frozen_mainflow_tc_ids_from_cwd(cwd: Path) -> set[str] | None:
|
|
561
|
-
"""Return the frozen API/page suite for new workflows, else preserve legacy compilation."""
|
|
562
|
-
state_path = cwd / ".planning" / "STATE.md"
|
|
563
|
-
if not state_path.is_file():
|
|
564
|
-
return None
|
|
565
|
-
match = re.search(r"^current_feature:\s*['\"]?([^'\"\n]+)", state_path.read_text(encoding="utf-8"), re.MULTILINE)
|
|
566
|
-
if not match:
|
|
567
|
-
return None
|
|
568
|
-
suite_path = cwd / ".planning" / "requirements" / match.group(1).strip() / "tests.json"
|
|
569
|
-
if not suite_path.is_file():
|
|
570
|
-
return None
|
|
571
|
-
suite = json.loads(suite_path.read_text(encoding="utf-8"))
|
|
572
|
-
if suite.get("frozen") is not True:
|
|
573
|
-
raise ValueError("current TC suite exists but is not frozen and fresh")
|
|
574
|
-
eligible = {
|
|
575
|
-
node.get("id")
|
|
576
|
-
for node in suite.get("nodes", [])
|
|
577
|
-
if isinstance(node, dict)
|
|
578
|
-
and node.get("kind") == "test_case"
|
|
579
|
-
and node.get("status") == "confirmed"
|
|
580
|
-
and isinstance(node.get("data"), dict)
|
|
581
|
-
and node["data"].get("test_type") in {"api", "page"}
|
|
582
|
-
# A generated API TC with an error-code oracle is a negative/exception
|
|
583
|
-
# test. It belongs to run-acceptance-tests, never to the positive
|
|
584
|
-
# mainflow importer even though its broad test_type is "api".
|
|
585
|
-
and not (
|
|
586
|
-
isinstance(node["data"].get("generated_case_key"), str)
|
|
587
|
-
and isinstance(node["data"].get("execution"), dict)
|
|
588
|
-
and isinstance(node["data"]["execution"].get("expect"), dict)
|
|
589
|
-
and "error_code" in node["data"]["execution"]["expect"]
|
|
590
|
-
)
|
|
591
|
-
}
|
|
592
|
-
return {value for value in eligible if isinstance(value, str)}
|
|
593
511
|
|
|
512
|
+
def write_atomic(path: Path, content: bytes) -> None:
|
|
513
|
+
temporary = path.with_name(f".{path.name}.tmp")
|
|
514
|
+
temporary.write_bytes(content)
|
|
515
|
+
temporary.replace(path)
|
|
594
516
|
|
|
595
|
-
# ---------------------------------------------------------------------------
|
|
596
|
-
# CLI
|
|
597
|
-
# ---------------------------------------------------------------------------
|
|
598
517
|
|
|
599
518
|
def main() -> int:
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
parser.add_argument("input", help="
|
|
604
|
-
parser.add_argument("--
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
519
|
+
# allow_abbrev=False: the removed `--output` must not silently abbreviate to `--output-dir`.
|
|
520
|
+
parser = argparse.ArgumentParser(description="Compile case descriptions into one devflow-test-dsl/v1 file per frozen TC.",
|
|
521
|
+
allow_abbrev=False)
|
|
522
|
+
parser.add_argument("input", help="case-descriptions.json (see references/intermediate-format.md)")
|
|
523
|
+
parser.add_argument("--feature", help="feature name (default: current_feature in .devflow/planning/STATE.md)")
|
|
524
|
+
parser.add_argument("--suite", help="frozen TC suite (default: .devflow/planning/requirements/<feature>/tests.json)")
|
|
525
|
+
parser.add_argument("--output-dir", help="default: .devflow/planning/generated-cases/<feature>")
|
|
526
|
+
args, unknown = parser.parse_known_args()
|
|
527
|
+
if unknown:
|
|
528
|
+
removed = [item for item in unknown if item == "--output" or item.startswith("--output=")]
|
|
529
|
+
hint = (" `--output` was removed: the compiler writes one TC-xxxx.json per frozen TC into a directory; "
|
|
530
|
+
"use --output-dir (default .devflow/planning/generated-cases/<feature>)." if removed else "")
|
|
531
|
+
parser.error(f"unrecognized arguments: {' '.join(unknown)}.{hint}")
|
|
532
|
+
|
|
533
|
+
root = Path.cwd().resolve()
|
|
534
|
+
feature = args.feature or current_feature(root)
|
|
616
535
|
try:
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
536
|
+
if not feature or not FEATURE_RE.fullmatch(feature):
|
|
537
|
+
raise CompileError("feature is required (--feature or current_feature in .devflow/planning/STATE.md)")
|
|
538
|
+
suite = load_suite(Path(args.suite) if args.suite else root / ".devflow/planning" / "requirements" / feature / "tests.json")
|
|
539
|
+
try:
|
|
540
|
+
payload = json.loads(Path(args.input).read_text(encoding="utf-8"))
|
|
541
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
542
|
+
raise CompileError(f"cannot read case descriptions: {error}") from error
|
|
543
|
+
compiled, warnings, errors = compile_all(payload, suite)
|
|
544
|
+
except CompileError as error:
|
|
545
|
+
print(f"COMPILE ERROR: {error}", file=sys.stderr)
|
|
620
546
|
return 1
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
547
|
+
for warning in warnings:
|
|
548
|
+
print(f"WARNING: {warning}", file=sys.stderr)
|
|
549
|
+
if errors:
|
|
550
|
+
# All-or-nothing: never leave a partially regenerated execution plan on disk.
|
|
551
|
+
print("COMPILE ERROR: no DSL file was written\n" + "\n".join(f"- {error}" for error in errors), file=sys.stderr)
|
|
552
|
+
return 1
|
|
553
|
+
output_dir = Path(args.output_dir) if args.output_dir else root / ".devflow/planning" / "generated-cases" / feature
|
|
554
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
555
|
+
files: list[dict[str, Any]] = []
|
|
556
|
+
for tc_id in sorted(compiled):
|
|
557
|
+
path = output_dir / f"{tc_id}.json"
|
|
558
|
+
content = render(compiled[tc_id])
|
|
559
|
+
unchanged = path.is_file() and path.read_bytes() == content
|
|
560
|
+
if not unchanged:
|
|
561
|
+
write_atomic(path, content)
|
|
562
|
+
files.append({"tc_id": tc_id, "path": path.relative_to(root).as_posix() if path.is_relative_to(root) else str(path),
|
|
563
|
+
"changed": not unchanged})
|
|
564
|
+
print(json.dumps({"status": "compiled", "feature": feature, "case_count": len(files), "files": files,
|
|
565
|
+
"warnings": len(warnings)}, ensure_ascii=False, indent=2))
|
|
625
566
|
return 0
|
|
626
567
|
|
|
627
568
|
|