@aarwitz/tapp 0.15.0 → 0.16.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/AGENTS.md +4 -4
- package/Harness/OCQAHarness/AppDelegate.swift +1 -1
- package/Harness/OCQAHarness.xcodeproj/project.pbxproj +2 -2
- package/Harness/OCQAHarnessUITests/ExplorerTests.swift +2 -2
- package/Harness/generate-harness-xcodeproj.rb +3 -3
- package/README.md +17 -15
- package/bin/tapp.js +28 -28
- package/browser/app.js +2 -2
- package/browser/index.html +1 -1
- package/docs/BROWSER-PRODUCT.md +2 -2
- package/docs/PRODUCT-ENGINE.md +7 -6
- package/docs/application-model.md +20 -25
- package/docs/scenarios.md +7 -7
- package/mcp-server/src/application-model.js +32 -26
- package/mcp-server/src/browser-product.js +1 -1
- package/mcp-server/src/ci-report.js +5 -4
- package/mcp-server/src/ci-setup.js +16 -7
- package/mcp-server/src/enrich.js +1 -1
- package/mcp-server/src/index.js +63 -62
- package/mcp-server/src/maintenance-proposal.js +4 -4
- package/mcp-server/src/pr-selection.js +9 -8
- package/mcp-server/src/product-execution.js +1 -1
- package/mcp-server/src/product-operations.js +20 -18
- package/mcp-server/src/project-config.js +8 -5
- package/mcp-server/src/project-paths.js +32 -0
- package/mcp-server/src/report.js +3 -3
- package/mcp-server/src/task-runtime.js +14 -10
- package/package.json +15 -3
- package/scripts/ci-gate.sh +15 -10
- package/scripts/flow_ai_judge.py +1 -1
- package/scripts/flow_lib.py +1 -1
- package/scripts/quick-capture.sh +15 -14
- package/scripts/run-flow.sh +6 -6
- package/scripts/android-corpus-e2e.sh +0 -30
- package/scripts/cleanup-xcode.sh +0 -157
- package/scripts/corpus-apps.txt +0 -9
- package/scripts/corpus-sweep.sh +0 -121
- package/scripts/coverage-eval.sh +0 -92
- package/scripts/coverage_eval_parse.py +0 -95
- package/scripts/deploy-and-build.sh +0 -99
- package/scripts/mutation-recall-desktop.sh +0 -186
- package/scripts/mutation-recall.sh +0 -121
- package/scripts/mutation_lib.py +0 -128
- package/scripts/mutation_operators.py +0 -144
- package/scripts/validation-matrix.sh +0 -146
- package/scripts/vision-fp-eval.sh +0 -206
- package/scripts/vision_escalation_responder.py +0 -147
- package/scripts/vision_fp_probe.py +0 -221
package/mcp-server/src/index.js
CHANGED
|
@@ -13,15 +13,16 @@ import {
|
|
|
13
13
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
14
14
|
|
|
15
15
|
import { parseOcqaMarkers, buildQaReport, computeRegression } from "./report.js";
|
|
16
|
+
import { existingProjectArtifactPath, projectArtifactDirectory } from "./project-paths.js";
|
|
16
17
|
|
|
17
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
18
19
|
const __dirname = path.dirname(__filename);
|
|
19
20
|
const repoRoot = path.resolve(__dirname, "../..");
|
|
20
21
|
const scriptsDir = path.join(repoRoot, "scripts");
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
const
|
|
24
|
-
const capturesDir =
|
|
22
|
+
// TAPP_HOME (set by the `tapp` CLI when installed) redirects writable output to a user directory.
|
|
23
|
+
// The old alias remains a read-only fallback; unset repository development stays local.
|
|
24
|
+
const tappHome = (process.env.TAPP_HOME || process.env.AUTOTAP_HOME || "").trim();
|
|
25
|
+
const capturesDir = tappHome ? path.join(tappHome, "captures") : path.join(repoRoot, "captures");
|
|
25
26
|
const MAX_OUTPUT_CHARS = 60_000;
|
|
26
27
|
const requiredAuthToken = (process.env.TAPP_MCP_TOKEN || process.env.AUTOTAP_MCP_TOKEN || "").trim();
|
|
27
28
|
|
|
@@ -83,7 +84,7 @@ function ensureAuthorized(args = {}) {
|
|
|
83
84
|
const provided = typeof args.authToken === "string" ? args.authToken.trim() : "";
|
|
84
85
|
if (provided !== requiredAuthToken) {
|
|
85
86
|
return errorResult("Unauthorized", {
|
|
86
|
-
reason: "Provide valid authToken when
|
|
87
|
+
reason: "Provide valid authToken when TAPP_MCP_TOKEN is set",
|
|
87
88
|
});
|
|
88
89
|
}
|
|
89
90
|
|
|
@@ -339,7 +340,7 @@ export async function buildAppForSim({ dir, container, scheme, configuration = "
|
|
|
339
340
|
};
|
|
340
341
|
}
|
|
341
342
|
}
|
|
342
|
-
const derived = path.join(
|
|
343
|
+
const derived = path.join(tappHome || os.tmpdir(), "app-builds", schemeName.replace(/[^a-zA-Z0-9]/g, "_"));
|
|
343
344
|
const build = await runCommand(
|
|
344
345
|
"xcodebuild",
|
|
345
346
|
[
|
|
@@ -918,7 +919,7 @@ export async function saveInteractiveSessionFlow({ projectDir, name, addFinalAss
|
|
|
918
919
|
steps,
|
|
919
920
|
};
|
|
920
921
|
const slug = flowName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "flow";
|
|
921
|
-
const dir = path.join(root, ".
|
|
922
|
+
const dir = path.join(root, ".tapp", "flows");
|
|
922
923
|
const outPath = path.join(dir, `${slug}.yml`);
|
|
923
924
|
if (fs.existsSync(outPath) && !replace) {
|
|
924
925
|
const error = new Error(`Flow '${path.relative(root, outPath)}' already exists. Choose another name or explicitly replace it.`);
|
|
@@ -1065,7 +1066,7 @@ export async function captureScreenshotImage(maxWidth) {
|
|
|
1065
1066
|
// change data-handling behavior. A subscription token is an explicit tapp choice, and
|
|
1066
1067
|
// explicitly-invoked AI tools (tapp_flow_generate, assert_ai) carry their own consent.
|
|
1067
1068
|
export function remoteAiOptedIn(env = process.env) {
|
|
1068
|
-
if ((env.
|
|
1069
|
+
if ((env.TAPP_SUBSCRIPTION_TOKEN || env.AUTOTAP_SUBSCRIPTION_TOKEN || "").trim()) return true;
|
|
1069
1070
|
return ["1", "true", "yes"].includes(String(env.TAPP_ENABLE_REMOTE_AI || "").trim().toLowerCase());
|
|
1070
1071
|
}
|
|
1071
1072
|
|
|
@@ -1077,9 +1078,9 @@ export function isInsideDir(root, p) {
|
|
|
1077
1078
|
}
|
|
1078
1079
|
|
|
1079
1080
|
function resolveModelBackend() {
|
|
1080
|
-
const token = (process.env.AUTOTAP_SUBSCRIPTION_TOKEN || "").trim();
|
|
1081
|
+
const token = (process.env.TAPP_SUBSCRIPTION_TOKEN || process.env.AUTOTAP_SUBSCRIPTION_TOKEN || "").trim();
|
|
1081
1082
|
if (token) {
|
|
1082
|
-
const base = (process.env.AUTOTAP_PROXY_URL || "http://localhost:8787").replace(/\/$/, "");
|
|
1083
|
+
const base = (process.env.TAPP_PROXY_URL || process.env.AUTOTAP_PROXY_URL || "http://localhost:8787").replace(/\/$/, "");
|
|
1083
1084
|
const url = base.endsWith("/v1/messages") ? base : base + "/v1/messages";
|
|
1084
1085
|
return { url, headers: { authorization: `Bearer ${token}`, "content-type": "application/json" } };
|
|
1085
1086
|
}
|
|
@@ -1091,7 +1092,7 @@ function resolveModelBackend() {
|
|
|
1091
1092
|
}
|
|
1092
1093
|
|
|
1093
1094
|
async function callModel(backend, { system, userText, model, maxTokens = 1500 }) {
|
|
1094
|
-
const body = JSON.stringify({ model: model || process.env.AUTOTAP_FLOW_MODEL || "claude-sonnet-4-6", max_tokens: maxTokens, system, messages: [{ role: "user", content: userText }] });
|
|
1095
|
+
const body = JSON.stringify({ model: model || process.env.TAPP_FLOW_MODEL || process.env.AUTOTAP_FLOW_MODEL || "claude-sonnet-4-6", max_tokens: maxTokens, system, messages: [{ role: "user", content: userText }] });
|
|
1095
1096
|
const res = await fetch(backend.url, { method: "POST", headers: backend.headers, body });
|
|
1096
1097
|
if (!res.ok) return { error: `model HTTP ${res.status}: ${(await res.text()).slice(0, 300)}` };
|
|
1097
1098
|
const data = await res.json();
|
|
@@ -1589,7 +1590,7 @@ export async function runQaIos({ bundleId, maxActions, timeout, args = {}, onPro
|
|
|
1589
1590
|
export async function runInitExploration({
|
|
1590
1591
|
projectDir,
|
|
1591
1592
|
platform,
|
|
1592
|
-
outDir = ".
|
|
1593
|
+
outDir = ".tapp",
|
|
1593
1594
|
url = "",
|
|
1594
1595
|
target = "",
|
|
1595
1596
|
bundleId = "",
|
|
@@ -1610,7 +1611,7 @@ export async function runInitExploration({
|
|
|
1610
1611
|
catch { return { error: `Repository directory not found: ${projectDir || process.cwd()}` }; }
|
|
1611
1612
|
const selected = String(platform || (url ? "web" : appId || apkPath ? "android" : "ios")).toLowerCase();
|
|
1612
1613
|
if (!["ios", "android", "web"].includes(selected)) return { error: "platform must be ios|android|web" };
|
|
1613
|
-
const mapPath = path.resolve(root, outDir, "ui-map.json");
|
|
1614
|
+
const mapPath = path.resolve(root, projectArtifactDirectory(root, outDir), "ui-map.json");
|
|
1614
1615
|
if (!isInsideDir(root, mapPath)) return { error: "UI Map output must remain inside the repository" };
|
|
1615
1616
|
|
|
1616
1617
|
let resolvedTarget = "";
|
|
@@ -1832,7 +1833,7 @@ export async function startManagedWebTarget({ root, requestedTarget = "", timeou
|
|
|
1832
1833
|
command = process.execPath;
|
|
1833
1834
|
startArgs = [path.join(__dirname, "static-server.js"), startDir, String(port)];
|
|
1834
1835
|
}
|
|
1835
|
-
const logDir = path.join(
|
|
1836
|
+
const logDir = path.join(tappHome || os.tmpdir(), "init-runtime");
|
|
1836
1837
|
fs.mkdirSync(logDir, { recursive: true });
|
|
1837
1838
|
const logPath = path.join(logDir, `web-${process.pid}-${Date.now()}.log`);
|
|
1838
1839
|
const child = spawn(command, startArgs, {
|
|
@@ -1938,7 +1939,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1938
1939
|
properties: {
|
|
1939
1940
|
authToken: {
|
|
1940
1941
|
type: "string",
|
|
1941
|
-
description: "Required when
|
|
1942
|
+
description: "Required when TAPP_MCP_TOKEN is set",
|
|
1942
1943
|
},
|
|
1943
1944
|
projectDir: { type: "string", description: "Repo/dir to search for the .xcworkspace/.xcodeproj (default: cwd)" },
|
|
1944
1945
|
scheme: { type: "string", description: "Scheme to build (default: auto-detected)" },
|
|
@@ -1956,7 +1957,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1956
1957
|
properties: {
|
|
1957
1958
|
authToken: {
|
|
1958
1959
|
type: "string",
|
|
1959
|
-
description: "Required when
|
|
1960
|
+
description: "Required when TAPP_MCP_TOKEN is set",
|
|
1960
1961
|
},
|
|
1961
1962
|
mode: {
|
|
1962
1963
|
type: "string",
|
|
@@ -2075,7 +2076,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2075
2076
|
inputSchema: {
|
|
2076
2077
|
type: "object",
|
|
2077
2078
|
properties: {
|
|
2078
|
-
authToken: { type: "string", description: "Required when
|
|
2079
|
+
authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
|
|
2079
2080
|
appBundleId: { type: "string", description: "iOS: bundle id of the installed app to test, e.g. com.acme.app. Provide exactly one of appBundleId | url." },
|
|
2080
2081
|
androidAppId: { type: "string", description: "Android: application id installed on a connected emulator/device, e.g. com.acme.app." },
|
|
2081
2082
|
apkPath: { type: "string", description: "Android: optional APK to install before testing." },
|
|
@@ -2137,7 +2138,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2137
2138
|
inputSchema: {
|
|
2138
2139
|
type: "object",
|
|
2139
2140
|
properties: {
|
|
2140
|
-
authToken: { type: "string", description: "Required when
|
|
2141
|
+
authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
|
|
2141
2142
|
operation: { type: "string", enum: ["inspect", "write", "refresh", "explore"], default: "inspect" },
|
|
2142
2143
|
projectDir: { type: "string", description: "Repo-relative project root; defaults to the MCP workspace root" },
|
|
2143
2144
|
platform: { type: "string", enum: ["ios", "android", "web"], description: "Optional target filter" },
|
|
@@ -2152,7 +2153,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2152
2153
|
testEmail: { type: "string", description: "Explore: actor/login email; never persisted in the model" },
|
|
2153
2154
|
testPassword: { type: "string", description: "Explore: actor/login password; never persisted in the model" },
|
|
2154
2155
|
maxContracts: { type: "integer", minimum: 1, maximum: 50, default: 15 },
|
|
2155
|
-
outDir: { type: "string", description: "Repo-relative artifact directory; default .
|
|
2156
|
+
outDir: { type: "string", description: "Repo-relative artifact directory; default .tapp" },
|
|
2156
2157
|
},
|
|
2157
2158
|
},
|
|
2158
2159
|
},
|
|
@@ -2160,11 +2161,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2160
2161
|
name: "tapp_actor_config",
|
|
2161
2162
|
title: "Inspect or configure named test actors without storing credential values",
|
|
2162
2163
|
description:
|
|
2163
|
-
"Manage the repository-native .
|
|
2164
|
+
"Manage the repository-native .tapp/project.json actor/session contract used by init, release-contract generation, and CI. `read` is inspect-only. `set` writes an explicit actor role, isolation/provisioning policy, and credential-name to environment-variable-name bindings. The tool never accepts, returns, or persists credential values and never overwrites an actor unless replace is explicit.",
|
|
2164
2165
|
inputSchema: {
|
|
2165
2166
|
type: "object",
|
|
2166
2167
|
properties: {
|
|
2167
|
-
authToken: { type: "string", description: "Required when
|
|
2168
|
+
authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
|
|
2168
2169
|
operation: { type: "string", enum: ["read", "set"], default: "read" },
|
|
2169
2170
|
projectDir: { type: "string", description: "Repo-relative project root; defaults to the MCP workspace root" },
|
|
2170
2171
|
name: { type: "string", description: "Set: stable actor name" },
|
|
@@ -2180,14 +2181,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2180
2181
|
name: "tapp_release_plan",
|
|
2181
2182
|
title: "Inspect or explicitly review a Tapp release plan",
|
|
2182
2183
|
description:
|
|
2183
|
-
"Read the repository-native release plan, apply explicit approve/reject/defer decisions, generate grounded Task/contract drafts, deterministically validate drafts on a real target, or explicitly promote fully replay-validated drafts into reviewed repository-native artifacts. Review changes only decision metadata. Generation writes under .
|
|
2184
|
+
"Read the repository-native release plan, apply explicit approve/reject/defer decisions, generate grounded Task/contract drafts, deterministically validate drafts on a real target, or explicitly promote fully replay-validated drafts into reviewed repository-native artifacts. Review changes only decision metadata. Generation writes under .tapp/proposals, never overwrites, never invokes AI, and remains untrusted until real deterministic replay passes. Web validation can build/start/stop the detected managed target when url is omitted.",
|
|
2184
2185
|
inputSchema: {
|
|
2185
2186
|
type: "object",
|
|
2186
2187
|
properties: {
|
|
2187
|
-
authToken: { type: "string", description: "Required when
|
|
2188
|
+
authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
|
|
2188
2189
|
operation: { type: "string", enum: ["read", "review", "generate", "validate", "promote"], default: "read" },
|
|
2189
|
-
planPath: { type: "string", description: "Repo-relative plan path; default .
|
|
2190
|
-
projectDir: { type: "string", description: "Generate: repo-relative project root containing the scoped .
|
|
2190
|
+
planPath: { type: "string", description: "Repo-relative plan path; default .tapp/release-plan.json" },
|
|
2191
|
+
projectDir: { type: "string", description: "Generate: repo-relative project root containing the scoped .tapp Task directories" },
|
|
2191
2192
|
approve: { type: "array", items: { type: "string" }, description: "Plan item ids or names to approve" },
|
|
2192
2193
|
reject: { type: "array", items: { type: "string" }, description: "Plan item ids or names to reject" },
|
|
2193
2194
|
defer: { type: "array", items: { type: "string" }, description: "Plan item ids or names to defer" },
|
|
@@ -2211,14 +2212,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2211
2212
|
inputSchema: {
|
|
2212
2213
|
type: "object",
|
|
2213
2214
|
properties: {
|
|
2214
|
-
authToken: { type: "string", description: "Required when
|
|
2215
|
+
authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
|
|
2215
2216
|
operation: { type: "string", enum: ["inspect", "install", "baseline"], default: "inspect" },
|
|
2216
2217
|
projectDir: { type: "string", description: "Repo-relative project root; defaults to the MCP workspace root" },
|
|
2217
|
-
modelPath: { type: "string", description: "Repo-relative application model path; defaults to <projectDir>/.
|
|
2218
|
+
modelPath: { type: "string", description: "Repo-relative application model path; defaults to <projectDir>/.tapp/application-model.json" },
|
|
2218
2219
|
actionRef: { type: "string", description: "GitHub Action reference owner/repository@release-tag-or-sha; defaults to the current Tapp release tag" },
|
|
2219
2220
|
defaultBranch: { type: "string", default: "main" },
|
|
2220
2221
|
workflowPath: { type: "string", description: "Install: project-relative output; default .github/workflows/tapp.yml" },
|
|
2221
|
-
manifestPath: { type: "string", description: "Install: project-relative output; default .
|
|
2222
|
+
manifestPath: { type: "string", description: "Install: project-relative output; default .tapp/ci.json" },
|
|
2222
2223
|
allowUnresolved: { type: "boolean", default: false, description: "Permit writing a draft whose manifest names unresolved target configuration" },
|
|
2223
2224
|
replace: { type: "boolean", default: false, description: "Explicitly replace an existing generated workflow/manifest or target baseline" },
|
|
2224
2225
|
reportPath: { type: "string", description: "Baseline: repo-relative successful conclusive portable-gate JSON report" },
|
|
@@ -2237,10 +2238,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2237
2238
|
inputSchema: {
|
|
2238
2239
|
type: "object",
|
|
2239
2240
|
properties: {
|
|
2240
|
-
authToken: { type: "string", description: "Required when
|
|
2241
|
+
authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
|
|
2241
2242
|
operation: { type: "string", enum: ["read", "build", "diff"], default: "read" },
|
|
2242
2243
|
captureId: { type: "string", description: "Read/build from this Tapp capture's ocqa-markers.txt/ui-map.json" },
|
|
2243
|
-
mapPath: { type: "string", description: "Repo-relative UI Map path for read, or build output (default .
|
|
2244
|
+
mapPath: { type: "string", description: "Repo-relative UI Map path for read, or build output (default .tapp/ui-map.json)" },
|
|
2244
2245
|
markersPath: { type: "string", description: "Repo-relative OCQA markers path for build when captureId is not supplied" },
|
|
2245
2246
|
beforePath: { type: "string", description: "Repo-relative baseline UI Map for diff" },
|
|
2246
2247
|
afterPath: { type: "string", description: "Repo-relative current UI Map for diff" },
|
|
@@ -2255,15 +2256,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2255
2256
|
name: "tapp_task",
|
|
2256
2257
|
title: "Inspect, validate, or compile a reusable deterministic Task",
|
|
2257
2258
|
description:
|
|
2258
|
-
"Work with repository-native compositional Tasks in .
|
|
2259
|
+
"Work with repository-native compositional Tasks in .tapp/tasks. Tasks define inputs, outputs, pre/postconditions, platform implementations, and the UI Map states/transitions they cover. " +
|
|
2259
2260
|
"Validation is deterministic and can ground selectors/coverage against ui-map.json. Compilation expands a Task into the shared keyless Flow contract with reviewable Task provenance; pass that returned flow to tapp_flow_run to replay it. No AI or API key is used.",
|
|
2260
2261
|
inputSchema: {
|
|
2261
2262
|
type: "object",
|
|
2262
2263
|
required: ["taskPath"],
|
|
2263
2264
|
properties: {
|
|
2264
|
-
authToken: { type: "string", description: "Required when
|
|
2265
|
+
authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
|
|
2265
2266
|
operation: { type: "string", enum: ["read", "validate", "compile"], default: "validate" },
|
|
2266
|
-
taskPath: { type: "string", description: "Repo-relative .
|
|
2267
|
+
taskPath: { type: "string", description: "Repo-relative .tapp/tasks/*.yml|json file" },
|
|
2267
2268
|
platform: { type: "string", enum: ["ios", "android", "web"], description: "Implementation to validate/compile" },
|
|
2268
2269
|
inputs: { type: "object", additionalProperties: { type: "string" }, description: "Task inputs for compile. Secret inputs must be environment placeholders such as $TEST_PASSWORD, never plaintext." },
|
|
2269
2270
|
mapPath: { type: "string", description: "Optional repo-relative UI Map v1 used to ground states, edges, and semantic controls" },
|
|
@@ -2276,15 +2277,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2276
2277
|
name: "tapp_release_contract",
|
|
2277
2278
|
title: "Inspect, validate, compile, or run a release contract",
|
|
2278
2279
|
description:
|
|
2279
|
-
"Work with repository-native TypeScript release contracts in .
|
|
2280
|
+
"Work with repository-native TypeScript release contracts in .tapp/contracts. Contracts express business guarantees through reusable Tasks, named actors, exact/eventual expectations, criticality, policy, and UI Map coverage. " +
|
|
2280
2281
|
"Compilation targets the same deterministic Flow/Scenario evidence contract; ordinary run is keyless and never invokes a model. Multi-actor isolated replay is currently web-only.",
|
|
2281
2282
|
inputSchema: {
|
|
2282
2283
|
type: "object",
|
|
2283
2284
|
required: ["contractPath"],
|
|
2284
2285
|
properties: {
|
|
2285
|
-
authToken: { type: "string", description: "Required when
|
|
2286
|
+
authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
|
|
2286
2287
|
operation: { type: "string", enum: ["read", "validate", "compile", "run"], default: "validate" },
|
|
2287
|
-
contractPath: { type: "string", description: "Repo-relative .
|
|
2288
|
+
contractPath: { type: "string", description: "Repo-relative .tapp/contracts/*.contract.ts file" },
|
|
2288
2289
|
platform: { type: "string", enum: ["ios", "android", "web"], description: "Target platform; optional when the contract declares exactly one" },
|
|
2289
2290
|
mapPath: { type: "string", description: "Optional repo-relative UI Map v1 for coverage grounding" },
|
|
2290
2291
|
updateMap: { type: "boolean", default: false, description: "Explicitly add the reviewed contract coverage to mapPath" },
|
|
@@ -2305,7 +2306,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2305
2306
|
inputSchema: {
|
|
2306
2307
|
type: "object",
|
|
2307
2308
|
properties: {
|
|
2308
|
-
authToken: { type: "string", description: "Required when
|
|
2309
|
+
authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
|
|
2309
2310
|
operation: { type: "string", enum: ["plan", "adopt"], default: "plan" },
|
|
2310
2311
|
changedFiles: {
|
|
2311
2312
|
type: "array", minItems: 1,
|
|
@@ -2326,10 +2327,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2326
2327
|
},
|
|
2327
2328
|
projectDir: { type: "string", description: "Repo-relative project root; defaults to the MCP workspace root" },
|
|
2328
2329
|
platform: { type: "string", enum: ["ios", "android", "web"], description: "Optional platform filter" },
|
|
2329
|
-
mapPath: { type: "string", description: "Project-relative UI Map; defaults to .
|
|
2330
|
+
mapPath: { type: "string", description: "Project-relative UI Map; defaults to .tapp/ui-map.json" },
|
|
2330
2331
|
prPlanPath: { type: "string", description: "Adopt: project-relative executed PR plan containing conclusive exploration evidence" },
|
|
2331
2332
|
item: { type: "string", description: "Adopt: stable exploration target id whose reviewable proposal should be appended" },
|
|
2332
|
-
releasePlanPath: { type: "string", description: "Adopt: project-relative target; defaults to .
|
|
2333
|
+
releasePlanPath: { type: "string", description: "Adopt: project-relative target; defaults to .tapp/release-plan.json" },
|
|
2333
2334
|
},
|
|
2334
2335
|
},
|
|
2335
2336
|
},
|
|
@@ -2350,13 +2351,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2350
2351
|
inputSchema: {
|
|
2351
2352
|
type: "object",
|
|
2352
2353
|
properties: {
|
|
2353
|
-
authToken: { type: "string", description: "Required when
|
|
2354
|
+
authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
|
|
2354
2355
|
flow: {
|
|
2355
2356
|
type: "object",
|
|
2356
2357
|
description:
|
|
2357
2358
|
"Inline Flow: {name, app, steps:[...], vars?}. Example: {name:'login', app:'com.acme.app', steps:[{tap:'Sign In'}, {type:{field:'Email', value:'$TEST_EMAIL'}}, {tap:'Continue'}, {assert_screen:'Home'}]}",
|
|
2358
2359
|
},
|
|
2359
|
-
flowPath: { type: "string", description: "Alternative to `flow`: repo-relative path to a .yml/.json Flow (e.g. .
|
|
2360
|
+
flowPath: { type: "string", description: "Alternative to `flow`: repo-relative path to a .yml/.json Flow (e.g. .tapp/flows/login.yml)" },
|
|
2360
2361
|
platform: { type: "string", enum: ["ios", "web", "android"], description: "Overrides Flow platform detection" },
|
|
2361
2362
|
appBundleId: { type: "string", description: "iOS: overrides the Flow's `app:` field" },
|
|
2362
2363
|
androidAppId: { type: "string", description: "Android: overrides the Flow's `app:` field" },
|
|
@@ -2379,7 +2380,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2379
2380
|
inputSchema: {
|
|
2380
2381
|
type: "object",
|
|
2381
2382
|
properties: {
|
|
2382
|
-
authToken: { type: "string", description: "Required when
|
|
2383
|
+
authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
|
|
2383
2384
|
scenario: { type: "object", description: "Inline Scenario with {kind:'scenario', platform:'web', actors, steps, setup?, teardown?}" },
|
|
2384
2385
|
scenarioPath: { type: "string", description: "Repo-relative path to a .yml/.json Scenario" },
|
|
2385
2386
|
url: { type: "string", description: "Override the Scenario's web URL" },
|
|
@@ -2394,13 +2395,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2394
2395
|
"Write a deterministic E2E Flow from a natural-language goal (e.g. 'sign in and open Settings'), " +
|
|
2395
2396
|
"GROUNDED in the app's real screens so it can't invent steps. Tapp explores the app to build a " +
|
|
2396
2397
|
"screen/control map (or reuses a recent run via captureId), then a model authors a Flow using only " +
|
|
2397
|
-
"screens/controls that were actually observed. Saves it to .
|
|
2398
|
+
"screens/controls that were actually observed. Saves it to .tapp/flows/<name>.yml and returns the " +
|
|
2398
2399
|
"YAML for review (optionally runs it). Needs a model backend (Tapp subscription token or " +
|
|
2399
2400
|
"ANTHROPIC_API_KEY). Use this to bootstrap a test you then refine; use tapp_flow_run to replay it.",
|
|
2400
2401
|
inputSchema: {
|
|
2401
2402
|
type: "object",
|
|
2402
2403
|
properties: {
|
|
2403
|
-
authToken: { type: "string", description: "Required when
|
|
2404
|
+
authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
|
|
2404
2405
|
goal: { type: "string", description: "What the test should do, in plain English (e.g. 'sign in with test creds and reach the dashboard')" },
|
|
2405
2406
|
appBundleId: { type: "string", description: "Bundle id of the installed app to author against" },
|
|
2406
2407
|
captureId: { type: "string", description: "Reuse this capture's grounding instead of exploring (from a prior run_qa, faster)" },
|
|
@@ -2418,13 +2419,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2418
2419
|
description:
|
|
2419
2420
|
"Save what you've done in the CURRENT interactive session as a reusable, deterministic Flow " +
|
|
2420
2421
|
"(record-by-doing). Every successful tapp_session_act (tap/type/swipe/back) is recorded; this " +
|
|
2421
|
-
"writes them to .
|
|
2422
|
+
"writes them to .tapp/flows/<name>.yml with wait_for steps auto-inserted on screen changes and a " +
|
|
2422
2423
|
"final assert_screen checkpoint. Typed credentials are templated to $TEST_EMAIL/$TEST_PASSWORD so the " +
|
|
2423
2424
|
"flow is shareable. The saved flow replays with tapp_flow_run. Do it once → it's a test.",
|
|
2424
2425
|
inputSchema: {
|
|
2425
2426
|
type: "object",
|
|
2426
2427
|
properties: {
|
|
2427
|
-
authToken: { type: "string", description: "Required when
|
|
2428
|
+
authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
|
|
2428
2429
|
name: { type: "string", description: "Human name for the flow, e.g. 'Sign in and reach Home'" },
|
|
2429
2430
|
addFinalAssertion: { type: "boolean", default: true, description: "Append assert_screen for the final screen as a checkpoint" },
|
|
2430
2431
|
replace: { type: "boolean", default: false, description: "Explicitly replace a Flow with the same generated filename. Existing Flows are preserved by default." },
|
|
@@ -2442,7 +2443,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2442
2443
|
inputSchema: {
|
|
2443
2444
|
type: "object",
|
|
2444
2445
|
properties: {
|
|
2445
|
-
authToken: { type: "string", description: "Required when
|
|
2446
|
+
authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
|
|
2446
2447
|
appBundleId: { type: "string", description: "iOS bundle id of the installed app" },
|
|
2447
2448
|
androidAppId: { type: "string", description: "Android application id of the installed app" },
|
|
2448
2449
|
androidSerial: { type: "string", description: "Android adb device serial" },
|
|
@@ -2459,7 +2460,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2459
2460
|
inputSchema: {
|
|
2460
2461
|
type: "object",
|
|
2461
2462
|
properties: {
|
|
2462
|
-
authToken: { type: "string", description: "Required when
|
|
2463
|
+
authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
|
|
2463
2464
|
maxWidth: { type: "integer", minimum: 200, maximum: 1400, default: 700, description: "Max image width in px (downscaled to keep payload small)" },
|
|
2464
2465
|
},
|
|
2465
2466
|
},
|
|
@@ -2477,7 +2478,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2477
2478
|
inputSchema: {
|
|
2478
2479
|
type: "object",
|
|
2479
2480
|
properties: {
|
|
2480
|
-
authToken: { type: "string", description: "Required when
|
|
2481
|
+
authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
|
|
2481
2482
|
appBundleId: { type: "string", description: "iOS bundle id of the installed app" },
|
|
2482
2483
|
androidAppId: { type: "string", description: "Android application id of the installed app" },
|
|
2483
2484
|
apkPath: { type: "string", description: "Android APK to install before launch" },
|
|
@@ -2502,7 +2503,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2502
2503
|
inputSchema: {
|
|
2503
2504
|
type: "object",
|
|
2504
2505
|
properties: {
|
|
2505
|
-
authToken: { type: "string", description: "Required when
|
|
2506
|
+
authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
|
|
2506
2507
|
udid: { type: "string", description: "Simulator UDID (from tapp_list_simulators)" },
|
|
2507
2508
|
name: { type: "string", description: "Simulator name, e.g. 'iPhone 16 Pro' (used if udid omitted)" },
|
|
2508
2509
|
},
|
|
@@ -2518,7 +2519,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2518
2519
|
inputSchema: {
|
|
2519
2520
|
type: "object",
|
|
2520
2521
|
properties: {
|
|
2521
|
-
authToken: { type: "string", description: "Required when
|
|
2522
|
+
authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
|
|
2522
2523
|
project: { type: "string", description: "Absolute path to .xcodeproj (use this OR workspace)" },
|
|
2523
2524
|
workspace: { type: "string", description: "Absolute path to .xcworkspace (use this OR project)" },
|
|
2524
2525
|
scheme: { type: "string", description: "Scheme to build" },
|
|
@@ -2542,7 +2543,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2542
2543
|
inputSchema: {
|
|
2543
2544
|
type: "object",
|
|
2544
2545
|
properties: {
|
|
2545
|
-
authToken: { type: "string", description: "Required when
|
|
2546
|
+
authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
|
|
2546
2547
|
appBundleId: { type: "string", description: "Bundle id of the installed app to drive" },
|
|
2547
2548
|
androidAppId: { type: "string", description: "Android application id to drive (alternative to appBundleId)" },
|
|
2548
2549
|
apkPath: { type: "string", description: "Android APK to install before starting" },
|
|
@@ -2571,7 +2572,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2571
2572
|
inputSchema: {
|
|
2572
2573
|
type: "object",
|
|
2573
2574
|
properties: {
|
|
2574
|
-
authToken: { type: "string", description: "Required when
|
|
2575
|
+
authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
|
|
2575
2576
|
action: { type: "string", enum: ["login", "tap", "type", "swipe", "back", "wait", "tree", "screenshot"] },
|
|
2576
2577
|
email: { type: "string", description: "login: email/username to sign in with" },
|
|
2577
2578
|
password: { type: "string", description: "login: password to sign in with" },
|
|
@@ -2592,7 +2593,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2592
2593
|
description: "End the active interactive session (quits the app + harness). Always call this when done.",
|
|
2593
2594
|
inputSchema: {
|
|
2594
2595
|
type: "object",
|
|
2595
|
-
properties: { authToken: { type: "string", description: "Required when
|
|
2596
|
+
properties: { authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" } },
|
|
2596
2597
|
},
|
|
2597
2598
|
},
|
|
2598
2599
|
],
|
|
@@ -2908,7 +2909,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2908
2909
|
if (maxContracts < 1 || maxContracts > 50) return errorResult("maxContracts must be between 1 and 50");
|
|
2909
2910
|
const { initializeProductProject } = await import("./product-operations.js");
|
|
2910
2911
|
try {
|
|
2911
|
-
const outDir = isNonEmptyString(args.outDir) ? args.outDir.trim() : ".
|
|
2912
|
+
const outDir = isNonEmptyString(args.outDir) ? args.outDir.trim() : ".tapp";
|
|
2912
2913
|
const resolvedOut = path.resolve(projectDir, outDir);
|
|
2913
2914
|
if (!isInsideDir(projectDir, resolvedOut)) return errorResult("outDir must be inside projectDir");
|
|
2914
2915
|
const selectedPlatform = isNonEmptyString(args.platform) ? args.platform.trim().toLowerCase()
|
|
@@ -2979,7 +2980,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2979
2980
|
if (unauthorized) return unauthorized;
|
|
2980
2981
|
const operation = String(args.operation || "read").toLowerCase();
|
|
2981
2982
|
if (!["read", "review", "generate", "validate", "promote"].includes(operation)) return errorResult("operation must be read|review|generate|validate|promote");
|
|
2982
|
-
const planPath =
|
|
2983
|
+
const planPath = isNonEmptyString(args.planPath) ? path.resolve(repoRoot, args.planPath.trim()) : existingProjectArtifactPath(repoRoot, "release-plan.json");
|
|
2983
2984
|
if (!isInsideDir(repoRoot, planPath)) return errorResult("planPath must be inside the repo");
|
|
2984
2985
|
if (!fs.existsSync(planPath)) return errorResult("Release plan not found", { planPath });
|
|
2985
2986
|
let plan;
|
|
@@ -3054,7 +3055,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3054
3055
|
if (!["inspect", "install", "baseline"].includes(operation)) return errorResult("operation must be inspect|install|baseline");
|
|
3055
3056
|
const projectDir = isNonEmptyString(args.projectDir) ? path.resolve(repoRoot, args.projectDir.trim()) : repoRoot;
|
|
3056
3057
|
if (!isInsideDir(repoRoot, projectDir) || !fs.existsSync(projectDir) || !fs.statSync(projectDir).isDirectory()) return errorResult("projectDir must be an existing directory inside the repo");
|
|
3057
|
-
const modelPath = isNonEmptyString(args.modelPath) ? path.resolve(repoRoot, args.modelPath.trim()) :
|
|
3058
|
+
const modelPath = isNonEmptyString(args.modelPath) ? path.resolve(repoRoot, args.modelPath.trim()) : existingProjectArtifactPath(projectDir, "application-model.json");
|
|
3058
3059
|
if (!isInsideDir(projectDir, modelPath) || !fs.existsSync(modelPath)) return errorResult("Application model not found inside projectDir; run tapp_init first", { modelPath });
|
|
3059
3060
|
let model;
|
|
3060
3061
|
try { model = JSON.parse(fs.readFileSync(modelPath, "utf8")); }
|
|
@@ -3078,7 +3079,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3078
3079
|
const rendered = prepareProductCi({ projectDir, modelPath, actionRef, defaultBranch });
|
|
3079
3080
|
if (operation === "inspect") return richResult(`🧩 CI plan — ${rendered.manifest.targets.length} target job(s) · ${rendered.manifest.unresolved.length} unresolved · read-only`, rendered);
|
|
3080
3081
|
if (rendered.manifest.unresolved.length && !asBoolean(args.allowUnresolved)) return errorResult("CI workflow not installed because target configuration remains unresolved", { unresolved: rendered.manifest.unresolved, next: "Resolve the application model requirements or explicitly allow an inspect-only draft." });
|
|
3081
|
-
const result = installProductCi({ projectDir, modelPath, actionRef, defaultBranch, workflowPath: isNonEmptyString(args.workflowPath) ? args.workflowPath.trim() : ".github/workflows/tapp.yml", manifestPath: isNonEmptyString(args.manifestPath) ? args.manifestPath.trim() : ".
|
|
3082
|
+
const result = installProductCi({ projectDir, modelPath, actionRef, defaultBranch, workflowPath: isNonEmptyString(args.workflowPath) ? args.workflowPath.trim() : ".github/workflows/tapp.yml", manifestPath: isNonEmptyString(args.manifestPath) ? args.manifestPath.trim() : ".tapp/ci.json", replace: asBoolean(args.replace), allowUnresolved: asBoolean(args.allowUnresolved) });
|
|
3082
3083
|
return richResult(`✅ Reviewable CI gate installed — ${result.manifest.targets.length} target job(s); no commit, push, branch protection, or GitHub resource was created`, result);
|
|
3083
3084
|
} catch (error) { return errorResult("Could not prepare CI installation", { detail: error.message || String(error) }); }
|
|
3084
3085
|
}
|
|
@@ -3109,7 +3110,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3109
3110
|
const markersPath = capture ? path.join(capture.path, "ocqa-markers.txt") : resolveRepoFile(args.markersPath);
|
|
3110
3111
|
if (!markersPath) return errorResult("markersPath must be inside the repo, or provide captureId");
|
|
3111
3112
|
if (!fs.existsSync(markersPath)) return errorResult("OCQA markers not found", { markersPath });
|
|
3112
|
-
const outPath = resolveRepoFile(args.mapPath
|
|
3113
|
+
const outPath = isNonEmptyString(args.mapPath) ? resolveRepoFile(args.mapPath) : existingProjectArtifactPath(repoRoot, "ui-map.json");
|
|
3113
3114
|
if (!outPath) return errorResult("mapPath must be inside the repo");
|
|
3114
3115
|
try {
|
|
3115
3116
|
const observed = buildUiMapFromMarkers({ markersPath, platform: args.platform || "ios", target: args.target || "", runId: capture?.id || "" });
|
|
@@ -3120,7 +3121,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3120
3121
|
} catch (error) { return errorResult("Could not build UI Map", { detail: error.message || String(error) }); }
|
|
3121
3122
|
}
|
|
3122
3123
|
if (operation !== "read") return errorResult("operation must be read|build|diff");
|
|
3123
|
-
const mapPath = capture ? path.join(capture.path, "ui-map.json") : resolveRepoFile(args.mapPath
|
|
3124
|
+
const mapPath = capture ? path.join(capture.path, "ui-map.json") : isNonEmptyString(args.mapPath) ? resolveRepoFile(args.mapPath) : existingProjectArtifactPath(repoRoot, "ui-map.json");
|
|
3124
3125
|
if (!mapPath) return errorResult("mapPath must be inside the repo");
|
|
3125
3126
|
if (!fs.existsSync(mapPath)) return errorResult("UI Map not found; run QA or operation=build first", { mapPath });
|
|
3126
3127
|
try {
|
|
@@ -3450,7 +3451,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3450
3451
|
if (!isNonEmptyString(args.goal)) return errorResult("goal is required");
|
|
3451
3452
|
if (!isNonEmptyString(args.appBundleId)) return errorResult("appBundleId is required");
|
|
3452
3453
|
const backend = resolveModelBackend();
|
|
3453
|
-
if (!backend) return errorResult("AI-generate needs a model backend — set
|
|
3454
|
+
if (!backend) return errorResult("AI-generate needs a model backend — set TAPP_SUBSCRIPTION_TOKEN or ANTHROPIC_API_KEY.");
|
|
3454
3455
|
const bundleId = args.appBundleId.trim();
|
|
3455
3456
|
|
|
3456
3457
|
// 1) Grounding: reuse a capture's markers, else explore the app to build a screen/control map.
|
|
@@ -3480,7 +3481,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3480
3481
|
const ungrounded = ungroundedScreens(parsed.steps, grounding);
|
|
3481
3482
|
const flow = { name: args.name || parsed.name, app: bundleId, steps: parsed.steps };
|
|
3482
3483
|
const slug = flow.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "generated-flow";
|
|
3483
|
-
const dir = path.join(repoRoot, ".
|
|
3484
|
+
const dir = path.join(repoRoot, ".tapp", "flows");
|
|
3484
3485
|
fs.mkdirSync(dir, { recursive: true });
|
|
3485
3486
|
const outPath = path.join(dir, `${slug}.yml`);
|
|
3486
3487
|
const yamlRes = await runCommand("python3", [path.join(scriptsDir, "flow_lib.py"), "to-yaml", JSON.stringify(flow)], { cwd: repoRoot });
|
|
@@ -125,16 +125,16 @@ export async function validateWebMaintenanceProposal({ proposal, projectDir, url
|
|
|
125
125
|
if (!projectDir || !url) throw new Error("web maintenance validation requires projectDir and the running target URL");
|
|
126
126
|
const root = fs.realpathSync(path.resolve(projectDir));
|
|
127
127
|
const operation = proposal.operations[0];
|
|
128
|
-
const taskRoot = fs.realpathSync(path.join(root, ".
|
|
128
|
+
const taskRoot = fs.realpathSync(path.join(root, ".tapp", "tasks"));
|
|
129
129
|
const sourceTask = fs.realpathSync(path.resolve(root, operation.taskPath));
|
|
130
130
|
const sourceContract = fs.realpathSync(path.resolve(root, proposal.contractIntent.path));
|
|
131
|
-
if (!inside(taskRoot, sourceTask)) throw new Error("maintenance Task must be a regular reviewed file under .
|
|
131
|
+
if (!inside(taskRoot, sourceTask)) throw new Error("maintenance Task must be a regular reviewed file under .tapp/tasks");
|
|
132
132
|
if (!inside(root, sourceContract)) throw new Error("maintenance contract must remain inside the project");
|
|
133
133
|
if (digest(sourceTask) !== operation.taskSha256) throw new Error("Task digest changed after the proposal was created");
|
|
134
134
|
if (digest(sourceContract) !== proposal.contractIntent.sha256) throw new Error("release-contract intent digest changed after the proposal was created");
|
|
135
135
|
|
|
136
136
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "tapp-maintenance-validation-"));
|
|
137
|
-
const tempTasks = path.join(tempRoot, ".
|
|
137
|
+
const tempTasks = path.join(tempRoot, ".tapp", "tasks");
|
|
138
138
|
const stem = String(proposal.contractIntent.name || "contract").replace(/[^A-Za-z0-9._-]/g, "-");
|
|
139
139
|
const outputDir = evidenceDir ? path.resolve(evidenceDir, stem) : path.join(tempRoot, "evidence");
|
|
140
140
|
const logPath = path.join(outputDir, "validation.log");
|
|
@@ -153,7 +153,7 @@ export async function validateWebMaintenanceProposal({ proposal, projectDir, url
|
|
|
153
153
|
if (!(contract.setup || []).length || !(contract.teardown || []).length) {
|
|
154
154
|
throw new Error("automatic disposable maintenance validation requires controlled contract setup and teardown");
|
|
155
155
|
}
|
|
156
|
-
const pseudoContractPath = path.join(tempRoot, ".
|
|
156
|
+
const pseudoContractPath = path.join(tempRoot, ".tapp", "contracts", path.basename(sourceContract));
|
|
157
157
|
const execution = compileReleaseContract(contract, { platform: "web", sourcePath: pseudoContractPath });
|
|
158
158
|
const result = await runWebFlow({ flow: execution, url, logPath, screenshotDir: outputDir });
|
|
159
159
|
const contractUnchanged = digest(sourceContract) === proposal.contractIntent.sha256;
|
|
@@ -8,6 +8,7 @@ import crypto from "node:crypto";
|
|
|
8
8
|
import { loadReleaseContractFile } from "./release-contract.js";
|
|
9
9
|
import { loadTaskRegistry } from "./task-runtime.js";
|
|
10
10
|
import { replayableUiMapNavigation, semanticUiKey } from "./ui-map.js";
|
|
11
|
+
import { existingProjectArtifactPath, isProjectArtifactDirectory } from "./project-paths.js";
|
|
11
12
|
|
|
12
13
|
function posix(value) {
|
|
13
14
|
return String(value || "").replaceAll("\\", "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
@@ -28,8 +29,8 @@ export function sourcePathMatches(changedFile, ownershipPath) {
|
|
|
28
29
|
function repoRootFor(sourcePath) {
|
|
29
30
|
let current = path.dirname(path.resolve(sourcePath));
|
|
30
31
|
while (current !== path.dirname(current)) {
|
|
31
|
-
if (path.basename(current)
|
|
32
|
-
if (fs.existsSync(
|
|
32
|
+
if (isProjectArtifactDirectory(path.basename(current))) return path.dirname(current);
|
|
33
|
+
if (fs.existsSync(existingProjectArtifactPath(current))) return current;
|
|
33
34
|
current = path.dirname(current);
|
|
34
35
|
}
|
|
35
36
|
return process.cwd();
|
|
@@ -403,11 +404,11 @@ function mergeGroundingEvidence(existing, incoming) {
|
|
|
403
404
|
return merged;
|
|
404
405
|
}
|
|
405
406
|
|
|
406
|
-
export function adoptPrCoverageProposal({ projectDir, prPlanPath, item, releasePlanPath = ".
|
|
407
|
+
export function adoptPrCoverageProposal({ projectDir, prPlanPath, item, releasePlanPath = ".tapp/release-plan.json" } = {}) {
|
|
407
408
|
const root = fs.realpathSync(path.resolve(projectDir || process.cwd()));
|
|
408
409
|
const source = path.resolve(prPlanPath || "");
|
|
409
410
|
if (!prPlanPath || !fs.existsSync(source)) throw new Error(`PR plan not found: ${source || "(missing path)"}`);
|
|
410
|
-
const targetPath = path.resolve(root, releasePlanPath);
|
|
411
|
+
const targetPath = releasePlanPath === ".tapp/release-plan.json" ? existingProjectArtifactPath(root, "release-plan.json") : path.resolve(root, releasePlanPath);
|
|
411
412
|
if (!inside(root, targetPath)) throw new Error("Release plan path must stay inside the project directory");
|
|
412
413
|
if (!fs.existsSync(targetPath)) throw new Error(`Release plan not found: ${targetPath}; run tapp init first`);
|
|
413
414
|
const prPlan = JSON.parse(fs.readFileSync(source, "utf8"));
|
|
@@ -423,7 +424,7 @@ export function adoptPrCoverageProposal({ projectDir, prPlanPath, item, releaseP
|
|
|
423
424
|
const proposed = structuredClone(proposal.operation.item);
|
|
424
425
|
if (proposed?.origin !== "deterministic-ui-map-proposal" || proposed?.decision !== "pending") throw new Error("Coverage proposal is not a pending UI-Map-grounded release-plan item");
|
|
425
426
|
const ground = (proposed.groundedBy || []).find((entry) => entry.type === "ui-map-node");
|
|
426
|
-
const mapPath =
|
|
427
|
+
const mapPath = existingProjectArtifactPath(root, "ui-map.json");
|
|
427
428
|
if (!ground || !fs.existsSync(mapPath)) throw new Error("Coverage proposal requires the repository's persistent UI Map");
|
|
428
429
|
const map = JSON.parse(fs.readFileSync(mapPath, "utf8"));
|
|
429
430
|
const node = (map.nodes || []).find((candidate) => candidate.id === ground.id && candidate.status !== "proposed");
|
|
@@ -583,7 +584,7 @@ export async function buildPrContractPlan({
|
|
|
583
584
|
...(Array.isArray(changedSymbolEvidence) ? changedSymbolEvidence : []),
|
|
584
585
|
]);
|
|
585
586
|
const evidenceByFile = new Map(diffEvidence.map((item) => [item.file, item]));
|
|
586
|
-
const contractDir =
|
|
587
|
+
const contractDir = existingProjectArtifactPath(root, "contracts");
|
|
587
588
|
const discovered = discoverContracts && fs.existsSync(contractDir)
|
|
588
589
|
? fs.readdirSync(contractDir).filter((name) => /\.contract\.(?:ts|mts|mjs|js|json)$/i.test(name)).map((name) => path.join(contractDir, name)) : [];
|
|
589
590
|
const files = [...new Set([...discovered, ...contractPaths.map((item) => path.resolve(root, item))])];
|
|
@@ -594,10 +595,10 @@ export async function buildPrContractPlan({
|
|
|
594
595
|
}
|
|
595
596
|
|
|
596
597
|
let tasks = new Map();
|
|
597
|
-
const registrySource = files[0] ||
|
|
598
|
+
const registrySource = files[0] || existingProjectArtifactPath(root, "contracts", "contract.ts");
|
|
598
599
|
try { tasks = loadTaskRegistry({ sourcePath: registrySource, projectDir: root }); } catch {}
|
|
599
600
|
let map = null;
|
|
600
|
-
const resolvedMapPath = mapPath ? path.resolve(root, mapPath) :
|
|
601
|
+
const resolvedMapPath = mapPath ? path.resolve(root, mapPath) : existingProjectArtifactPath(root, "ui-map.json");
|
|
601
602
|
if (fs.existsSync(resolvedMapPath)) map = JSON.parse(fs.readFileSync(resolvedMapPath, "utf8"));
|
|
602
603
|
|
|
603
604
|
const impactedNodes = map ? map.nodes.filter((node) => matchedFiles(changes, node.sourcePaths || []).length) : [];
|
|
@@ -13,7 +13,7 @@ import { compileReleaseContract, loadReleaseContractFile } from "./release-contr
|
|
|
13
13
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
14
14
|
|
|
15
15
|
function executionHome() {
|
|
16
|
-
return process.env.
|
|
16
|
+
return process.env.TAPP_HOME || process.env.AUTOTAP_HOME || path.join(os.homedir(), ".tapp");
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
function atomicJson(destination, value) {
|