@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
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Pure-data validator for devflow-test-dsl/v1 (standard library only).
|
|
3
|
+
|
|
4
|
+
Structural validation interprets the shipped JSON Schema
|
|
5
|
+
(RT/devflow-test-dsl-v1.schema.json) with a small 2020-12 subset evaluator, so
|
|
6
|
+
the Python gate and the Runtime judge the shared corpus from one source.
|
|
7
|
+
Semantic rules the schema cannot express (unique ids, variable scoping, retry
|
|
8
|
+
safety, secret headers) are checked afterwards. Nothing here imports or
|
|
9
|
+
executes code from the project under test.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
from functools import lru_cache
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from tc_hash import canonicalize
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
SCHEMA_NAME = "devflow-test-dsl-v1.schema.json"
|
|
26
|
+
SCHEMA_VERSION = "devflow-test-dsl/v1"
|
|
27
|
+
BUILTIN_VARIABLES = frozenset({"execution_id", "namespace"})
|
|
28
|
+
VARIABLE_REF_RE = re.compile(r"\$\{([^}]*)\}")
|
|
29
|
+
VARIABLE_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]{0,63}$")
|
|
30
|
+
SECRET_HEADERS = frozenset({"authorization", "proxy-authorization", "cookie", "x-api-key", "x-auth-token"})
|
|
31
|
+
UI_ASSERTIONS = frozenset({"assert_visible", "assert_text", "assert_url"})
|
|
32
|
+
UI_INTERACTIONS = frozenset({"click", "fill", "select", "check", "uncheck"})
|
|
33
|
+
_IGNORED_KEYWORDS = {"$schema", "$id", "title", "description", "$defs", "default", "examples"}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def runtime_dir_candidates() -> list[Path]:
|
|
37
|
+
"""Script-relative package first, then DEVFLOW_KIT_ROOT (…/plugins/devflow-core/skills)."""
|
|
38
|
+
candidates = [Path(__file__).resolve().parents[4] / "devflow-quality" / "runtime" / "playwright"]
|
|
39
|
+
configured = os.environ.get("DEVFLOW_KIT_ROOT")
|
|
40
|
+
if configured:
|
|
41
|
+
candidates.append(Path(configured).expanduser().resolve().parents[1] / "devflow-quality" / "runtime" / "playwright")
|
|
42
|
+
return candidates
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def schema_path() -> Path | None:
|
|
46
|
+
return next((candidate / SCHEMA_NAME for candidate in runtime_dir_candidates() if (candidate / SCHEMA_NAME).is_file()), None)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@lru_cache(maxsize=4)
|
|
50
|
+
def _load_schema(path: str) -> dict[str, Any]:
|
|
51
|
+
return json.loads(Path(path).read_text(encoding="utf-8"))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def load_schema() -> dict[str, Any]:
|
|
55
|
+
path = schema_path()
|
|
56
|
+
if path is None:
|
|
57
|
+
raise FileNotFoundError(f"{SCHEMA_NAME} not found in the Devflow runtime directory")
|
|
58
|
+
return _load_schema(str(path))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _json_equal(left: Any, right: Any) -> bool:
|
|
62
|
+
try:
|
|
63
|
+
return canonicalize(left) == canonicalize(right)
|
|
64
|
+
except ValueError:
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _type_ok(value: Any, expected: str) -> bool:
|
|
69
|
+
if expected == "object":
|
|
70
|
+
return isinstance(value, dict)
|
|
71
|
+
if expected == "array":
|
|
72
|
+
return isinstance(value, list)
|
|
73
|
+
if expected == "string":
|
|
74
|
+
return isinstance(value, str)
|
|
75
|
+
if expected == "boolean":
|
|
76
|
+
return isinstance(value, bool)
|
|
77
|
+
if expected == "null":
|
|
78
|
+
return value is None
|
|
79
|
+
if expected == "integer":
|
|
80
|
+
return (isinstance(value, int) and not isinstance(value, bool)) or (isinstance(value, float) and value.is_integer())
|
|
81
|
+
if expected == "number":
|
|
82
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
83
|
+
raise ValueError(f"unsupported schema type: {expected}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class _Validator:
|
|
87
|
+
def __init__(self, root: dict[str, Any]) -> None:
|
|
88
|
+
self.root = root
|
|
89
|
+
|
|
90
|
+
def resolve(self, schema: dict[str, Any]) -> dict[str, Any]:
|
|
91
|
+
while "$ref" in schema:
|
|
92
|
+
reference = schema["$ref"]
|
|
93
|
+
if not reference.startswith("#/"):
|
|
94
|
+
raise ValueError(f"unsupported $ref: {reference}")
|
|
95
|
+
target: Any = self.root
|
|
96
|
+
for token in reference[2:].split("/"):
|
|
97
|
+
target = target[token.replace("~1", "/").replace("~0", "~")]
|
|
98
|
+
schema = target
|
|
99
|
+
return schema
|
|
100
|
+
|
|
101
|
+
def errors(self, value: Any, schema: dict[str, Any] | bool, path: str) -> list[str]:
|
|
102
|
+
if schema is True:
|
|
103
|
+
return []
|
|
104
|
+
if schema is False:
|
|
105
|
+
return [f"{path} is not allowed"]
|
|
106
|
+
schema = self.resolve(schema)
|
|
107
|
+
unknown = set(schema) - _IGNORED_KEYWORDS - {
|
|
108
|
+
"type", "const", "enum", "pattern", "minLength", "maxLength", "minimum", "maximum", "minItems", "maxItems",
|
|
109
|
+
"uniqueItems", "items", "properties", "required", "additionalProperties", "patternProperties",
|
|
110
|
+
"minProperties", "maxProperties", "oneOf"}
|
|
111
|
+
if unknown:
|
|
112
|
+
raise ValueError(f"unsupported schema keywords: {sorted(unknown)}")
|
|
113
|
+
if "oneOf" in schema:
|
|
114
|
+
return self.one_of(value, schema["oneOf"], path)
|
|
115
|
+
found: list[str] = []
|
|
116
|
+
if "type" in schema:
|
|
117
|
+
types = schema["type"] if isinstance(schema["type"], list) else [schema["type"]]
|
|
118
|
+
if not any(_type_ok(value, name) for name in types):
|
|
119
|
+
return [f"{path} must be of type {' or '.join(types)}"]
|
|
120
|
+
if "const" in schema and not _json_equal(value, schema["const"]):
|
|
121
|
+
found.append(f"{path} must equal {json.dumps(schema['const'], ensure_ascii=False)}")
|
|
122
|
+
if "enum" in schema and not any(_json_equal(value, item) for item in schema["enum"]):
|
|
123
|
+
found.append(f"{path} must be one of {', '.join(json.dumps(item, ensure_ascii=False) for item in schema['enum'])}")
|
|
124
|
+
if isinstance(value, str):
|
|
125
|
+
if "minLength" in schema and len(value) < schema["minLength"]:
|
|
126
|
+
found.append(f"{path} must be at least {schema['minLength']} characters")
|
|
127
|
+
if "maxLength" in schema and len(value) > schema["maxLength"]:
|
|
128
|
+
found.append(f"{path} must be at most {schema['maxLength']} characters")
|
|
129
|
+
if "pattern" in schema and not re.search(schema["pattern"], value):
|
|
130
|
+
found.append(f"{path} does not match pattern {schema['pattern']}")
|
|
131
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
132
|
+
if "minimum" in schema and value < schema["minimum"]:
|
|
133
|
+
found.append(f"{path} must be >= {schema['minimum']}")
|
|
134
|
+
if "maximum" in schema and value > schema["maximum"]:
|
|
135
|
+
found.append(f"{path} must be <= {schema['maximum']}")
|
|
136
|
+
if isinstance(value, list):
|
|
137
|
+
if "minItems" in schema and len(value) < schema["minItems"]:
|
|
138
|
+
found.append(f"{path} must contain at least {schema['minItems']} items")
|
|
139
|
+
if "maxItems" in schema and len(value) > schema["maxItems"]:
|
|
140
|
+
found.append(f"{path} must contain at most {schema['maxItems']} items")
|
|
141
|
+
if schema.get("uniqueItems") and len({canonicalize(item) for item in value}) != len(value):
|
|
142
|
+
found.append(f"{path} items must be unique")
|
|
143
|
+
if "items" in schema:
|
|
144
|
+
for index, item in enumerate(value):
|
|
145
|
+
found.extend(self.errors(item, schema["items"], f"{path}[{index}]"))
|
|
146
|
+
if isinstance(value, dict):
|
|
147
|
+
if "minProperties" in schema and len(value) < schema["minProperties"]:
|
|
148
|
+
found.append(f"{path} must contain at least {schema['minProperties']} properties")
|
|
149
|
+
if "maxProperties" in schema and len(value) > schema["maxProperties"]:
|
|
150
|
+
found.append(f"{path} must contain at most {schema['maxProperties']} properties")
|
|
151
|
+
for key in schema.get("required", []):
|
|
152
|
+
if key not in value:
|
|
153
|
+
found.append(f"{path}.{key} is required")
|
|
154
|
+
properties = schema.get("properties", {})
|
|
155
|
+
patterns = schema.get("patternProperties", {})
|
|
156
|
+
for key, child in value.items():
|
|
157
|
+
child_path = f"{path}.{key}"
|
|
158
|
+
matched = False
|
|
159
|
+
if key in properties:
|
|
160
|
+
matched = True
|
|
161
|
+
found.extend(self.errors(child, properties[key], child_path))
|
|
162
|
+
for pattern, child_schema in patterns.items():
|
|
163
|
+
if re.search(pattern, key):
|
|
164
|
+
matched = True
|
|
165
|
+
found.extend(self.errors(child, child_schema, child_path))
|
|
166
|
+
if not matched and "additionalProperties" in schema:
|
|
167
|
+
additional = schema["additionalProperties"]
|
|
168
|
+
if additional is False:
|
|
169
|
+
found.append(f"{child_path} is not an allowed property")
|
|
170
|
+
else:
|
|
171
|
+
found.extend(self.errors(child, additional, child_path))
|
|
172
|
+
return found
|
|
173
|
+
|
|
174
|
+
def one_of(self, value: Any, branches: list[Any], path: str) -> list[str]:
|
|
175
|
+
results = [self.errors(value, branch, path) for branch in branches]
|
|
176
|
+
passing = [index for index, errors in enumerate(results) if not errors]
|
|
177
|
+
if len(passing) == 1:
|
|
178
|
+
return []
|
|
179
|
+
if len(passing) > 1:
|
|
180
|
+
return [f"{path} matches more than one allowed form"]
|
|
181
|
+
# Report the branch selected by const discriminators (e.g. driver + action).
|
|
182
|
+
applicable: list[int] = []
|
|
183
|
+
discriminators: set[str] = set()
|
|
184
|
+
for index, branch in enumerate(branches):
|
|
185
|
+
resolved = self.resolve(branch) if isinstance(branch, dict) else {}
|
|
186
|
+
consts = {key: child["const"] for key, child in resolved.get("properties", {}).items() if isinstance(child, dict) and "const" in child}
|
|
187
|
+
discriminators.update(consts)
|
|
188
|
+
if consts and isinstance(value, dict) and all(key in value and _json_equal(value[key], expected) for key, expected in consts.items()):
|
|
189
|
+
applicable.append(index)
|
|
190
|
+
if len(applicable) == 1:
|
|
191
|
+
return results[applicable[0]]
|
|
192
|
+
if discriminators and isinstance(value, dict):
|
|
193
|
+
shown = ", ".join(f"{key}={json.dumps(value.get(key), ensure_ascii=False)}" for key in sorted(discriminators))
|
|
194
|
+
return [f"{path} has an unsupported combination ({shown})"]
|
|
195
|
+
if isinstance(value, dict) and not value:
|
|
196
|
+
return [f"{path} must not be empty"]
|
|
197
|
+
return [f"{path} must match exactly one allowed form"]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def schema_errors(document: Any) -> list[str]:
|
|
201
|
+
schema = load_schema()
|
|
202
|
+
return _Validator(schema).errors(document, schema, "$")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _strings(value: Any, path: str) -> list[tuple[str, str]]:
|
|
206
|
+
if isinstance(value, str):
|
|
207
|
+
return [(path, value)]
|
|
208
|
+
if isinstance(value, dict):
|
|
209
|
+
return [item for key, child in value.items() for item in _strings(child, f"{path}.{key}")]
|
|
210
|
+
if isinstance(value, list):
|
|
211
|
+
return [item for index, child in enumerate(value) for item in _strings(child, f"{path}[{index}]")]
|
|
212
|
+
return []
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _references(value: Any, path: str, known: set[str], errors: list[str]) -> set[str]:
|
|
216
|
+
used: set[str] = set()
|
|
217
|
+
for location, text in _strings(value, path):
|
|
218
|
+
for match in VARIABLE_REF_RE.finditer(text):
|
|
219
|
+
name = match.group(1)
|
|
220
|
+
if not VARIABLE_NAME_RE.fullmatch(name):
|
|
221
|
+
errors.append(f"{location} has an invalid variable reference ${{{name}}}")
|
|
222
|
+
elif name not in known:
|
|
223
|
+
errors.append(f"{location} references undefined variable ${{{name}}}")
|
|
224
|
+
used.add(name)
|
|
225
|
+
stripped = VARIABLE_REF_RE.sub("", text)
|
|
226
|
+
if "${" in stripped:
|
|
227
|
+
errors.append(f"{location} has an unterminated variable reference")
|
|
228
|
+
return used
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def all_steps(dsl: dict[str, Any]) -> list[tuple[str, str, dict[str, Any]]]:
|
|
232
|
+
"""(phase, json-path, step) in execution order: setup, main, cleanup."""
|
|
233
|
+
data = dsl.get("data") if isinstance(dsl.get("data"), dict) else {}
|
|
234
|
+
out: list[tuple[str, str, dict[str, Any]]] = []
|
|
235
|
+
for phase, items, base in (("setup", data.get("setup"), "$.data.setup"), ("main", dsl.get("steps"), "$.steps"), ("cleanup", data.get("cleanup"), "$.data.cleanup")):
|
|
236
|
+
for index, step in enumerate(items if isinstance(items, list) else []):
|
|
237
|
+
if isinstance(step, dict):
|
|
238
|
+
out.append((phase, f"{base}[{index}]", step))
|
|
239
|
+
return out
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def semantic_errors(dsl: dict[str, Any]) -> list[str]:
|
|
243
|
+
errors: list[str] = []
|
|
244
|
+
variables = dsl.get("variables", {})
|
|
245
|
+
sensitive: set[str] = set()
|
|
246
|
+
for name, spec in variables.items():
|
|
247
|
+
if name in BUILTIN_VARIABLES:
|
|
248
|
+
errors.append(f"$.variables.{name} redefines a built-in variable")
|
|
249
|
+
if "secret" in spec or spec.get("sensitive") is True:
|
|
250
|
+
sensitive.add(name)
|
|
251
|
+
known = set(variables) | {"execution_id"}
|
|
252
|
+
_references(dsl["data"]["namespace"], "$.data.namespace", known, errors)
|
|
253
|
+
known.add("namespace")
|
|
254
|
+
seen_ids: dict[str, str] = {}
|
|
255
|
+
for _phase, path, step in all_steps(dsl):
|
|
256
|
+
step_id = step["id"]
|
|
257
|
+
if step_id in seen_ids:
|
|
258
|
+
errors.append(f"{path}.id duplicates step id '{step_id}' ({seen_ids[step_id]})")
|
|
259
|
+
seen_ids.setdefault(step_id, path)
|
|
260
|
+
payload = {key: value for key, value in step.items() if key not in {"id", "driver", "action", "covers", "extract", "assertions", "retry"}}
|
|
261
|
+
_references(payload, path, known, errors)
|
|
262
|
+
if step["driver"] != "api":
|
|
263
|
+
continue
|
|
264
|
+
for index, assertion in enumerate(step.get("assertions", [])):
|
|
265
|
+
_references(assertion.get("value"), f"{path}.assertions[{index}].value", known, errors)
|
|
266
|
+
has_value = "value" in assertion
|
|
267
|
+
if assertion["op"] in {"exists", "not_exists"} and has_value:
|
|
268
|
+
errors.append(f"{path}.assertions[{index}] op {assertion['op']} must not have a value")
|
|
269
|
+
if assertion["op"] not in {"exists", "not_exists"} and not has_value:
|
|
270
|
+
errors.append(f"{path}.assertions[{index}] op {assertion['op']} requires a value")
|
|
271
|
+
if "retry" in step and step["method"] in {"POST", "PATCH"} and step.get("idempotent") is not True:
|
|
272
|
+
errors.append(f"{path}.retry on {step['method']} requires idempotent=true")
|
|
273
|
+
for header, value in step.get("headers", {}).items():
|
|
274
|
+
if header.lower() in SECRET_HEADERS:
|
|
275
|
+
refs = {match.group(1) for match in VARIABLE_REF_RE.finditer(value)}
|
|
276
|
+
if not refs or not refs <= sensitive:
|
|
277
|
+
errors.append(f"{path}.headers.{header} must be built only from secret or sensitive variables")
|
|
278
|
+
for name, spec in step.get("extract", {}).items():
|
|
279
|
+
if name in known:
|
|
280
|
+
errors.append(f"{path}.extract.{name} redefines an existing variable")
|
|
281
|
+
known.add(name)
|
|
282
|
+
if spec.get("sensitive") is True:
|
|
283
|
+
sensitive.add(name)
|
|
284
|
+
return errors
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def validate(dsl: Any) -> list[str]:
|
|
288
|
+
try:
|
|
289
|
+
errors = schema_errors(dsl)
|
|
290
|
+
except (OSError, ValueError, KeyError) as error:
|
|
291
|
+
return [f"DSL v1 schema unavailable: {error}"]
|
|
292
|
+
return errors or semantic_errors(dsl)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def load_file(path: Path) -> tuple[Any, list[str]]:
|
|
296
|
+
try:
|
|
297
|
+
return json.loads(path.read_text(encoding="utf-8")), []
|
|
298
|
+
except FileNotFoundError:
|
|
299
|
+
return None, [f"DSL file is missing: {path.as_posix()}"]
|
|
300
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
301
|
+
return None, [f"DSL file is not valid JSON: {error}"]
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def main() -> int:
|
|
305
|
+
parser = argparse.ArgumentParser(description="Validate devflow-test-dsl/v1 files.")
|
|
306
|
+
parser.add_argument("paths", nargs="+")
|
|
307
|
+
args = parser.parse_args()
|
|
308
|
+
report: dict[str, Any] = {}
|
|
309
|
+
for raw in args.paths:
|
|
310
|
+
document, errors = load_file(Path(raw))
|
|
311
|
+
report[raw] = errors or validate(document)
|
|
312
|
+
ok = not any(report.values())
|
|
313
|
+
print(json.dumps({"ok": ok, "errors": report}, ensure_ascii=False, indent=2))
|
|
314
|
+
return 0 if ok else 1
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
if __name__ == "__main__":
|
|
318
|
+
sys.exit(main())
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Load and validate .devflow/planning/test-environments/<profile>.json.
|
|
3
|
+
|
|
4
|
+
The profile hash is the SHA-256 of the raw file bytes; only an
|
|
5
|
+
environment_profile approval pinned to that hash makes a profile usable.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
from urllib.parse import urlsplit
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
PROFILE_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$")
|
|
19
|
+
SECRET_KEY_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]{0,63}$")
|
|
20
|
+
ENV_NAME_RE = re.compile(r"^[A-Z_][A-Z0-9_]{0,127}$")
|
|
21
|
+
ORIGIN_RE = re.compile(r"^https?://(\[[0-9A-Fa-f:.]+\]|[A-Za-z0-9](?:[A-Za-z0-9.-]{0,251}[A-Za-z0-9])?)(:[0-9]{1,5})?$")
|
|
22
|
+
RESERVED_ENV = frozenset({
|
|
23
|
+
"DEVFLOW_DSL_FILE", "DEVFLOW_DSL_SHA256", "DEVFLOW_TC_ID", "DEVFLOW_TC_SHA256", "DEVFLOW_EXECUTION_ID",
|
|
24
|
+
"DEVFLOW_EXECUTION_NONCE", "DEVFLOW_RESULT_FILE", "DEVFLOW_ARTIFACT_DIR", "DEVFLOW_ENVIRONMENT_FILE",
|
|
25
|
+
"PATH", "HOME", "TMPDIR", "LANG", "PLAYWRIGHT_BROWSERS_PATH", "SHELL", "USER", "PWD",
|
|
26
|
+
})
|
|
27
|
+
RESERVED_ENV_PREFIXES = ("NODE_", "LD_", "DYLD_", "PLAYWRIGHT_", "PYTHON", "BUN_", "NPM_", "DEVFLOW_HOME", "DEVFLOW_KIT", "DEVFLOW_SOFT")
|
|
28
|
+
SUBRESOURCE_POLICIES = ("same-site-plus-declared", "declared-only")
|
|
29
|
+
ARTIFACT_MODES = ("on_failure", "off")
|
|
30
|
+
TOP_LEVEL = {"profile", "base_url", "api_base_url", "navigation_origins", "api_origins", "subresources", "secret_env", "trace", "screenshots"}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def profile_path(root: Path, name: str) -> Path:
|
|
34
|
+
return root / ".devflow/planning" / "test-environments" / f"{name}.json"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def origin_of(url: str) -> str | None:
|
|
38
|
+
parts = urlsplit(url)
|
|
39
|
+
if parts.scheme not in {"http", "https"} or not parts.hostname or parts.username or parts.password:
|
|
40
|
+
return None
|
|
41
|
+
origin = f"{parts.scheme}://{parts.netloc}"
|
|
42
|
+
return origin if ORIGIN_RE.fullmatch(origin) else None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _origins(value: Any, label: str, errors: list[str], required: bool) -> list[str]:
|
|
46
|
+
if not isinstance(value, list) or (required and not value):
|
|
47
|
+
errors.append(f"{label} must be a {'non-empty ' if required else ''}array of origins")
|
|
48
|
+
return []
|
|
49
|
+
origins: list[str] = []
|
|
50
|
+
for index, item in enumerate(value):
|
|
51
|
+
if not isinstance(item, str) or not ORIGIN_RE.fullmatch(item):
|
|
52
|
+
errors.append(f"{label}[{index}] must be an exact http(s) origin without path, wildcard, or credentials")
|
|
53
|
+
elif item in origins:
|
|
54
|
+
errors.append(f"{label}[{index}] duplicates {item}")
|
|
55
|
+
else:
|
|
56
|
+
origins.append(item)
|
|
57
|
+
return origins
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def validate_profile(profile: Any, name: str) -> list[str]:
|
|
61
|
+
if not isinstance(profile, dict):
|
|
62
|
+
return ["environment profile root must be an object"]
|
|
63
|
+
errors = [f"environment profile field is not allowed: {key}" for key in sorted(set(profile) - TOP_LEVEL)]
|
|
64
|
+
if profile.get("profile") != name:
|
|
65
|
+
errors.append("environment profile 'profile' must equal the profile file name")
|
|
66
|
+
navigation = _origins(profile.get("navigation_origins"), "navigation_origins", errors, required=True)
|
|
67
|
+
api = _origins(profile.get("api_origins", []), "api_origins", errors, required=False)
|
|
68
|
+
for key, allowed in (("base_url", navigation), ("api_base_url", api)):
|
|
69
|
+
if key not in profile and key == "api_base_url":
|
|
70
|
+
continue
|
|
71
|
+
value = profile.get(key)
|
|
72
|
+
origin = origin_of(value) if isinstance(value, str) else None
|
|
73
|
+
if origin is None:
|
|
74
|
+
errors.append(f"{key} must be an absolute http(s) URL")
|
|
75
|
+
elif origin not in allowed:
|
|
76
|
+
errors.append(f"{key} origin {origin} must be listed in {'navigation_origins' if key == 'base_url' else 'api_origins'}")
|
|
77
|
+
subresources = profile.get("subresources", {"policy": "same-site-plus-declared", "allowed_origins": []})
|
|
78
|
+
if not isinstance(subresources, dict) or set(subresources) - {"policy", "allowed_origins"}:
|
|
79
|
+
errors.append("subresources must be an object with policy and allowed_origins")
|
|
80
|
+
else:
|
|
81
|
+
if subresources.get("policy") not in SUBRESOURCE_POLICIES:
|
|
82
|
+
errors.append(f"subresources.policy must be one of {', '.join(SUBRESOURCE_POLICIES)}")
|
|
83
|
+
_origins(subresources.get("allowed_origins", []), "subresources.allowed_origins", errors, required=False)
|
|
84
|
+
secret_env = profile.get("secret_env", {})
|
|
85
|
+
if not isinstance(secret_env, dict):
|
|
86
|
+
errors.append("secret_env must be an object mapping secret keys to environment variable names")
|
|
87
|
+
else:
|
|
88
|
+
for key, env_name in secret_env.items():
|
|
89
|
+
if not SECRET_KEY_RE.fullmatch(key):
|
|
90
|
+
errors.append(f"secret_env key is invalid: {key}")
|
|
91
|
+
if not isinstance(env_name, str) or not ENV_NAME_RE.fullmatch(env_name):
|
|
92
|
+
errors.append(f"secret_env.{key} must name an upper-case environment variable")
|
|
93
|
+
elif env_name in RESERVED_ENV or env_name.startswith(RESERVED_ENV_PREFIXES):
|
|
94
|
+
errors.append(f"secret_env.{key} uses a reserved environment variable: {env_name}")
|
|
95
|
+
for key in ("trace", "screenshots"):
|
|
96
|
+
if key in profile and profile[key] not in ARTIFACT_MODES:
|
|
97
|
+
errors.append(f"{key} must be one of {', '.join(ARTIFACT_MODES)}")
|
|
98
|
+
return errors
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def load(root: Path, name: str) -> tuple[dict[str, Any] | None, str | None, list[str]]:
|
|
102
|
+
"""Return (profile, sha256 of raw bytes, errors)."""
|
|
103
|
+
if not isinstance(name, str) or not PROFILE_NAME_RE.fullmatch(name):
|
|
104
|
+
return None, None, [f"invalid environment profile name: {name!r}"]
|
|
105
|
+
path = profile_path(root, name)
|
|
106
|
+
try:
|
|
107
|
+
if path.is_symlink():
|
|
108
|
+
return None, None, [f"environment profile must not be a symlink: {path.relative_to(root).as_posix()}"]
|
|
109
|
+
raw = path.read_bytes()
|
|
110
|
+
except FileNotFoundError:
|
|
111
|
+
return None, None, [f"environment profile is missing: {path.relative_to(root).as_posix()}"]
|
|
112
|
+
except OSError as error:
|
|
113
|
+
return None, None, [f"environment profile is unreadable: {error}"]
|
|
114
|
+
sha256 = hashlib.sha256(raw).hexdigest()
|
|
115
|
+
try:
|
|
116
|
+
profile = json.loads(raw.decode("utf-8"))
|
|
117
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
118
|
+
return None, sha256, [f"environment profile is not valid JSON: {error}"]
|
|
119
|
+
errors = validate_profile(profile, name)
|
|
120
|
+
return (profile if not errors else None), sha256, errors
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def main() -> int:
|
|
124
|
+
parser = argparse.ArgumentParser()
|
|
125
|
+
parser.add_argument("name")
|
|
126
|
+
parser.add_argument("--json", action="store_true")
|
|
127
|
+
args = parser.parse_args()
|
|
128
|
+
_profile, sha256, errors = load(Path.cwd().resolve(), args.name)
|
|
129
|
+
print(json.dumps({"ok": not errors, "profile": args.name, "sha256": sha256, "errors": errors}, ensure_ascii=False, indent=2))
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
raise SystemExit(main())
|
|
@@ -43,7 +43,7 @@ OptionParser.new do |o|
|
|
|
43
43
|
end.parse!
|
|
44
44
|
|
|
45
45
|
def load_state(root)
|
|
46
|
-
document = File.read(File.join(root, ".planning/STATE.md"))
|
|
46
|
+
document = File.read(File.join(root, ".devflow/planning/STATE.md"))
|
|
47
47
|
front = document.match(/\A---\n(.*?)\n---/m)&.[](1)
|
|
48
48
|
raise "STATE.md has no YAML front matter" unless front
|
|
49
49
|
YAML.safe_load(front, permitted_classes: [], aliases: false)
|
|
@@ -57,7 +57,7 @@ def render_execution_contract_markdown(contract)
|
|
|
57
57
|
lines = [
|
|
58
58
|
"# Execution Contract",
|
|
59
59
|
"",
|
|
60
|
-
"> Core-generated derived view. The immutable contract JSON under `.planning/contracts/` is authoritative.",
|
|
60
|
+
"> Core-generated derived view. The immutable contract JSON under `.devflow/planning/contracts/` is authoritative.",
|
|
61
61
|
"",
|
|
62
62
|
"- Contract ID: `#{contract.fetch("contract_id")}`",
|
|
63
63
|
"- Feature: `#{contract.fetch("feature")}`",
|
|
@@ -106,7 +106,7 @@ begin
|
|
|
106
106
|
# omitting production_paths strands implementation workers in a contract
|
|
107
107
|
# that can write summaries but never code.
|
|
108
108
|
if options[:step] == "execute"
|
|
109
|
-
binding_path = File.join(root, ".planning/features", options[:feature], "test-bindings.json")
|
|
109
|
+
binding_path = File.join(root, ".devflow/planning/features", options[:feature], "test-bindings.json")
|
|
110
110
|
if File.file?(binding_path)
|
|
111
111
|
bindings = JSON.parse(File.read(binding_path))
|
|
112
112
|
Array(bindings["tasks"]).each do |task|
|
|
@@ -130,6 +130,7 @@ begin
|
|
|
130
130
|
devflow_api_test_matrix devflow_tdd_evidence devflow_run_acceptance_tests devflow_mainflow
|
|
131
131
|
devflow_requirement_graph devflow_static_runtime_context
|
|
132
132
|
devflow_architecture_constraint devflow_architecture_feasibility devflow_architecture_brief
|
|
133
|
+
devflow_approval
|
|
133
134
|
],
|
|
134
135
|
"tool_budget" => policy["budget"], "hard_gate_mode" => (workflow.dig("governance", "hard_gate_mode").to_s.empty? ? "chain" : workflow.dig("governance", "hard_gate_mode").to_s), "acceptance" => Array(graph["nodes"]).select { |node| node["kind"] == "acceptance" }.map { |node| node["id"] },
|
|
135
136
|
"allowed_graph_delta" => Array(policy["allowed_graph_delta"]),
|
|
@@ -142,10 +143,10 @@ begin
|
|
|
142
143
|
result = { "ok" => true, "contract" => frozen }
|
|
143
144
|
else
|
|
144
145
|
raise ArgumentError, "--write is required" unless options[:write]
|
|
145
|
-
destination = File.join(root, ".planning/contracts", options[:feature], "#{contract_id}.json")
|
|
146
|
+
destination = File.join(root, ".devflow/planning/contracts", options[:feature], "#{contract_id}.json")
|
|
146
147
|
DevflowControlIO.atomic_write(destination, JSON.pretty_generate(frozen) + "\n")
|
|
147
|
-
DevflowControlIO.atomic_write(File.join(root, ".planning/execution-contract.md"), render_execution_contract_markdown(frozen) + "\n")
|
|
148
|
-
audit = File.join(root, ".planning/contracts", options[:feature], "control-events.jsonl")
|
|
148
|
+
DevflowControlIO.atomic_write(File.join(root, ".devflow/planning/execution-contract.md"), render_execution_contract_markdown(frozen) + "\n")
|
|
149
|
+
audit = File.join(root, ".devflow/planning/contracts", options[:feature], "control-events.jsonl")
|
|
149
150
|
existing = File.exist?(audit) ? File.read(audit) : ""
|
|
150
151
|
DevflowControlIO.atomic_write(audit, existing + JSON.generate({ "event" => "execution_contract_frozen", "contract_id" => contract_id, "state_revision" => state["revision"], "graph_revision" => graph["revision"], "frozen_hash" => frozen["frozen_hash"] }) + "\n")
|
|
151
152
|
result = { "ok" => true, "path" => destination.sub("#{root}/", ""), "contract" => frozen }
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Single implementation of frozen-TC execution_surface derivation and consistency rules.
|
|
3
|
+
|
|
4
|
+
execution_surface is a top-level field of the frozen TC node data (http | ui |
|
|
5
|
+
api_flow | project_test). Legacy TCs without the field are derived
|
|
6
|
+
conservatively; only the project_test fallback is marked legacy_unclassified.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
SURFACES = ("http", "ui", "api_flow", "project_test")
|
|
18
|
+
FEATURE_RE = re.compile(r"^[^/\\\x00-\x1f\x7f]+$")
|
|
19
|
+
# Allowed binding kinds per surface (方案 §4.3). manual/blocked are modes, not adapters.
|
|
20
|
+
ADAPTERS_BY_SURFACE = {
|
|
21
|
+
"http": {"http"},
|
|
22
|
+
"ui": {"devflow_playwright"},
|
|
23
|
+
"api_flow": {"devflow_playwright"},
|
|
24
|
+
"project_test": {"command"},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def http_execution_error(source: dict[str, Any]) -> str | None:
|
|
29
|
+
execution = source.get("execution")
|
|
30
|
+
if not isinstance(execution, dict) or execution.get("protocol") != "http":
|
|
31
|
+
return "frozen TC has no HTTP execution definition"
|
|
32
|
+
request = execution.get("request")
|
|
33
|
+
expect = execution.get("expect")
|
|
34
|
+
if not isinstance(request, dict) or not isinstance(expect, dict):
|
|
35
|
+
return "frozen TC HTTP execution requires request and expect"
|
|
36
|
+
if not isinstance(request.get("method"), str) or not re.fullmatch(r"[A-Z]+", request["method"]):
|
|
37
|
+
return "frozen TC HTTP request method is invalid"
|
|
38
|
+
if not isinstance(request.get("path"), str) or not request["path"].startswith("/"):
|
|
39
|
+
return "frozen TC HTTP request path is invalid"
|
|
40
|
+
if not isinstance(expect.get("http_status"), int) or isinstance(expect.get("http_status"), bool) or not 100 <= expect["http_status"] <= 599:
|
|
41
|
+
return "frozen TC HTTP expectation http_status is invalid"
|
|
42
|
+
for key in ("path_params", "query", "headers", "body"):
|
|
43
|
+
if key in request and not isinstance(request[key], dict):
|
|
44
|
+
return f"frozen TC HTTP request {key} must be an object"
|
|
45
|
+
if "error_code" in expect and (not isinstance(expect["error_code"], str) or not expect["error_code"].strip()):
|
|
46
|
+
return "frozen TC HTTP expectation error_code is invalid"
|
|
47
|
+
if "error_code_pointer" in expect and (not isinstance(expect["error_code_pointer"], str) or not expect["error_code_pointer"].startswith("/")):
|
|
48
|
+
return "frozen TC HTTP expectation error_code_pointer is invalid"
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _declares_http(data: dict[str, Any]) -> bool:
|
|
53
|
+
execution = data.get("execution")
|
|
54
|
+
return isinstance(execution, dict) and execution.get("protocol") == "http"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def derive(data: dict[str, Any]) -> tuple[str | None, bool]:
|
|
58
|
+
"""Return (surface, legacy_unclassified); surface is None for an invalid explicit value."""
|
|
59
|
+
if "execution_surface" in data:
|
|
60
|
+
value = data.get("execution_surface")
|
|
61
|
+
return (value, False) if value in SURFACES else (None, False)
|
|
62
|
+
if _declares_http(data) and http_execution_error(data) is None:
|
|
63
|
+
return "http", False
|
|
64
|
+
if data.get("test_type") == "page":
|
|
65
|
+
return "ui", False
|
|
66
|
+
return "project_test", True
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def consistency_errors(data: dict[str, Any], require_explicit: bool = False) -> list[str]:
|
|
70
|
+
errors: list[str] = []
|
|
71
|
+
explicit = "execution_surface" in data
|
|
72
|
+
if require_explicit and not explicit:
|
|
73
|
+
return ["execution_surface is required for a newly frozen TC"]
|
|
74
|
+
surface, _legacy = derive(data)
|
|
75
|
+
if surface is None:
|
|
76
|
+
return [f"execution_surface must be one of {', '.join(SURFACES)}"]
|
|
77
|
+
if _declares_http(data) and surface != "http":
|
|
78
|
+
errors.append("TC with execution.protocol=http must have execution_surface=http")
|
|
79
|
+
if surface == "http":
|
|
80
|
+
http_error = http_execution_error(data)
|
|
81
|
+
if http_error:
|
|
82
|
+
errors.append(f"execution_surface=http requires a valid execution.protocol=http definition ({http_error})")
|
|
83
|
+
if data.get("test_type") == "page":
|
|
84
|
+
if surface != "ui":
|
|
85
|
+
errors.append("test_type=page requires execution_surface=ui")
|
|
86
|
+
if _declares_http(data):
|
|
87
|
+
errors.append("test_type=page must not declare execution.protocol=http")
|
|
88
|
+
if surface in {"ui", "api_flow"} and "execution" in data:
|
|
89
|
+
errors.append(f"execution_surface={surface} must not carry an execution object")
|
|
90
|
+
return errors
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _test_cases(suite: dict[str, Any]) -> list[tuple[str, dict[str, Any]]]:
|
|
94
|
+
cases: list[tuple[str, dict[str, Any]]] = []
|
|
95
|
+
for node in suite.get("nodes", []) if isinstance(suite.get("nodes"), list) else []:
|
|
96
|
+
if isinstance(node, dict) and node.get("kind") == "test_case":
|
|
97
|
+
cases.append((str(node.get("id")), node.get("data") if isinstance(node.get("data"), dict) else {}))
|
|
98
|
+
return cases
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def check_suite(suite: dict[str, Any], require_explicit: bool = True) -> list[str]:
|
|
102
|
+
"""Consistency errors for every TC; a new freeze requires the explicit field."""
|
|
103
|
+
errors: list[str] = []
|
|
104
|
+
for tc_id, data in _test_cases(suite):
|
|
105
|
+
errors.extend(f"{tc_id}: {error}" for error in consistency_errors(data, require_explicit=require_explicit))
|
|
106
|
+
return errors
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def main() -> int:
|
|
110
|
+
parser = argparse.ArgumentParser()
|
|
111
|
+
parser.add_argument("action", choices=("check-suite",))
|
|
112
|
+
parser.add_argument("--feature", required=True)
|
|
113
|
+
parser.add_argument("--allow-legacy", action="store_true", help="derive missing fields instead of requiring them")
|
|
114
|
+
parser.add_argument("--json", action="store_true")
|
|
115
|
+
args = parser.parse_args()
|
|
116
|
+
root = Path.cwd().resolve()
|
|
117
|
+
if not FEATURE_RE.fullmatch(args.feature):
|
|
118
|
+
result: dict[str, Any] = {"ok": False, "reason": "execution_surface_mismatch", "errors": ["invalid feature"]}
|
|
119
|
+
else:
|
|
120
|
+
path = root / ".devflow/planning" / "requirements" / args.feature / "tests.json"
|
|
121
|
+
try:
|
|
122
|
+
suite = json.loads(path.read_text(encoding="utf-8"))
|
|
123
|
+
if not isinstance(suite, dict):
|
|
124
|
+
raise ValueError("TC suite root must be an object")
|
|
125
|
+
errors = check_suite(suite, require_explicit=not args.allow_legacy)
|
|
126
|
+
if not _test_cases(suite):
|
|
127
|
+
errors.append("TC suite contains no test_case nodes")
|
|
128
|
+
except (OSError, ValueError) as error:
|
|
129
|
+
errors = [f"cannot read TC suite: {error}"]
|
|
130
|
+
result = {"ok": not errors, "feature": args.feature, "errors": errors}
|
|
131
|
+
if errors:
|
|
132
|
+
result["reason"] = "execution_surface_mismatch"
|
|
133
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
134
|
+
return 0
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
raise SystemExit(main())
|
|
@@ -87,7 +87,7 @@ module GateRuntime
|
|
|
87
87
|
invalid_outputs = validate_declared_inputs(contract.dig("outputs", "required_inputs"), root, workflow_state, purpose: :completion)
|
|
88
88
|
return GateResult.block(**base, code: "REQUIRED_OUTPUT_INVALID", message: "required step input/output fact is invalid", details: { "invalid" => invalid_outputs }) unless invalid_outputs.empty?
|
|
89
89
|
if step_id == "code-review"
|
|
90
|
-
contract_view = File.join(root, ".planning/execution-contract.md")
|
|
90
|
+
contract_view = File.join(root, ".devflow/planning/execution-contract.md")
|
|
91
91
|
contract_id = if File.file?(contract_view) && !File.symlink?(contract_view)
|
|
92
92
|
File.read(contract_view).match(/^- Contract ID: `(EC-[0-9a-f]{16})`$/)&.[](1)
|
|
93
93
|
end
|
|
@@ -234,9 +234,9 @@ module GateRuntime
|
|
|
234
234
|
case validation
|
|
235
235
|
when "static-runtime-context"
|
|
236
236
|
pointer_path = payload.fetch("path")
|
|
237
|
-
raise "invalid pointer path" unless pointer_path.start_with?(".context/static-runtime-context/revisions/")
|
|
237
|
+
raise "invalid pointer path" unless pointer_path.start_with?(".devflow/context/static-runtime-context/revisions/")
|
|
238
238
|
root_real = File.realpath(root)
|
|
239
|
-
revisions_root = File.realpath(File.join(root, ".context/static-runtime-context/revisions"))
|
|
239
|
+
revisions_root = File.realpath(File.join(root, ".devflow/context/static-runtime-context/revisions"))
|
|
240
240
|
snapshot_real = File.realpath(File.expand_path(pointer_path, root))
|
|
241
241
|
raise "snapshot path escapes project root" unless snapshot_real.start_with?("#{root_real}#{File::SEPARATOR}")
|
|
242
242
|
raise "snapshot path escapes revisions directory" unless snapshot_real.start_with?("#{revisions_root}#{File::SEPARATOR}")
|