swipium 1.4.0 → 1.5.0
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/CHANGELOG.md +142 -0
- package/README.md +88 -25
- package/THREAT_MODEL.md +1 -1
- package/dist/appMap/automationLink.js +13 -3
- package/dist/appMap/automationLink.js.map +1 -1
- package/dist/appMap/build.js +28 -6
- package/dist/appMap/build.js.map +1 -1
- package/dist/appMap/codeIndex.js +29 -3
- package/dist/appMap/codeIndex.js.map +1 -1
- package/dist/appMap/featureIndex.js +20 -2
- package/dist/appMap/featureIndex.js.map +1 -1
- package/dist/appMap/featureModel.js +42 -6
- package/dist/appMap/featureModel.js.map +1 -1
- package/dist/appMap/firstRunApply.js +22 -4
- package/dist/appMap/firstRunApply.js.map +1 -1
- package/dist/appMap/fsWalk.js +19 -2
- package/dist/appMap/fsWalk.js.map +1 -1
- package/dist/appMap/issues.js +0 -20
- package/dist/appMap/issues.js.map +1 -1
- package/dist/appMap/migrations.js +1 -1
- package/dist/appMap/migrations.js.map +1 -1
- package/dist/appMap/prelaunch.js.map +1 -1
- package/dist/appMap/projectRegistry.js +8 -2
- package/dist/appMap/projectRegistry.js.map +1 -1
- package/dist/appMap/query.js +20 -4
- package/dist/appMap/query.js.map +1 -1
- package/dist/appMap/runtimeMerge.js +9 -2
- package/dist/appMap/runtimeMerge.js.map +1 -1
- package/dist/appMap/schema.js.map +1 -1
- package/dist/appMap/screenMatch.js +5 -1
- package/dist/appMap/screenMatch.js.map +1 -1
- package/dist/appMap/staticScan.js +134 -22
- package/dist/appMap/staticScan.js.map +1 -1
- package/dist/appMap/store.js +4 -79
- package/dist/appMap/store.js.map +1 -1
- package/dist/appMap/tsAstScan.js +31 -9
- package/dist/appMap/tsAstScan.js.map +1 -1
- package/dist/artifacts/bundletool.js +44 -7
- package/dist/artifacts/bundletool.js.map +1 -1
- package/dist/artifacts/resolve.js +33 -6
- package/dist/artifacts/resolve.js.map +1 -1
- package/dist/automation/capabilities.js +109 -27
- package/dist/automation/capabilities.js.map +1 -1
- package/dist/automation/gestures.js +5 -1
- package/dist/automation/gestures.js.map +1 -1
- package/dist/automation/plan.js +65 -17
- package/dist/automation/plan.js.map +1 -1
- package/dist/automation/report.js +12 -2
- package/dist/automation/report.js.map +1 -1
- package/dist/automation/selectors.js +8 -3
- package/dist/automation/selectors.js.map +1 -1
- package/dist/automation/types.js.map +1 -1
- package/dist/automation/waits.js +9 -3
- package/dist/automation/waits.js.map +1 -1
- package/dist/automation/webview.js +7 -1
- package/dist/automation/webview.js.map +1 -1
- package/dist/automationGen/appiumModel.js +3 -1
- package/dist/automationGen/appiumModel.js.map +1 -1
- package/dist/automationGen/ciEmitter.js +1 -1
- package/dist/automationGen/ciEmitter.js.map +1 -1
- package/dist/automationGen/jsEmitter.js +29 -21
- package/dist/automationGen/jsEmitter.js.map +1 -1
- package/dist/automationGen/projectProfile.js +5 -1
- package/dist/automationGen/projectProfile.js.map +1 -1
- package/dist/automationGen/pythonEmitter.js +3 -3
- package/dist/automationGen/pythonEmitter.js.map +1 -1
- package/dist/automationGen/readmeEmitter.js.map +1 -1
- package/dist/automationGen/suitePlan.js +11 -6
- package/dist/automationGen/suitePlan.js.map +1 -1
- package/dist/automationGen/validation.js +33 -5
- package/dist/automationGen/validation.js.map +1 -1
- package/dist/build/parseBuildLog.js +234 -30
- package/dist/build/parseBuildLog.js.map +1 -1
- package/dist/build/plan.js +33 -12
- package/dist/build/plan.js.map +1 -1
- package/dist/ci/preflight.js.map +1 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/scan.js +1 -1
- package/dist/cli/scan.js.map +1 -1
- package/dist/cli/suite.js +2 -2
- package/dist/cli/suite.js.map +1 -1
- package/dist/cli/verify.js.map +1 -1
- package/dist/context/detect.js +10 -4
- package/dist/context/detect.js.map +1 -1
- package/dist/context/findApps.js +16 -2
- package/dist/context/findApps.js.map +1 -1
- package/dist/context/scan.js.map +1 -1
- package/dist/core/target.js +7 -5
- package/dist/core/target.js.map +1 -1
- package/dist/core/targetPlan.js +135 -17
- package/dist/core/targetPlan.js.map +1 -1
- package/dist/drivers/DirectDriver.js +17 -7
- package/dist/drivers/DirectDriver.js.map +1 -1
- package/dist/drivers/SimctlDriver.js +4 -1
- package/dist/drivers/SimctlDriver.js.map +1 -1
- package/dist/drivers/WdaDriver.js +15 -5
- package/dist/drivers/WdaDriver.js.map +1 -1
- package/dist/explore/candidates.js +5 -1
- package/dist/explore/candidates.js.map +1 -1
- package/dist/explore/graph.js +11 -2
- package/dist/explore/graph.js.map +1 -1
- package/dist/explore/memory.js +4 -1
- package/dist/explore/memory.js.map +1 -1
- package/dist/explore/planner.js +54 -9
- package/dist/explore/planner.js.map +1 -1
- package/dist/explore/policy.js +6 -1
- package/dist/explore/policy.js.map +1 -1
- package/dist/explore/runner.js +179 -23
- package/dist/explore/runner.js.map +1 -1
- package/dist/explore/signatures.js +3 -1
- package/dist/explore/signatures.js.map +1 -1
- package/dist/explore/suite.js +3 -1
- package/dist/explore/suite.js.map +1 -1
- package/dist/featureTesting/executionBootstrap.js +128 -19
- package/dist/featureTesting/executionBootstrap.js.map +1 -1
- package/dist/featureTesting/featureMap.js +4 -1
- package/dist/featureTesting/featureMap.js.map +1 -1
- package/dist/featureTesting/featureScope.js +94 -13
- package/dist/featureTesting/featureScope.js.map +1 -1
- package/dist/featureTesting/mapFeatureScope.js +37 -8
- package/dist/featureTesting/mapFeatureScope.js.map +1 -1
- package/dist/featureTesting/objectiveModel.js +5 -1
- package/dist/featureTesting/objectiveModel.js.map +1 -1
- package/dist/featureTesting/resultMerge.js +10 -2
- package/dist/featureTesting/resultMerge.js.map +1 -1
- package/dist/featureTesting/sources.js +4 -1
- package/dist/featureTesting/sources.js.map +1 -1
- package/dist/featureTesting/suiteBridge.js +7 -1
- package/dist/featureTesting/suiteBridge.js.map +1 -1
- package/dist/featureTesting/synonyms.js +31 -3
- package/dist/featureTesting/synonyms.js.map +1 -1
- package/dist/featureTesting/testCaseFactory.js +16 -4
- package/dist/featureTesting/testCaseFactory.js.map +1 -1
- package/dist/featureTesting/testPlan.js +32 -10
- package/dist/featureTesting/testPlan.js.map +1 -1
- package/dist/firstRun/classifyScreen.js +4 -1
- package/dist/firstRun/classifyScreen.js.map +1 -1
- package/dist/firstRun/firstRunPlanner.js +5 -2
- package/dist/firstRun/firstRunPlanner.js.map +1 -1
- package/dist/firstRun/firstRunRunner.js +84 -23
- package/dist/firstRun/firstRunRunner.js.map +1 -1
- package/dist/firstRun/generatedDataPolicy.js +16 -6
- package/dist/firstRun/generatedDataPolicy.js.map +1 -1
- package/dist/firstRun/inputPlanner.js +40 -6
- package/dist/firstRun/inputPlanner.js.map +1 -1
- package/dist/firstRun/paywallPolicy.js.map +1 -1
- package/dist/fixtures/catalog.js +39 -6
- package/dist/fixtures/catalog.js.map +1 -1
- package/dist/flows/generate.js +53 -12
- package/dist/flows/generate.js.map +1 -1
- package/dist/flows/lint.js +88 -14
- package/dist/flows/lint.js.map +1 -1
- package/dist/flows/pack.js +18 -2
- package/dist/flows/pack.js.map +1 -1
- package/dist/flows/repair.js +71 -20
- package/dist/flows/repair.js.map +1 -1
- package/dist/flows/run.js +164 -51
- package/dist/flows/run.js.map +1 -1
- package/dist/flows/schema.js +57 -20
- package/dist/flows/schema.js.map +1 -1
- package/dist/flows/seedExec.js +5 -1
- package/dist/flows/seedExec.js.map +1 -1
- package/dist/ios/signing.js +32 -3
- package/dist/ios/signing.js.map +1 -1
- package/dist/issues/classify.js +12 -4
- package/dist/issues/classify.js.map +1 -1
- package/dist/issues/fingerprint.js +11 -5
- package/dist/issues/fingerprint.js.map +1 -1
- package/dist/issues/index.js +4 -20
- package/dist/issues/index.js.map +1 -1
- package/dist/issues/metrics.js +2 -1
- package/dist/issues/metrics.js.map +1 -1
- package/dist/issues/recurrence.js.map +1 -1
- package/dist/issues/report.js +8 -4
- package/dist/issues/report.js.map +1 -1
- package/dist/issues/reportBridge.js +36 -5
- package/dist/issues/reportBridge.js.map +1 -1
- package/dist/issues/schema.js.map +1 -1
- package/dist/issues/store.js.map +1 -1
- package/dist/lib/android.js +9 -1
- package/dist/lib/android.js.map +1 -1
- package/dist/lib/device.js +21 -5
- package/dist/lib/device.js.map +1 -1
- package/dist/lib/image.js +11 -2
- package/dist/lib/image.js.map +1 -1
- package/dist/lib/lockfile.js +60 -0
- package/dist/lib/lockfile.js.map +1 -0
- package/dist/lib/needsInput.js +6 -3
- package/dist/lib/needsInput.js.map +1 -1
- package/dist/lib/png.js +2 -1
- package/dist/lib/png.js.map +1 -1
- package/dist/lib/result.js +16 -1
- package/dist/lib/result.js.map +1 -1
- package/dist/lib/schemaHash.js +4 -2
- package/dist/lib/schemaHash.js.map +1 -1
- package/dist/lib/sensitive.js +3 -1
- package/dist/lib/sensitive.js.map +1 -1
- package/dist/lib/simctl.js.map +1 -1
- package/dist/lib/spawn.js +29 -10
- package/dist/lib/spawn.js.map +1 -1
- package/dist/lib/wda.js +27 -10
- package/dist/lib/wda.js.map +1 -1
- package/dist/lib/wdaConfig.js +12 -5
- package/dist/lib/wdaConfig.js.map +1 -1
- package/dist/lib/wdaTune.js +52 -23
- package/dist/lib/wdaTune.js.map +1 -1
- package/dist/mobileAudit/checks.js +141 -19
- package/dist/mobileAudit/checks.js.map +1 -1
- package/dist/mobileAudit/profiles.js +120 -19
- package/dist/mobileAudit/profiles.js.map +1 -1
- package/dist/mobileAudit/results.js +7 -2
- package/dist/mobileAudit/results.js.map +1 -1
- package/dist/mobileAudit/runner.js +144 -19
- package/dist/mobileAudit/runner.js.map +1 -1
- package/dist/oracle/auth.js.map +1 -1
- package/dist/oracle/failures.js +888 -131
- package/dist/oracle/failures.js.map +1 -1
- package/dist/oracle/health.js +90 -14
- package/dist/oracle/health.js.map +1 -1
- package/dist/oracle/locator.js +147 -18
- package/dist/oracle/locator.js.map +1 -1
- package/dist/oracle/record.js.map +1 -1
- package/dist/orchestration/goal.js +3 -9
- package/dist/orchestration/goal.js.map +1 -1
- package/dist/orchestration/testThis/execute.js +126 -0
- package/dist/orchestration/testThis/execute.js.map +1 -0
- package/dist/orchestration/testThis/pipeline.js +324 -0
- package/dist/orchestration/testThis/pipeline.js.map +1 -0
- package/dist/orchestration/testThis/plan.js +430 -0
- package/dist/orchestration/testThis/plan.js.map +1 -0
- package/dist/orchestration/testThis/terminal.js +118 -0
- package/dist/orchestration/testThis/terminal.js.map +1 -0
- package/dist/orchestration/testThis/types.js +12 -0
- package/dist/orchestration/testThis/types.js.map +1 -0
- package/dist/plan/plan.js.map +1 -1
- package/dist/prompts/index.js +3 -3
- package/dist/prompts/index.js.map +1 -1
- package/dist/report/coverage.js +0 -1
- package/dist/report/coverage.js.map +1 -1
- package/dist/report/evidence.js.map +1 -1
- package/dist/report/export.js +59 -37
- package/dist/report/export.js.map +1 -1
- package/dist/report/flake.js +13 -14
- package/dist/report/flake.js.map +1 -1
- package/dist/report/history.js +72 -38
- package/dist/report/history.js.map +1 -1
- package/dist/report/policy.js +11 -3
- package/dist/report/policy.js.map +1 -1
- package/dist/report/qaLevel.js +2 -8
- package/dist/report/qaLevel.js.map +1 -1
- package/dist/report/readiness.js +42 -12
- package/dist/report/readiness.js.map +1 -1
- package/dist/report/summary.js.map +1 -1
- package/dist/report/timing.js +23 -23
- package/dist/report/timing.js.map +1 -1
- package/dist/server.js +42 -37
- package/dist/server.js.map +1 -1
- package/dist/services/automationGenerate.js.map +1 -1
- package/dist/services/build.js +8 -2
- package/dist/services/build.js.map +1 -1
- package/dist/services/preflight.js +38 -6
- package/dist/services/preflight.js.map +1 -1
- package/dist/services/prepareAndroid.js +46 -10
- package/dist/services/prepareAndroid.js.map +1 -1
- package/dist/services/prepareIos.js +72 -10
- package/dist/services/prepareIos.js.map +1 -1
- package/dist/services/report.js +173 -37
- package/dist/services/report.js.map +1 -1
- package/dist/services/smoke.js +36 -6
- package/dist/services/smoke.js.map +1 -1
- package/dist/services/suiteGenerate.js +16 -4
- package/dist/services/suiteGenerate.js.map +1 -1
- package/dist/services/testSuiteKnowledge.js +38 -6
- package/dist/services/testSuiteKnowledge.js.map +1 -1
- package/dist/session/attach.js +16 -0
- package/dist/session/attach.js.map +1 -1
- package/dist/session/processRegistry.js +164 -0
- package/dist/session/processRegistry.js.map +1 -0
- package/dist/session/progress.js +1 -1
- package/dist/session/progress.js.map +1 -1
- package/dist/session/store.js +216 -43
- package/dist/session/store.js.map +1 -1
- package/dist/snapshot/overlays.js +7 -2
- package/dist/snapshot/overlays.js.map +1 -1
- package/dist/snapshot/parse.js +2 -8
- package/dist/snapshot/parse.js.map +1 -1
- package/dist/snapshot/present.js +27 -3
- package/dist/snapshot/present.js.map +1 -1
- package/dist/state/profile.js +36 -9
- package/dist/state/profile.js.map +1 -1
- package/dist/suite/compile.js +7 -2
- package/dist/suite/compile.js.map +1 -1
- package/dist/suite/lint.js +21 -3
- package/dist/suite/lint.js.map +1 -1
- package/dist/suite/pom.js +26 -4
- package/dist/suite/pom.js.map +1 -1
- package/dist/suite/testcase.js +6 -4
- package/dist/suite/testcase.js.map +1 -1
- package/dist/testSuite/exporter.js +6 -2
- package/dist/testSuite/exporter.js.map +1 -1
- package/dist/testSuite/generator.js +31 -6
- package/dist/testSuite/generator.js.map +1 -1
- package/dist/testSuite/issueLinks.js.map +1 -1
- package/dist/testSuite/lint.js +1 -1
- package/dist/testSuite/lint.js.map +1 -1
- package/dist/testSuite/merge.js +8 -2
- package/dist/testSuite/merge.js.map +1 -1
- package/dist/testSuite/schema.js +16 -11
- package/dist/testSuite/schema.js.map +1 -1
- package/dist/testSuite/store.js +9 -1
- package/dist/testSuite/store.js.map +1 -1
- package/dist/tools/act.js +107 -43
- package/dist/tools/act.js.map +1 -1
- package/dist/tools/agent.js +114 -28
- package/dist/tools/agent.js.map +1 -1
- package/dist/tools/appControl.js +16 -3
- package/dist/tools/appControl.js.map +1 -1
- package/dist/tools/appMap.js +279 -133
- package/dist/tools/appMap.js.map +1 -1
- package/dist/tools/assertVisual.js +12 -2
- package/dist/tools/assertVisual.js.map +1 -1
- package/dist/tools/automationGenerate.js +276 -241
- package/dist/tools/automationGenerate.js.map +1 -1
- package/dist/tools/build.js +92 -41
- package/dist/tools/build.js.map +1 -1
- package/dist/tools/bundletool.js +139 -28
- package/dist/tools/bundletool.js.map +1 -1
- package/dist/tools/capabilities.js +43 -73
- package/dist/tools/capabilities.js.map +1 -1
- package/dist/tools/clearOverlay.js +45 -8
- package/dist/tools/clearOverlay.js.map +1 -1
- package/dist/tools/detectContext.js +1 -1
- package/dist/tools/detectContext.js.map +1 -1
- package/dist/tools/device.js +44 -7
- package/dist/tools/device.js.map +1 -1
- package/dist/tools/doctor.js +38 -15
- package/dist/tools/doctor.js.map +1 -1
- package/dist/tools/explore.js +96 -22
- package/dist/tools/explore.js.map +1 -1
- package/dist/tools/featureTesting.js +153 -114
- package/dist/tools/featureTesting.js.map +1 -1
- package/dist/tools/firstRun.js +95 -60
- package/dist/tools/firstRun.js.map +1 -1
- package/dist/tools/flow.js +236 -101
- package/dist/tools/flow.js.map +1 -1
- package/dist/tools/flowGenerate.js +51 -47
- package/dist/tools/flowGenerate.js.map +1 -1
- package/dist/tools/flowRepair.js +24 -4
- package/dist/tools/flowRepair.js.map +1 -1
- package/dist/tools/generate.js +236 -0
- package/dist/tools/generate.js.map +1 -0
- package/dist/tools/getArtifact.js +21 -3
- package/dist/tools/getArtifact.js.map +1 -1
- package/dist/tools/health.js +3 -1
- package/dist/tools/health.js.map +1 -1
- package/dist/tools/history.js +40 -10
- package/dist/tools/history.js.map +1 -1
- package/dist/tools/ios.js +157 -25
- package/dist/tools/ios.js.map +1 -1
- package/dist/tools/issues.js +19 -150
- package/dist/tools/issues.js.map +1 -1
- package/dist/tools/jobs.js +23 -4
- package/dist/tools/jobs.js.map +1 -1
- package/dist/tools/locator.js +36 -8
- package/dist/tools/locator.js.map +1 -1
- package/dist/tools/metro.js +61 -16
- package/dist/tools/metro.js.map +1 -1
- package/dist/tools/mobileAudit.js +67 -15
- package/dist/tools/mobileAudit.js.map +1 -1
- package/dist/tools/network.js +6 -1
- package/dist/tools/network.js.map +1 -1
- package/dist/tools/note.js +20 -6
- package/dist/tools/note.js.map +1 -1
- package/dist/tools/permissions.js +31 -6
- package/dist/tools/permissions.js.map +1 -1
- package/dist/tools/plan.js +5 -2
- package/dist/tools/plan.js.map +1 -1
- package/dist/tools/prepareIosTarget.js +31 -8
- package/dist/tools/prepareIosTarget.js.map +1 -1
- package/dist/tools/prepareTarget.js +97 -18
- package/dist/tools/prepareTarget.js.map +1 -1
- package/dist/tools/report.js +24 -5
- package/dist/tools/report.js.map +1 -1
- package/dist/tools/resolveArtifact.js +9 -5
- package/dist/tools/resolveArtifact.js.map +1 -1
- package/dist/tools/resolveTarget.js +11 -2
- package/dist/tools/resolveTarget.js.map +1 -1
- package/dist/tools/screenInfo.js +8 -2
- package/dist/tools/screenInfo.js.map +1 -1
- package/dist/tools/screenRecord.js +74 -11
- package/dist/tools/screenRecord.js.map +1 -1
- package/dist/tools/screenshot.js +23 -6
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/seed.js +38 -6
- package/dist/tools/seed.js.map +1 -1
- package/dist/tools/smoke.js +16 -3
- package/dist/tools/smoke.js.map +1 -1
- package/dist/tools/snapshot.js +20 -7
- package/dist/tools/snapshot.js.map +1 -1
- package/dist/tools/startSession.js +54 -9
- package/dist/tools/startSession.js.map +1 -1
- package/dist/tools/state.js +62 -15
- package/dist/tools/state.js.map +1 -1
- package/dist/tools/suite.js +350 -298
- package/dist/tools/suite.js.map +1 -1
- package/dist/tools/testSuite.js +94 -28
- package/dist/tools/testSuite.js.map +1 -1
- package/dist/tools/testThis.js +30 -793
- package/dist/tools/testThis.js.map +1 -1
- package/dist/tools/visual.js +124 -18
- package/dist/tools/visual.js.map +1 -1
- package/dist/tools/visualText.js +51 -6
- package/dist/tools/visualText.js.map +1 -1
- package/dist/tools/wait.js +18 -3
- package/dist/tools/wait.js.map +1 -1
- package/dist/tools/wda.js +157 -29
- package/dist/tools/wda.js.map +1 -1
- package/dist/version.js +14 -50
- package/dist/version.js.map +1 -1
- package/dist/visual/ocr.js +12 -2
- package/dist/visual/ocr.js.map +1 -1
- package/dist/visual/provider.js +7 -3
- package/dist/visual/provider.js.map +1 -1
- package/docs/mcp-server.md +44 -4
- package/docs/tools.md +33 -102
- package/package.json +14 -5
- package/dist/appMap/diff.js +0 -60
- package/dist/appMap/diff.js.map +0 -1
- package/dist/automation/maestroIr.js +0 -282
- package/dist/automation/maestroIr.js.map +0 -1
- package/dist/interop/maestro.js +0 -151
- package/dist/interop/maestro.js.map +0 -1
- package/dist/tools/idling.js +0 -104
- package/dist/tools/idling.js.map +0 -1
- package/dist/tools/inputCapabilities.js +0 -30
- package/dist/tools/inputCapabilities.js.map +0 -1
- package/dist/tools/maestro.js +0 -78
- package/dist/tools/maestro.js.map +0 -1
package/dist/tools/appMap.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
// App Knowledge Map MCP tools. Public v1 reads, builds, queries,
|
|
1
|
+
// App Knowledge Map MCP tools. Public v1 reads, builds, queries, and scopes the
|
|
2
2
|
// durable `.swipium/app-map.json`. Large map data is returned by RESOURCE
|
|
3
3
|
// URI (swipium://project/<id>/app-map…) rather than flooded into the text channel; compact,
|
|
4
4
|
// structured data is returned inline. All heavy logic lives in src/appMap/*; these are thin.
|
|
5
5
|
import { existsSync, readFileSync } from 'node:fs';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { qaOk, qaError } from '../lib/result.js';
|
|
8
|
+
import { qaNeedsInput } from '../lib/needsInput.js';
|
|
8
9
|
import { resolveProjectRoot } from '../context/projectRoot.js';
|
|
9
10
|
import { buildAppMap, summarizeMap } from '../appMap/build.js';
|
|
10
11
|
import { queryAppMap } from '../appMap/query.js';
|
|
11
|
-
import { loadAppMap, loadCodeIndex, saveAppMap, saveIndexes,
|
|
12
|
+
import { loadAppMap, loadCodeIndex, saveAppMap, saveIndexes, appMapResourceUri, appMapPath, projectId } from '../appMap/store.js';
|
|
12
13
|
import { addProvenance, makeProvenance, recomputeConfidence } from '../appMap/provenance.js';
|
|
13
|
-
import { diffAppMaps } from '../appMap/diff.js';
|
|
14
|
-
import { migrateAppMap } from '../appMap/migrations.js';
|
|
15
14
|
import { rememberProject, lookupRoot } from '../appMap/projectRegistry.js';
|
|
15
|
+
import { resolveFeatureContext } from './featureTesting.js';
|
|
16
16
|
import { detectFramework } from '../context/detect.js';
|
|
17
17
|
// projectId(root) is a one-way hash, so resource reads need a reverse lookup. We remember every root
|
|
18
18
|
// touched this session (in-memory) AND in a DURABLE registry (~/.swipium/projects.json, Fix 8) so a
|
|
@@ -55,7 +55,14 @@ async function rootFor(server, sessions, args) {
|
|
|
55
55
|
}
|
|
56
56
|
function fallbackProject(root) {
|
|
57
57
|
const fw = detectFramework(root);
|
|
58
|
-
return {
|
|
58
|
+
return {
|
|
59
|
+
root,
|
|
60
|
+
gitRemote: null,
|
|
61
|
+
packageName: null,
|
|
62
|
+
workspaceTarget: null,
|
|
63
|
+
framework: fw,
|
|
64
|
+
platforms: fw === 'native-android' ? ['android'] : fw === 'native-ios' ? ['ios'] : ['android', 'ios'],
|
|
65
|
+
};
|
|
59
66
|
}
|
|
60
67
|
/** Read an existing map WITHOUT rescanning. Returns null when no map file exists yet. */
|
|
61
68
|
function readExistingMap(root) {
|
|
@@ -83,15 +90,26 @@ export function registerAppMap(server, sessions) {
|
|
|
83
90
|
inputSchema: {
|
|
84
91
|
projectRoot: z.string().optional().describe('Absolute project root. Omit to use a session root or the MCP workspace root.'),
|
|
85
92
|
sessionId: z.string().optional().describe('Reuse a session (its root + latest exploration graph).'),
|
|
86
|
-
mode: z
|
|
93
|
+
mode: z
|
|
94
|
+
.enum(['static_only', 'runtime_merge', 'full'])
|
|
95
|
+
.optional()
|
|
96
|
+
.describe('static_only: code scan only; runtime_merge: merge the session exploration graph only; full (default): both.'),
|
|
87
97
|
includeCodeIndex: z.boolean().optional().describe('Persist a code symbol index for queries (default true).'),
|
|
88
98
|
forceRescan: z.boolean().optional().describe('Re-run the static scan even if an up-to-date map exists (default false).'),
|
|
89
99
|
},
|
|
90
|
-
|
|
100
|
+
// NOTE: no outputSchema — a declared (closed) output schema makes strict MCP clients
|
|
101
|
+
// reject BOTH the rich qaOk payload and the qaError envelope as "additional properties"
|
|
102
|
+
// (caught by test/errorContract.test.ts). structuredContent stays self-describing.
|
|
91
103
|
}, async ({ projectRoot, sessionId, mode, includeCodeIndex, forceRescan }) => {
|
|
92
104
|
const { root, session, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
|
|
93
105
|
if (!root)
|
|
94
|
-
return qaError({
|
|
106
|
+
return qaError({
|
|
107
|
+
what: 'Could not resolve a project root',
|
|
108
|
+
changedState: false,
|
|
109
|
+
retrySafe: true,
|
|
110
|
+
nextSteps: ['Pass projectRoot="/abs/path" or a valid sessionId.'],
|
|
111
|
+
clientHint: hint,
|
|
112
|
+
});
|
|
95
113
|
remember(root);
|
|
96
114
|
const m = (mode ?? 'full');
|
|
97
115
|
const exploreGraph = m === 'static_only' ? null : latestExploreGraph(sessions, session);
|
|
@@ -101,7 +119,9 @@ export function registerAppMap(server, sessions) {
|
|
|
101
119
|
const merge = res.mergeResult;
|
|
102
120
|
const text = `🗺️ app map built (${m}) for ${root}\n` +
|
|
103
121
|
`framework=${res.map.project.framework} router=${res.map.staticTopology.router ?? 'none'} · static screens=${res.map.staticTopology.screens.length} · runtime screens=${res.map.runtimeTopology.screens.length}\n` +
|
|
104
|
-
(merge
|
|
122
|
+
(merge
|
|
123
|
+
? `merge: +${merge.newRuntimeScreens} new, ~${merge.updatedRuntimeScreens} updated, ${merge.linkedScreens} linked, ${merge.unmappedRuntimeScreens} unmapped\n`
|
|
124
|
+
: '') +
|
|
105
125
|
`confidence=${res.map.confidence.overall} · features=${res.map.features.length}\n` +
|
|
106
126
|
(Array.isArray(summary.topGaps) && summary.topGaps.length ? `gaps: ${summary.topGaps.join('; ')}\n` : '') +
|
|
107
127
|
`appMapUri: ${res.save?.resourceUri}`;
|
|
@@ -118,7 +138,12 @@ export function registerAppMap(server, sessions) {
|
|
|
118
138
|
}, text);
|
|
119
139
|
}
|
|
120
140
|
catch (e) {
|
|
121
|
-
return qaError({
|
|
141
|
+
return qaError({
|
|
142
|
+
what: `App map build failed: ${String(e)}`,
|
|
143
|
+
changedState: false,
|
|
144
|
+
retrySafe: true,
|
|
145
|
+
nextSteps: ['Check the project root is a supported mobile project.'],
|
|
146
|
+
});
|
|
122
147
|
}
|
|
123
148
|
});
|
|
124
149
|
// ------------------------------------------------------------------- read
|
|
@@ -132,29 +157,56 @@ export function registerAppMap(server, sessions) {
|
|
|
132
157
|
featureId: z.string().optional(),
|
|
133
158
|
screenId: z.string().optional(),
|
|
134
159
|
},
|
|
135
|
-
outputSchema: { ok: z.boolean(), appMapUri: z.string().optional(), section: z.string().optional() },
|
|
136
160
|
}, async ({ projectRoot, sessionId, section, featureId, screenId }) => {
|
|
137
161
|
const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
|
|
138
162
|
if (!root)
|
|
139
|
-
return qaError({
|
|
163
|
+
return qaError({
|
|
164
|
+
what: 'Could not resolve a project root',
|
|
165
|
+
changedState: false,
|
|
166
|
+
retrySafe: true,
|
|
167
|
+
nextSteps: ['Pass projectRoot or sessionId.'],
|
|
168
|
+
clientHint: hint,
|
|
169
|
+
});
|
|
140
170
|
remember(root);
|
|
141
171
|
const map = readExistingMap(root);
|
|
142
172
|
if (!map)
|
|
143
|
-
return qaError({
|
|
173
|
+
return qaError({
|
|
174
|
+
what: 'No app map yet',
|
|
175
|
+
changedState: false,
|
|
176
|
+
retrySafe: true,
|
|
177
|
+
nextSteps: ['Run qa_app_map_build first.'],
|
|
178
|
+
failureCode: 'NO_APP_MAP',
|
|
179
|
+
});
|
|
144
180
|
const uri = appMapResourceUri(root);
|
|
145
181
|
const sec = section ?? 'summary';
|
|
146
182
|
if (featureId) {
|
|
147
183
|
const f = map.features.find((x) => x.id === featureId);
|
|
148
184
|
if (!f)
|
|
149
|
-
return qaError({
|
|
185
|
+
return qaError({
|
|
186
|
+
what: `Unknown featureId ${featureId}`,
|
|
187
|
+
changedState: false,
|
|
188
|
+
retrySafe: true,
|
|
189
|
+
nextSteps: ['Call qa_app_map_read { section:"features" } to list ids.'],
|
|
190
|
+
});
|
|
150
191
|
return qaOk({ appMapUri: uri, section: 'feature', feature: f, featureResourceUri: `${uri}/feature/${featureId}` }, `feature ${f.title} — ${f.testCoverage} coverage, ${f.status}, confidence ${f.confidence}`);
|
|
151
192
|
}
|
|
152
193
|
if (screenId) {
|
|
153
194
|
const s = map.staticTopology.screens.find((x) => x.id === screenId);
|
|
154
195
|
const r = map.runtimeTopology.screens.find((x) => x.id === screenId);
|
|
155
196
|
if (!s && !r)
|
|
156
|
-
return qaError({
|
|
157
|
-
|
|
197
|
+
return qaError({
|
|
198
|
+
what: `Unknown screenId ${screenId}`,
|
|
199
|
+
changedState: false,
|
|
200
|
+
retrySafe: true,
|
|
201
|
+
nextSteps: ['Call qa_app_map_read { section:"screens" } to list ids.'],
|
|
202
|
+
});
|
|
203
|
+
return qaOk({
|
|
204
|
+
appMapUri: uri,
|
|
205
|
+
section: 'screen',
|
|
206
|
+
staticScreen: s ?? null,
|
|
207
|
+
runtimeScreen: r ?? null,
|
|
208
|
+
screenResourceUri: `${uri}/screen/${screenId}`,
|
|
209
|
+
}, `screen ${screenId}`);
|
|
158
210
|
}
|
|
159
211
|
switch (sec) {
|
|
160
212
|
case 'summary':
|
|
@@ -163,8 +215,21 @@ export function registerAppMap(server, sessions) {
|
|
|
163
215
|
return qaOk({
|
|
164
216
|
appMapUri: uri,
|
|
165
217
|
section: sec,
|
|
166
|
-
staticScreens: map.staticTopology.screens.map((s) => ({
|
|
167
|
-
|
|
218
|
+
staticScreens: map.staticTopology.screens.map((s) => ({
|
|
219
|
+
id: s.id,
|
|
220
|
+
name: s.name,
|
|
221
|
+
route: s.route,
|
|
222
|
+
kind: s.kind,
|
|
223
|
+
confidence: s.confidence,
|
|
224
|
+
})),
|
|
225
|
+
runtimeScreens: map.runtimeTopology.screens.map((r) => ({
|
|
226
|
+
id: r.id,
|
|
227
|
+
title: r.title,
|
|
228
|
+
visits: r.visits,
|
|
229
|
+
linkedStaticScreenId: r.linkedStaticScreenId,
|
|
230
|
+
unmapped: r.unmapped,
|
|
231
|
+
locatorReadiness: r.locatorReadiness,
|
|
232
|
+
})),
|
|
168
233
|
unvisitedStaticScreens: map.runtimeTopology.unvisitedStaticScreens,
|
|
169
234
|
}, `${map.staticTopology.screens.length} static / ${map.runtimeTopology.screens.length} runtime screens; ${map.runtimeTopology.unvisitedStaticScreens.length} unvisited`);
|
|
170
235
|
case 'features':
|
|
@@ -178,7 +243,12 @@ export function registerAppMap(server, sessions) {
|
|
|
178
243
|
case 'full':
|
|
179
244
|
default:
|
|
180
245
|
// Protect context: point at the resource instead of inlining the whole map.
|
|
181
|
-
return qaOk({
|
|
246
|
+
return qaOk({
|
|
247
|
+
appMapUri: uri,
|
|
248
|
+
section: 'full',
|
|
249
|
+
summary: summarizeMap(map),
|
|
250
|
+
note: 'Full map omitted from text to protect context — read the appMapUri resource for everything.',
|
|
251
|
+
}, `Full map at resource: ${uri}`);
|
|
182
252
|
}
|
|
183
253
|
});
|
|
184
254
|
// ------------------------------------------------------------------ query
|
|
@@ -192,57 +262,154 @@ export function registerAppMap(server, sessions) {
|
|
|
192
262
|
intent: z.enum(['feature', 'screen', 'code', 'test', 'freeform']).optional(),
|
|
193
263
|
limit: z.number().optional(),
|
|
194
264
|
},
|
|
195
|
-
outputSchema: { ok: z.boolean(), total: z.number().optional() },
|
|
196
265
|
}, async ({ query, projectRoot, sessionId, intent, limit }) => {
|
|
197
266
|
const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
|
|
198
267
|
if (!root)
|
|
199
|
-
return qaError({
|
|
268
|
+
return qaError({
|
|
269
|
+
what: 'Could not resolve a project root',
|
|
270
|
+
changedState: false,
|
|
271
|
+
retrySafe: true,
|
|
272
|
+
nextSteps: ['Pass projectRoot or sessionId.'],
|
|
273
|
+
clientHint: hint,
|
|
274
|
+
});
|
|
200
275
|
remember(root);
|
|
201
276
|
const map = readExistingMap(root);
|
|
202
277
|
if (!map)
|
|
203
|
-
return qaError({
|
|
278
|
+
return qaError({
|
|
279
|
+
what: 'No app map yet',
|
|
280
|
+
changedState: false,
|
|
281
|
+
retrySafe: true,
|
|
282
|
+
nextSteps: ['Run qa_app_map_build first.'],
|
|
283
|
+
failureCode: 'NO_APP_MAP',
|
|
284
|
+
});
|
|
204
285
|
const codeIndex = loadCodeIndex(root);
|
|
205
286
|
const out = queryAppMap(map, codeIndex, { query, intent, limit });
|
|
206
|
-
const top = out.results
|
|
287
|
+
const top = out.results
|
|
288
|
+
.slice(0, 5)
|
|
289
|
+
.map((r, i) => ` ${i + 1}. [${r.type}] ${r.title} (score ${r.score}${r.confidence !== undefined ? `, conf ${r.confidence}` : ''}) → ${r.recommendedNextTool.tool}`)
|
|
290
|
+
.join('\n');
|
|
207
291
|
return qaOk({ ...out, appMapUri: appMapResourceUri(root) }, `🔎 "${query}" — ${out.total} result(s)\n${top || ' (no matches)'}`);
|
|
208
292
|
});
|
|
209
293
|
// ----------------------------------------------------------- feature_scope
|
|
210
294
|
server.registerTool('qa_app_map_feature_scope', {
|
|
211
295
|
title: 'Scope testing to a feature',
|
|
212
|
-
description: 'Resolve a feature (by featureId or free-text query) to a focused test scope
|
|
296
|
+
description: 'Resolve a feature (by featureId or free-text query) to a focused test scope. READ-ONLY — no device, no mutation. With query (e.g. "weather analysis"), it scopes app-map-FIRST and falls back to a fresh code-index scan + the latest exploration screen graph when no map exists yet — returning ranked code symbols, static + runtime screens, existing tests, an inferred objective model, coverage gaps, a recommended test strategy, and ALL plausible candidates with confidence (asking ONE disambiguation question only when genuinely-different features tie). With featureId, it returns that map feature\'s source files, screens, coverage, blockers, and a recommended plan (requires an existing map). Pass sessionId (preferred — adds runtime evidence) or projectRoot.',
|
|
213
297
|
inputSchema: {
|
|
214
|
-
projectRoot: z.string().optional(),
|
|
215
|
-
sessionId: z.string().optional(),
|
|
216
|
-
featureId: z.string().optional(),
|
|
217
|
-
query: z.string().optional().describe('Free-text feature description, e.g. "checkout flow".'),
|
|
298
|
+
projectRoot: z.string().optional().describe('Project root when no session exists.'),
|
|
299
|
+
sessionId: z.string().optional().describe('Session to scope against (adds runtime screen-graph evidence).'),
|
|
300
|
+
featureId: z.string().optional().describe('Exact app-map feature id (requires an existing map). Provide this OR query.'),
|
|
301
|
+
query: z.string().optional().describe('Free-text feature description, e.g. "checkout flow" or "weather analysis".'),
|
|
302
|
+
platform: z.enum(['android', 'ios']).optional(),
|
|
303
|
+
includeCode: z.boolean().optional().describe('Query mode: scan source for code-aware matches (default true).'),
|
|
304
|
+
limit: z.number().optional().describe('Query mode: max items per list in the scope (default 8).'),
|
|
218
305
|
},
|
|
219
|
-
|
|
220
|
-
|
|
306
|
+
}, async ({ projectRoot, sessionId, featureId, query, platform, includeCode, limit }) => {
|
|
307
|
+
if (!featureId && !query) {
|
|
308
|
+
return qaError({
|
|
309
|
+
what: 'Provide one of: featureId or query',
|
|
310
|
+
changedState: false,
|
|
311
|
+
retrySafe: true,
|
|
312
|
+
nextSteps: ['e.g. qa_app_map_feature_scope { query:"login" }'],
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
// Free-text QUERY path: full feature scoping (app-map-first, code-index + runtime-graph
|
|
316
|
+
// fallback, objective model, candidate disambiguation). Works without an existing map.
|
|
317
|
+
if (!featureId) {
|
|
318
|
+
const r = await resolveFeatureContext(server, sessions, { sessionId, projectRoot, feature: query, platform, includeCode, limit });
|
|
319
|
+
if (!r.ok)
|
|
320
|
+
return r.result;
|
|
321
|
+
remember(r.ctx.root);
|
|
322
|
+
const { scopeResult, objective, index } = r.ctx;
|
|
323
|
+
const scope = scopeResult.primary;
|
|
324
|
+
if (!scopeResult.found) {
|
|
325
|
+
return qaOk({
|
|
326
|
+
sessionId: sessionId ?? null,
|
|
327
|
+
query,
|
|
328
|
+
found: false,
|
|
329
|
+
scope,
|
|
330
|
+
searched: scopeResult.searched,
|
|
331
|
+
nextRecommendedAction: {
|
|
332
|
+
tool: 'qa_test_this',
|
|
333
|
+
args: { ...(sessionId ? { sessionId } : { projectRoot: r.ctx.root }), goal: 'explore' },
|
|
334
|
+
why: 'Grow the map with an initial run, then re-scope the feature',
|
|
335
|
+
},
|
|
336
|
+
}, `🔎 No feature matched "${query}". Searched ${scopeResult.searched.symbols} symbols, ${scopeResult.searched.routes} routes, ${scopeResult.searched.files} files, ${scopeResult.searched.runtimeScreens} runtime screens with terms: ${scopeResult.searched.terms.slice(0, 12).join(', ')}. Run qa_test_this/qa_explore to grow coverage, or refine the feature name.`);
|
|
337
|
+
}
|
|
338
|
+
if (scopeResult.needsInput) {
|
|
339
|
+
return qaNeedsInput({
|
|
340
|
+
needsInput: true,
|
|
341
|
+
kind: 'monorepo_target',
|
|
342
|
+
question: scopeResult.needsInput.question,
|
|
343
|
+
fields: [{ name: 'query', description: 'The exact feature to test', example: scopeResult.candidates[0]?.title }],
|
|
344
|
+
fallbackOptions: scopeResult.needsInput.options,
|
|
345
|
+
resume: { tool: 'qa_app_map_feature_scope', args: {} },
|
|
346
|
+
attempted: [`scoped "${query}" — matched ${scopeResult.candidates.length} distinct candidates that tie`],
|
|
347
|
+
ifDeclined: 'Swipium scopes the highest-confidence candidate and records the others as alternatives.',
|
|
348
|
+
}, { sessionId: sessionId ?? undefined, candidates: scopeResult.candidates });
|
|
349
|
+
}
|
|
350
|
+
const nextRecommendedAction = scope.recommendedStrategy === 'manual_blocked'
|
|
351
|
+
? {
|
|
352
|
+
tool: 'qa_test_feature',
|
|
353
|
+
args: { ...(sessionId ? { sessionId } : { projectRoot: r.ctx.root }), feature: query, mode: 'plan' },
|
|
354
|
+
why: 'Review the plan + setup needed before any automated execution',
|
|
355
|
+
}
|
|
356
|
+
: {
|
|
357
|
+
tool: 'qa_test_feature',
|
|
358
|
+
args: { sessionId: sessionId ?? '${sessionId}', feature: query, mode: 'execute' },
|
|
359
|
+
why: 'Run a focused test of this feature',
|
|
360
|
+
};
|
|
361
|
+
return qaOk({
|
|
362
|
+
sessionId: sessionId ?? null,
|
|
363
|
+
query,
|
|
364
|
+
found: true,
|
|
365
|
+
featureId: scope.featureId,
|
|
366
|
+
scope,
|
|
367
|
+
objective,
|
|
368
|
+
appMapUri: r.ctx.appMapUri,
|
|
369
|
+
mapFeatureId: scopeResult.mapFeatureId ?? null,
|
|
370
|
+
ticketRefs: scopeResult.ticketRefs,
|
|
371
|
+
runtimeSource: scopeResult.runtimeSource,
|
|
372
|
+
candidates: scopeResult.candidates,
|
|
373
|
+
searched: scopeResult.searched,
|
|
374
|
+
codeIndex: { scannedFiles: index.scannedFiles, truncated: index.truncated },
|
|
375
|
+
nextRecommendedAction,
|
|
376
|
+
}, `🔎 ${scope.title} (confidence ${Math.round(scope.confidence * 100)}%, strategy ${scope.recommendedStrategy}) — ` +
|
|
377
|
+
`${scope.staticScreens.length} static screen(s), ${scope.runtimeScreens.length} runtime screen(s), ${scope.functions.length} symbol(s), ${scope.existingTests.length} existing test(s).` +
|
|
378
|
+
(scopeResult.candidates.length > 1 ? ` ${scopeResult.candidates.length} candidate(s).` : ''));
|
|
379
|
+
}
|
|
380
|
+
// FEATURE-ID path: exact map lookup (requires an existing map).
|
|
221
381
|
const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
|
|
222
382
|
if (!root)
|
|
223
|
-
return qaError({
|
|
383
|
+
return qaError({
|
|
384
|
+
what: 'Could not resolve a project root',
|
|
385
|
+
changedState: false,
|
|
386
|
+
retrySafe: true,
|
|
387
|
+
nextSteps: ['Pass projectRoot or sessionId.'],
|
|
388
|
+
clientHint: hint,
|
|
389
|
+
});
|
|
224
390
|
remember(root);
|
|
225
391
|
const map = readExistingMap(root);
|
|
226
392
|
if (!map)
|
|
227
|
-
return qaError({
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return qaError({ what: 'Provide one of: featureId or query', changedState: false, retrySafe: true, nextSteps: ['e.g. qa_app_map_feature_scope { query:"login" }'] });
|
|
243
|
-
}
|
|
393
|
+
return qaError({
|
|
394
|
+
what: 'No app map yet',
|
|
395
|
+
changedState: false,
|
|
396
|
+
retrySafe: true,
|
|
397
|
+
nextSteps: ['Run qa_app_map_build first, or pass query= for map-free scoping.'],
|
|
398
|
+
failureCode: 'NO_APP_MAP',
|
|
399
|
+
});
|
|
400
|
+
const features = map.features.filter((f) => f.id === featureId);
|
|
401
|
+
if (!features.length)
|
|
402
|
+
return qaError({
|
|
403
|
+
what: `Unknown featureId ${featureId}`,
|
|
404
|
+
changedState: false,
|
|
405
|
+
retrySafe: true,
|
|
406
|
+
nextSteps: ['List ids with qa_app_map_read { section:"features" }, or pass query= for free-text scoping.'],
|
|
407
|
+
});
|
|
244
408
|
const scope = features.map((f) => {
|
|
245
|
-
const staticScreens = f.staticScreens
|
|
409
|
+
const staticScreens = f.staticScreens
|
|
410
|
+
.map((id) => map.staticTopology.screens.find((s) => s.id === id))
|
|
411
|
+
.filter(Boolean)
|
|
412
|
+
.map((s) => ({ id: s.id, name: s.name, route: s.route, sourceFiles: s.sourceFiles }));
|
|
246
413
|
const needsCreds = f.id === 'feature:auth' || f.blockers.some((b) => /credential/i.test(b));
|
|
247
414
|
const recommendedGoal = f.id === 'feature:auth' ? 'test_login' : 'reproduce_bug';
|
|
248
415
|
return {
|
|
@@ -259,38 +426,25 @@ export function registerAppMap(server, sessions) {
|
|
|
259
426
|
blockers: f.blockers,
|
|
260
427
|
recommendedPlan: {
|
|
261
428
|
tool: 'qa_test_this',
|
|
262
|
-
args: {
|
|
263
|
-
|
|
429
|
+
args: {
|
|
430
|
+
mode: 'execute',
|
|
431
|
+
goal: recommendedGoal,
|
|
432
|
+
goalText: f.title,
|
|
433
|
+
explore: true,
|
|
434
|
+
...(needsCreds ? { stopOnNeedsInput: true } : {}),
|
|
435
|
+
},
|
|
436
|
+
why: needsCreds
|
|
437
|
+
? `Drive "${f.title}"; will stop for test credentials (fixture) — ${f.testCoverage} coverage today`
|
|
438
|
+
: `Drive "${f.title}" with focused exploration — ${f.testCoverage} coverage today`,
|
|
264
439
|
},
|
|
265
440
|
};
|
|
266
441
|
});
|
|
267
442
|
const text = `scope: ${scope.length} feature(s)\n` +
|
|
268
|
-
scope
|
|
443
|
+
scope
|
|
444
|
+
.map((s) => ` • ${s.title} [${s.testCoverage}] — ${s.staticScreens.length} screen(s), ${s.sourceFiles.length} file(s)${s.blockers.length ? ` · blockers: ${s.blockers.join(', ')}` : ''}\n → ${s.recommendedPlan.tool} ${JSON.stringify(s.recommendedPlan.args)}`)
|
|
445
|
+
.join('\n');
|
|
269
446
|
return qaOk({ appMapUri: appMapResourceUri(root), featureId: scope[0]?.featureId, scope }, text);
|
|
270
447
|
});
|
|
271
|
-
// --------------------------------------------------------------- validate
|
|
272
|
-
server.registerTool('qa_app_map_validate', {
|
|
273
|
-
title: 'Validate the app knowledge map',
|
|
274
|
-
description: 'Validate the app map: schema version, provenance completeness, missing/dangling links, duplicate ids, impossible states (e.g. covered-without-runtime), and (optionally) stale source fingerprints. Returns errors + warnings with codes. Use before trusting an old map for feature-focused testing.',
|
|
275
|
-
inputSchema: {
|
|
276
|
-
projectRoot: z.string().optional(),
|
|
277
|
-
sessionId: z.string().optional(),
|
|
278
|
-
checkFingerprint: z.boolean().optional().describe('Also re-hash fingerprinted source files to detect a stale map (default false).'),
|
|
279
|
-
},
|
|
280
|
-
outputSchema: { ok: z.boolean(), valid: z.boolean().optional(), errors: z.number().optional(), warnings: z.number().optional() },
|
|
281
|
-
}, async ({ projectRoot, sessionId, checkFingerprint }) => {
|
|
282
|
-
const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
|
|
283
|
-
if (!root)
|
|
284
|
-
return qaError({ what: 'Could not resolve a project root', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot or sessionId.'], clientHint: hint });
|
|
285
|
-
remember(root);
|
|
286
|
-
const map = readExistingMap(root);
|
|
287
|
-
if (!map)
|
|
288
|
-
return qaError({ what: 'No app map yet', changedState: false, retrySafe: true, nextSteps: ['Run qa_app_map_build first.'], failureCode: 'NO_APP_MAP' });
|
|
289
|
-
const v = validateAppMap(map, { checkFingerprint, root });
|
|
290
|
-
const text = `${v.ok ? '✅' : '❌'} app map ${v.ok ? 'valid' : 'INVALID'} — ${v.errors} error(s), ${v.warnings} warning(s)\n` +
|
|
291
|
-
v.issues.slice(0, 12).map((i) => ` ${i.severity === 'error' ? '✗' : '⚠'} ${i.code}: ${i.detail}`).join('\n');
|
|
292
|
-
return qaOk({ valid: v.ok, errors: v.errors, warnings: v.warnings, issues: v.issues, appMapUri: appMapResourceUri(root) }, text);
|
|
293
|
-
});
|
|
294
448
|
// ----------------------------------------------------------------- update
|
|
295
449
|
server.registerTool('qa_app_map_update', {
|
|
296
450
|
title: 'Update the app knowledge map',
|
|
@@ -299,20 +453,49 @@ export function registerAppMap(server, sessions) {
|
|
|
299
453
|
projectRoot: z.string().optional(),
|
|
300
454
|
sessionId: z.string().optional(),
|
|
301
455
|
note: z.string().optional().describe('A free-text user note added with user_note provenance.'),
|
|
302
|
-
testCases: z
|
|
303
|
-
|
|
456
|
+
testCases: z
|
|
457
|
+
.array(z.object({
|
|
458
|
+
id: z.string(),
|
|
459
|
+
title: z.string(),
|
|
460
|
+
featureId: z.string().optional(),
|
|
461
|
+
screenId: z.string().optional(),
|
|
462
|
+
status: z.string().optional(),
|
|
463
|
+
source: z.string().optional(),
|
|
464
|
+
stale: z.boolean().optional(),
|
|
465
|
+
}))
|
|
466
|
+
.optional(),
|
|
467
|
+
automationSuite: z
|
|
468
|
+
.object({
|
|
469
|
+
name: z.string(),
|
|
470
|
+
path: z.string(),
|
|
471
|
+
framework: z.string().optional(),
|
|
472
|
+
linkedFeatureIds: z.array(z.string()).optional(),
|
|
473
|
+
linkedScreenIds: z.array(z.string()).optional(),
|
|
474
|
+
})
|
|
475
|
+
.optional(),
|
|
304
476
|
environment: z.string().optional().describe("App environment, e.g. 'test' | 'staging'."),
|
|
305
477
|
featureCoverage: z.object({ featureId: z.string(), coverage: z.enum(['none', 'partial', 'covered']) }).optional(),
|
|
306
478
|
},
|
|
307
|
-
outputSchema: { ok: z.boolean(), appMapUri: z.string().optional(), applied: z.array(z.string()).optional() },
|
|
308
479
|
}, async ({ projectRoot, sessionId, note, testCases, automationSuite, environment, featureCoverage }) => {
|
|
309
480
|
const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
|
|
310
481
|
if (!root)
|
|
311
|
-
return qaError({
|
|
482
|
+
return qaError({
|
|
483
|
+
what: 'Could not resolve a project root',
|
|
484
|
+
changedState: false,
|
|
485
|
+
retrySafe: true,
|
|
486
|
+
nextSteps: ['Pass projectRoot or sessionId.'],
|
|
487
|
+
clientHint: hint,
|
|
488
|
+
});
|
|
312
489
|
remember(root);
|
|
313
490
|
const map = readExistingMap(root);
|
|
314
491
|
if (!map)
|
|
315
|
-
return qaError({
|
|
492
|
+
return qaError({
|
|
493
|
+
what: 'No app map yet',
|
|
494
|
+
changedState: false,
|
|
495
|
+
retrySafe: true,
|
|
496
|
+
nextSteps: ['Run qa_app_map_build first.'],
|
|
497
|
+
failureCode: 'NO_APP_MAP',
|
|
498
|
+
});
|
|
316
499
|
const at = nowIso();
|
|
317
500
|
const applied = [];
|
|
318
501
|
if (note) {
|
|
@@ -336,7 +519,10 @@ export function registerAppMap(server, sessions) {
|
|
|
336
519
|
Object.assign(existing, automationSuite);
|
|
337
520
|
else
|
|
338
521
|
map.automation.suites.push(automationSuite);
|
|
339
|
-
addProvenance(map, makeProvenance('test_case', at, `Automation suite ${automationSuite.name} linked`, {
|
|
522
|
+
addProvenance(map, makeProvenance('test_case', at, `Automation suite ${automationSuite.name} linked`, {
|
|
523
|
+
targetType: 'test',
|
|
524
|
+
refs: [automationSuite.path],
|
|
525
|
+
}));
|
|
340
526
|
applied.push('automationSuite');
|
|
341
527
|
}
|
|
342
528
|
if (environment) {
|
|
@@ -346,13 +532,23 @@ export function registerAppMap(server, sessions) {
|
|
|
346
532
|
if (featureCoverage) {
|
|
347
533
|
const f = map.features.find((x) => x.id === featureCoverage.featureId);
|
|
348
534
|
if (!f)
|
|
349
|
-
return qaError({
|
|
535
|
+
return qaError({
|
|
536
|
+
what: `Unknown featureId ${featureCoverage.featureId}`,
|
|
537
|
+
changedState: false,
|
|
538
|
+
retrySafe: true,
|
|
539
|
+
nextSteps: ['List ids with qa_app_map_read { section:"features" }.'],
|
|
540
|
+
});
|
|
350
541
|
f.testCoverage = featureCoverage.coverage;
|
|
351
542
|
addProvenance(map, makeProvenance('user_note', at, `coverage(${f.id})=${featureCoverage.coverage}`, { targetType: 'feature', targetId: f.id }));
|
|
352
543
|
applied.push('featureCoverage');
|
|
353
544
|
}
|
|
354
545
|
if (!applied.length)
|
|
355
|
-
return qaError({
|
|
546
|
+
return qaError({
|
|
547
|
+
what: 'No update fields provided',
|
|
548
|
+
changedState: false,
|
|
549
|
+
retrySafe: true,
|
|
550
|
+
nextSteps: ['Pass at least one of: note, testCases, automationSuite, environment, featureCoverage.'],
|
|
551
|
+
});
|
|
356
552
|
map.updatedAt = at;
|
|
357
553
|
map.coverage.staleTests = map.testSuite.cases.filter((c) => c.stale).length;
|
|
358
554
|
recomputeConfidence(map);
|
|
@@ -360,56 +556,6 @@ export function registerAppMap(server, sessions) {
|
|
|
360
556
|
saveIndexes(root, loadCodeIndex(root), map.features);
|
|
361
557
|
return qaOk({ appMapUri: save.resourceUri, applied }, `app map updated: ${applied.join(', ')}\nappMapUri: ${save.resourceUri}`);
|
|
362
558
|
});
|
|
363
|
-
// ------------------------------------------------------------------- diff
|
|
364
|
-
server.registerTool('qa_app_map_diff', {
|
|
365
|
-
title: 'Diff two app map snapshots',
|
|
366
|
-
description: 'Compare two App Knowledge Map snapshots and report added/removed/changed screens, feature coverage changes, locator-readiness changes, stale test cases, and new untested code areas. With no paths, compares the most recent history snapshot (baseline) against the current map.',
|
|
367
|
-
inputSchema: {
|
|
368
|
-
projectRoot: z.string().optional(),
|
|
369
|
-
sessionId: z.string().optional(),
|
|
370
|
-
baseline: z.string().optional().describe('Path to a baseline app-map.json (or history snapshot). Defaults to the latest history snapshot.'),
|
|
371
|
-
current: z.string().optional().describe('Path to the current app-map.json. Defaults to .swipium/app-map.json.'),
|
|
372
|
-
},
|
|
373
|
-
outputSchema: { ok: z.boolean(), summary: z.string().optional() },
|
|
374
|
-
}, async ({ projectRoot, sessionId, baseline, current }) => {
|
|
375
|
-
const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
|
|
376
|
-
if (!root)
|
|
377
|
-
return qaError({ what: 'Could not resolve a project root', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot or sessionId.'], clientHint: hint });
|
|
378
|
-
remember(root);
|
|
379
|
-
const readMap = (p) => {
|
|
380
|
-
if (!existsSync(p))
|
|
381
|
-
return null;
|
|
382
|
-
try {
|
|
383
|
-
const raw = JSON.parse(readFileSync(p, 'utf8'));
|
|
384
|
-
return migrateAppMap(raw, fallbackProject(root), nowIso()).map;
|
|
385
|
-
}
|
|
386
|
-
catch {
|
|
387
|
-
return null;
|
|
388
|
-
}
|
|
389
|
-
};
|
|
390
|
-
const currentMap = current ? readMap(current) : readExistingMap(root);
|
|
391
|
-
if (!currentMap)
|
|
392
|
-
return qaError({ what: 'No current app map to diff', changedState: false, retrySafe: true, nextSteps: ['Run qa_app_map_build first, or pass current=.'], failureCode: 'NO_APP_MAP' });
|
|
393
|
-
let baselineMap = baseline ? readMap(baseline) : null;
|
|
394
|
-
if (!baselineMap && !baseline) {
|
|
395
|
-
const { appMapHistoryDir } = await import('../appMap/store.js');
|
|
396
|
-
const dir = appMapHistoryDir(root);
|
|
397
|
-
try {
|
|
398
|
-
const { readdirSync } = await import('node:fs');
|
|
399
|
-
const snaps = existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith('.json')).sort() : [];
|
|
400
|
-
const prev = snaps.length >= 2 ? snaps[snaps.length - 2] : snaps[0];
|
|
401
|
-
if (prev)
|
|
402
|
-
baselineMap = readMap(`${dir}/${prev}`);
|
|
403
|
-
}
|
|
404
|
-
catch {
|
|
405
|
-
/* none */
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
if (!baselineMap)
|
|
409
|
-
return qaError({ what: 'No baseline snapshot available to diff against', changedState: false, retrySafe: true, nextSteps: ['Build the map at least twice, or pass baseline=path-to-app-map.json.'] });
|
|
410
|
-
const diff = diffAppMaps(baselineMap, currentMap);
|
|
411
|
-
return qaOk({ ...diff, appMapUri: appMapResourceUri(root) }, `app map diff: ${diff.summary}`);
|
|
412
|
-
});
|
|
413
559
|
}
|
|
414
560
|
/** Resolve a feature/screen/test-suite/full app-map resource read for the MCP resource handler.
|
|
415
561
|
* Routes through loadAppMap() so an older on-disk shape is MIGRATED before it is served (Fix 8). */
|