@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.
Files changed (48) hide show
  1. package/AGENTS.md +4 -4
  2. package/Harness/OCQAHarness/AppDelegate.swift +1 -1
  3. package/Harness/OCQAHarness.xcodeproj/project.pbxproj +2 -2
  4. package/Harness/OCQAHarnessUITests/ExplorerTests.swift +2 -2
  5. package/Harness/generate-harness-xcodeproj.rb +3 -3
  6. package/README.md +17 -15
  7. package/bin/tapp.js +28 -28
  8. package/browser/app.js +2 -2
  9. package/browser/index.html +1 -1
  10. package/docs/BROWSER-PRODUCT.md +2 -2
  11. package/docs/PRODUCT-ENGINE.md +7 -6
  12. package/docs/application-model.md +20 -25
  13. package/docs/scenarios.md +7 -7
  14. package/mcp-server/src/application-model.js +32 -26
  15. package/mcp-server/src/browser-product.js +1 -1
  16. package/mcp-server/src/ci-report.js +5 -4
  17. package/mcp-server/src/ci-setup.js +16 -7
  18. package/mcp-server/src/enrich.js +1 -1
  19. package/mcp-server/src/index.js +63 -62
  20. package/mcp-server/src/maintenance-proposal.js +4 -4
  21. package/mcp-server/src/pr-selection.js +9 -8
  22. package/mcp-server/src/product-execution.js +1 -1
  23. package/mcp-server/src/product-operations.js +20 -18
  24. package/mcp-server/src/project-config.js +8 -5
  25. package/mcp-server/src/project-paths.js +32 -0
  26. package/mcp-server/src/report.js +3 -3
  27. package/mcp-server/src/task-runtime.js +14 -10
  28. package/package.json +15 -3
  29. package/scripts/ci-gate.sh +15 -10
  30. package/scripts/flow_ai_judge.py +1 -1
  31. package/scripts/flow_lib.py +1 -1
  32. package/scripts/quick-capture.sh +15 -14
  33. package/scripts/run-flow.sh +6 -6
  34. package/scripts/android-corpus-e2e.sh +0 -30
  35. package/scripts/cleanup-xcode.sh +0 -157
  36. package/scripts/corpus-apps.txt +0 -9
  37. package/scripts/corpus-sweep.sh +0 -121
  38. package/scripts/coverage-eval.sh +0 -92
  39. package/scripts/coverage_eval_parse.py +0 -95
  40. package/scripts/deploy-and-build.sh +0 -99
  41. package/scripts/mutation-recall-desktop.sh +0 -186
  42. package/scripts/mutation-recall.sh +0 -121
  43. package/scripts/mutation_lib.py +0 -128
  44. package/scripts/mutation_operators.py +0 -144
  45. package/scripts/validation-matrix.sh +0 -146
  46. package/scripts/vision-fp-eval.sh +0 -206
  47. package/scripts/vision_escalation_responder.py +0 -147
  48. package/scripts/vision_fp_probe.py +0 -221
@@ -16,8 +16,9 @@ import {
16
16
  reviewReleasePlan,
17
17
  writeInitArtifacts,
18
18
  } from "./application-model.js";
19
- import { baselinePathForTarget, renderGithubWorkflow, selectApplicationTarget, writeCiInstallation, writeTargetBaseline } from "./ci-setup.js";
19
+ import { baselinePathForTarget, existingBaselinePathForTarget, renderGithubWorkflow, selectApplicationTarget, writeCiInstallation, writeTargetBaseline } from "./ci-setup.js";
20
20
  import { executeReleaseContract, runProductProcess } from "./product-execution.js";
21
+ import { projectArtifactDirectory } from "./project-paths.js";
21
22
 
22
23
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
23
24
  const packageVersion = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")).version;
@@ -50,8 +51,9 @@ function atomicJson(file, value) {
50
51
  fs.renameSync(temporary, file);
51
52
  }
52
53
 
53
- function artifactPaths(root, outDir = ".autotap") {
54
- const dir = path.resolve(root, outDir);
54
+ function artifactPaths(root, outDir = ".tapp") {
55
+ const selectedOutDir = projectArtifactDirectory(root, outDir);
56
+ const dir = path.resolve(root, selectedOutDir);
55
57
  if (!inside(root, dir)) throw new Error("Artifact directory must remain inside the repository");
56
58
  return {
57
59
  dir,
@@ -63,7 +65,7 @@ function artifactPaths(root, outDir = ".autotap") {
63
65
  }
64
66
 
65
67
  function productRunRoot(root) {
66
- const home = process.env.AUTOTAP_HOME || process.env.TAPP_HOME || path.join(os.homedir(), ".tapp");
68
+ const home = process.env.TAPP_HOME || process.env.AUTOTAP_HOME || path.join(os.homedir(), ".tapp");
67
69
  const identity = crypto.createHash("sha256").update(root).digest("hex").slice(0, 16);
68
70
  return path.join(home, "product-runs", identity);
69
71
  }
@@ -94,7 +96,7 @@ function listProductRuns(root) {
94
96
  }
95
97
 
96
98
  function listRepositoryFlows(root) {
97
- const directory = path.join(root, ".autotap", "flows");
99
+ const directory = path.join(root, projectArtifactDirectory(root), "flows");
98
100
  if (!fs.existsSync(directory)) return [];
99
101
  return fs.readdirSync(directory, { withFileTypes:true })
100
102
  .filter((entry) => entry.isFile() && /\.(?:ya?ml|json)$/i.test(entry.name))
@@ -127,7 +129,7 @@ function listRepositoryFlows(root) {
127
129
  .sort((left, right) => left.path.localeCompare(right.path));
128
130
  }
129
131
 
130
- export function readProductProject({ projectDir, outDir = ".autotap" } = {}) {
132
+ export function readProductProject({ projectDir, outDir = ".tapp" } = {}) {
131
133
  const root = realProject(projectDir);
132
134
  const paths = artifactPaths(root, outDir);
133
135
  const model = readJson(paths.model);
@@ -137,7 +139,7 @@ export function readProductProject({ projectDir, outDir = ".autotap" } = {}) {
137
139
  const requirements = model?.requirements || [];
138
140
  const planItems = plan?.items || [];
139
141
  const baselines = (model?.targets || []).map((target) => {
140
- const file = baselinePathForTarget(root, target);
142
+ const file = existingBaselinePathForTarget(root, target);
141
143
  return { targetId: target.id, platform: target.platform, path: file, relativePath: path.relative(root, file).replaceAll(path.sep, "/"), exists: fs.existsSync(file) };
142
144
  });
143
145
  return {
@@ -177,7 +179,7 @@ export function readProductProject({ projectDir, outDir = ".autotap" } = {}) {
177
179
  // target identity or configuration rules.
178
180
  export async function prepareProductTarget({
179
181
  projectDir,
180
- outDir = ".autotap",
182
+ outDir = ".tapp",
181
183
  platform = "",
182
184
  target = "",
183
185
  appPath = "",
@@ -262,7 +264,7 @@ export async function prepareProductTarget({
262
264
  export async function initializeProductProject({
263
265
  projectDir,
264
266
  mode = "inspect",
265
- outDir = ".autotap",
267
+ outDir = ".tapp",
266
268
  ownedUrl = "",
267
269
  platform = "",
268
270
  target = "",
@@ -326,7 +328,7 @@ function resolvePlan(root, outDir, planPath = "") {
326
328
  return { file, plan: readJson(file, { required: true }) };
327
329
  }
328
330
 
329
- export function reviewProductPlan({ projectDir, outDir = ".autotap", planPath = "", approve = [], reject = [], defer = [] } = {}) {
331
+ export function reviewProductPlan({ projectDir, outDir = ".tapp", planPath = "", approve = [], reject = [], defer = [] } = {}) {
330
332
  const root = realProject(projectDir);
331
333
  const resolved = resolvePlan(root, outDir, planPath);
332
334
  const plan = reviewReleasePlan(resolved.plan, { approve, reject, defer });
@@ -334,7 +336,7 @@ export function reviewProductPlan({ projectDir, outDir = ".autotap", planPath =
334
336
  return { operation: "review-plan", plan, planPath: resolved.file, project: readProductProject({ projectDir: root, outDir }) };
335
337
  }
336
338
 
337
- export async function generateProductPlan({ projectDir, outDir = ".autotap", planPath = "" } = {}) {
339
+ export async function generateProductPlan({ projectDir, outDir = ".tapp", planPath = "" } = {}) {
338
340
  const root = realProject(projectDir);
339
341
  const resolved = resolvePlan(root, outDir, planPath);
340
342
  const result = await generateApprovedContractProposals(resolved.plan, { projectDir: root });
@@ -344,7 +346,7 @@ export async function generateProductPlan({ projectDir, outDir = ".autotap", pla
344
346
 
345
347
  export async function validateProductPlan({
346
348
  projectDir,
347
- outDir = ".autotap",
349
+ outDir = ".tapp",
348
350
  planPath = "",
349
351
  items = [],
350
352
  platform = "",
@@ -411,7 +413,7 @@ export async function validateProductPlan({
411
413
  return { operation: "validate-plan", platform: selectedPlatform, passed: results.every((item) => item.passed), results, plan, planPath: resolved.file, project: readProductProject({ projectDir: root, outDir }) };
412
414
  }
413
415
 
414
- export async function promoteProductPlan({ projectDir, outDir = ".autotap", planPath = "", items = [] } = {}) {
416
+ export async function promoteProductPlan({ projectDir, outDir = ".tapp", planPath = "", items = [] } = {}) {
415
417
  const root = realProject(projectDir);
416
418
  const resolved = resolvePlan(root, outDir, planPath);
417
419
  const result = await promoteValidatedProposals(resolved.plan, { projectDir: root, ids: items || [] });
@@ -423,7 +425,7 @@ export async function promoteProductPlan({ projectDir, outDir = ".autotap", plan
423
425
  return { operation: "promote-plan", ...result, plan: written.plan, planPath: written.planPath, modelPath: written.modelPath, project: readProductProject({ projectDir: root, outDir }) };
424
426
  }
425
427
 
426
- export function prepareProductCi({ projectDir, outDir = ".autotap", modelPath = "", actionRef = DEFAULT_ACTION_REF, defaultBranch = "main" } = {}) {
428
+ export function prepareProductCi({ projectDir, outDir = ".tapp", modelPath = "", actionRef = DEFAULT_ACTION_REF, defaultBranch = "main" } = {}) {
427
429
  const root = realProject(projectDir);
428
430
  const modelFile = modelPath ? path.resolve(root, modelPath) : artifactPaths(root, outDir).model;
429
431
  if (!inside(root, modelFile)) throw new Error("Application model must remain inside the repository");
@@ -433,7 +435,7 @@ export function prepareProductCi({ projectDir, outDir = ".autotap", modelPath =
433
435
  return { operation: "prepare-ci", ...rendered, project: readProductProject({ projectDir: root, outDir }) };
434
436
  }
435
437
 
436
- export function installProductCi({ projectDir, outDir = ".autotap", modelPath = "", actionRef = DEFAULT_ACTION_REF, defaultBranch = "main", workflowPath = ".github/workflows/tapp.yml", manifestPath = ".autotap/ci.json", replace = false, allowUnresolved = false } = {}) {
438
+ export function installProductCi({ projectDir, outDir = ".tapp", modelPath = "", actionRef = DEFAULT_ACTION_REF, defaultBranch = "main", workflowPath = ".github/workflows/tapp.yml", manifestPath = ".tapp/ci.json", replace = false, allowUnresolved = false } = {}) {
437
439
  const root = realProject(projectDir);
438
440
  const rendered = prepareProductCi({ projectDir: root, outDir, modelPath, actionRef, defaultBranch });
439
441
  if (rendered.manifest.unresolved.length && !allowUnresolved) throw new Error(`CI installation is unresolved: ${rendered.manifest.unresolved.map((item) => `${item.platform}:${item.message}`).join("; ")}`);
@@ -443,7 +445,7 @@ export function installProductCi({ projectDir, outDir = ".autotap", modelPath =
443
445
 
444
446
  export async function runProductGate({
445
447
  projectDir,
446
- outDir = ".autotap",
448
+ outDir = ".tapp",
447
449
  platform = "web",
448
450
  target = "",
449
451
  url = "",
@@ -501,7 +503,7 @@ export async function runProductGate({
501
503
  if (!inside(root, baselinePath) || !fs.existsSync(baselinePath)) throw new Error("Baseline must be an existing file inside the repository");
502
504
  args.push("--baseline", baselinePath);
503
505
  } else {
504
- const targetBaseline = baselinePathForTarget(root, selected);
506
+ const targetBaseline = existingBaselinePathForTarget(root, selected);
505
507
  if (fs.existsSync(targetBaseline)) args.push("--baseline", targetBaseline);
506
508
  }
507
509
  onProgress({ phase: "gate", text: `Running ${selected.platform}:${selected.name} release gate` });
@@ -515,7 +517,7 @@ export async function runProductGate({
515
517
  return { operation: "run-gate", passed: execution.code === 0, code: execution.code, stdout: execution.stdout, stderr: execution.stderr, selectedTarget: selected, report, reportPath, markdownPath, runDir, project: readProductProject({ projectDir: root, outDir }) };
516
518
  }
517
519
 
518
- export function createProductBaseline({ projectDir, outDir = ".autotap", reportPath, platform = "web", target = "", replace = false, baselinePath = "" } = {}) {
520
+ export function createProductBaseline({ projectDir, outDir = ".tapp", reportPath, platform = "web", target = "", replace = false, baselinePath = "" } = {}) {
519
521
  const root = realProject(projectDir);
520
522
  const project = readProductProject({ projectDir: root, outDir });
521
523
  const selected = selectApplicationTarget(project.model, { platform, target });
@@ -1,7 +1,9 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { LEGACY_TAPP_DIRECTORY, TAPP_DIRECTORY, projectArtifactDirectory } from "./project-paths.js";
3
4
 
4
- export const PROJECT_CONFIG_RELATIVE_PATH = ".autotap/project.json";
5
+ export const PROJECT_CONFIG_RELATIVE_PATH = `${TAPP_DIRECTORY}/project.json`;
6
+ export const LEGACY_PROJECT_CONFIG_RELATIVE_PATH = `${LEGACY_TAPP_DIRECTORY}/project.json`;
5
7
 
6
8
  const ACTOR_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
7
9
  const ENV_NAME = /^[A-Z_][A-Z0-9_]{0,127}$/;
@@ -58,15 +60,16 @@ export function validateProjectConfig(config) {
58
60
 
59
61
  export function readProjectConfig(projectDir, { required = false } = {}) {
60
62
  const root = fs.realpathSync(path.resolve(projectDir || process.cwd()));
61
- const configPath = path.join(root, PROJECT_CONFIG_RELATIVE_PATH);
63
+ const relativePath = `${projectArtifactDirectory(root)}/project.json`;
64
+ const configPath = path.join(root, relativePath);
62
65
  if (!fs.existsSync(configPath)) {
63
66
  if (required) throw new Error(`Project configuration not found: ${configPath}`);
64
- return { root, path: configPath, relativePath: PROJECT_CONFIG_RELATIVE_PATH, config: { kind: "tapp-project-config", schemaVersion: 1, actors: {} }, exists: false, errors: [] };
67
+ return { root, path: configPath, relativePath, config: { kind: "tapp-project-config", schemaVersion: 1, actors: {} }, exists: false, errors: [] };
65
68
  }
66
69
  let config;
67
70
  try { config = JSON.parse(fs.readFileSync(configPath, "utf8")); }
68
- catch (error) { return { root, path: configPath, relativePath: PROJECT_CONFIG_RELATIVE_PATH, config: null, exists: true, errors: [`Invalid JSON: ${error.message}`] }; }
69
- return { root, path: configPath, relativePath: PROJECT_CONFIG_RELATIVE_PATH, config, exists: true, errors: validateProjectConfig(config) };
71
+ catch (error) { return { root, path: configPath, relativePath, config: null, exists: true, errors: [`Invalid JSON: ${error.message}`] }; }
72
+ return { root, path: configPath, relativePath, config, exists: true, errors: validateProjectConfig(config) };
70
73
  }
71
74
 
72
75
  export function configureActor(projectDir, { name, role = "", session = "default", provisioning = "existing", credentials = {}, replace = false } = {}) {
@@ -0,0 +1,32 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export const TAPP_DIRECTORY = ".tapp";
5
+ export const LEGACY_TAPP_DIRECTORY = ".autotap";
6
+ export const TAPP_CONFIG = ".tapp.yml";
7
+ export const LEGACY_TAPP_CONFIG = ".autotap.yml";
8
+
9
+ export function projectArtifactDirectory(projectDir, requested = TAPP_DIRECTORY) {
10
+ const root = path.resolve(projectDir);
11
+ if (requested !== TAPP_DIRECTORY) return requested;
12
+ const canonical = path.join(root, TAPP_DIRECTORY);
13
+ const legacy = path.join(root, LEGACY_TAPP_DIRECTORY);
14
+ if (!fs.existsSync(canonical) && fs.existsSync(legacy)) return LEGACY_TAPP_DIRECTORY;
15
+ return TAPP_DIRECTORY;
16
+ }
17
+
18
+ export function projectArtifactPath(projectDir, ...parts) {
19
+ return path.join(path.resolve(projectDir), projectArtifactDirectory(projectDir), ...parts);
20
+ }
21
+
22
+ export function existingProjectArtifactPath(projectDir, ...parts) {
23
+ const root = path.resolve(projectDir);
24
+ const canonical = path.join(root, TAPP_DIRECTORY, ...parts);
25
+ if (fs.existsSync(canonical)) return canonical;
26
+ const legacy = path.join(root, LEGACY_TAPP_DIRECTORY, ...parts);
27
+ return fs.existsSync(legacy) ? legacy : canonical;
28
+ }
29
+
30
+ export function isProjectArtifactDirectory(name) {
31
+ return name === TAPP_DIRECTORY || name === LEGACY_TAPP_DIRECTORY;
32
+ }
@@ -1,5 +1,5 @@
1
1
  // Pure report/gate logic shared by the MCP server (index.js) and the CI gate CLI
2
- // (ci-report.js). Turns a capture's OCQA markers into the same ship/no-ship report the AutoTap
2
+ // (ci-report.js). Turns a capture's OCQA markers into the same ship/no-ship report the Tapp
3
3
  // app produces, and diffs two runs' findings into the CI regression gate. No shell, no server —
4
4
  // keep it dependency-free so the CI path stays importable and testable.
5
5
  import fs from "fs";
@@ -78,7 +78,7 @@ export function parseOcqaMarkers(markersFilePath) {
78
78
  };
79
79
  }
80
80
 
81
- // Map harness OCQA_ISSUE `type` -> AutoTap FindingCategory. Crashes are always critical.
81
+ // Map harness OCQA_ISSUE `type` -> Tapp FindingCategory. Crashes are always critical.
82
82
  export const ISSUE_CATEGORY = {
83
83
  crash: "crash",
84
84
  app_hang: "app_hang",
@@ -100,7 +100,7 @@ export function severityRank(s) {
100
100
  return { critical: 0, high: 1, medium: 2, low: 3 }[s] ?? 4;
101
101
  }
102
102
 
103
- // Turn a capture's OCQA markers into the same ship/no-ship report the AutoTap app produces:
103
+ // Turn a capture's OCQA markers into the same ship/no-ship report Tapp produces:
104
104
  // deduped findings + a trustworthy verdict with a coverage floor (mirrors OrchestratorService).
105
105
  export function buildQaReport(markersFilePath, { platform = "ios" } = {}) {
106
106
  const base = parseOcqaMarkers(markersFilePath);
@@ -7,6 +7,7 @@ import path from "node:path";
7
7
  import { spawnSync } from "node:child_process";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { semanticUiKey } from "./ui-map.js";
10
+ import { isProjectArtifactDirectory, projectArtifactDirectory } from "./project-paths.js";
10
11
 
11
12
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
12
13
 
@@ -84,12 +85,15 @@ export function loadTaskFile(taskPath) {
84
85
  return { ...task, __path: path.resolve(taskPath) };
85
86
  }
86
87
 
87
- function findAutotapDir(sourcePath, explicitProjectDir = "") {
88
- if (explicitProjectDir) return path.join(path.resolve(explicitProjectDir), ".autotap");
88
+ function findTappDir(sourcePath, explicitProjectDir = "") {
89
+ if (explicitProjectDir) {
90
+ const root = path.resolve(explicitProjectDir);
91
+ return path.join(root, projectArtifactDirectory(root));
92
+ }
89
93
  let current = path.dirname(path.resolve(sourcePath));
90
94
  while (current !== path.dirname(current)) {
91
- if (path.basename(current) === ".autotap") return current;
92
- const candidate = path.join(current, ".autotap");
95
+ if (isProjectArtifactDirectory(path.basename(current))) return current;
96
+ const candidate = path.join(current, projectArtifactDirectory(current));
93
97
  if (fs.existsSync(candidate)) return candidate;
94
98
  current = path.dirname(current);
95
99
  }
@@ -97,16 +101,16 @@ function findAutotapDir(sourcePath, explicitProjectDir = "") {
97
101
  }
98
102
 
99
103
  export function loadTaskRegistry({ sourcePath, projectDir = "", taskFiles = [] }) {
100
- const autotapDir = findAutotapDir(sourcePath, projectDir);
101
- const taskDir = autotapDir ? path.join(autotapDir, "tasks") : "";
104
+ const tappDir = findTappDir(sourcePath, projectDir);
105
+ const taskDir = tappDir ? path.join(tappDir, "tasks") : "";
102
106
  const reviewed = taskDir && fs.existsSync(taskDir)
103
107
  ? fs.readdirSync(taskDir).filter((name) => /\.ya?ml$|\.json$/i.test(name)).map((name) => path.join(taskDir, name))
104
108
  : [];
105
- // Draft contracts generated under `.autotap/proposals/contracts` may compile
109
+ // Draft contracts generated under `.tapp/proposals/contracts` may compile
106
110
  // against sibling untrusted Task drafts. Ordinary committed contracts never
107
111
  // see this directory, so a proposal cannot silently enter the release gate.
108
- const proposalSource = String(path.resolve(sourcePath || "")).includes(`${path.sep}.autotap${path.sep}proposals${path.sep}`);
109
- const proposalDir = proposalSource && autotapDir ? path.join(autotapDir, "proposals", "tasks") : "";
112
+ const proposalSource = [".tapp", ".autotap"].some((directory) => String(path.resolve(sourcePath || "")).includes(`${path.sep}${directory}${path.sep}proposals${path.sep}`));
113
+ const proposalDir = proposalSource && tappDir ? path.join(tappDir, "proposals", "tasks") : "";
110
114
  const proposed = proposalDir && fs.existsSync(proposalDir)
111
115
  ? fs.readdirSync(proposalDir).filter((name) => /\.ya?ml$|\.json$/i.test(name)).map((name) => path.join(proposalDir, name))
112
116
  : [];
@@ -169,7 +173,7 @@ export function compileTaskSteps({ steps, registry, platform = "", flowVars = {}
169
173
  const invocation = taskCall(step);
170
174
  if (!invocation) { expanded.push(step); continue; }
171
175
  const task = registry.get(invocation.call.task);
172
- if (!task) throw new Error(`Task '${invocation.call.task}' was not found in .autotap/tasks`);
176
+ if (!task) throw new Error(`Task '${invocation.call.task}' was not found in .tapp/tasks`);
173
177
  if (stack.includes(task.name)) throw new Error(`Task cycle detected: ${[...stack, task.name].join(" -> ")}`);
174
178
  const implementation = implementationSteps(task, platform);
175
179
  if (!implementation?.steps) throw new Error(`Task '${task.name}' has no '${platform || "shared"}' implementation`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aarwitz/tapp",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "mcpName": "io.github.aarwitz/tapp",
5
5
  "description": "Release contracts, autonomous QA, and evidence-backed CI gates for iOS, Android, and web.",
6
6
  "license": "MIT",
@@ -24,7 +24,19 @@
24
24
  "docs/application-model.md",
25
25
  "docs/scenarios.md",
26
26
  "mcp-server/src/",
27
- "scripts/",
27
+ "scripts/ci-gate.sh",
28
+ "scripts/compile-contract.js",
29
+ "scripts/compile-flow.js",
30
+ "scripts/flow-platform.js",
31
+ "scripts/flow_ai_judge.py",
32
+ "scripts/flow_lib.py",
33
+ "scripts/platform-gate.js",
34
+ "scripts/pr-plan.js",
35
+ "scripts/quick-capture.sh",
36
+ "scripts/run-android-flow.js",
37
+ "scripts/run-flow.sh",
38
+ "scripts/run-web-flow.js",
39
+ "scripts/run-web-scenario.js",
28
40
  "Harness/OCQAHarness/",
29
41
  "Harness/OCQAHarnessUITests/",
30
42
  "Harness/OCQAHarness.xcodeproj/",
@@ -75,7 +87,7 @@
75
87
  "mobile"
76
88
  ],
77
89
  "scripts": {
78
- "test": "node --test tests/report.test.js tests/regression.test.js tests/engine.test.js tests/project-config.test.js tests/application-model.test.js tests/ui-map.test.js tests/task-runtime.test.js tests/release-contract.test.js tests/pr-selection.test.js tests/flow-runtime.test.js tests/web-explorer.test.js tests/web-session.test.js tests/web-flow.test.js tests/scenario-runtime.test.js tests/android-driver.test.js tests/android-explorer.test.js tests/android-flow.test.js tests/managed-web.test.js tests/product-operations.test.js tests/browser-product.test.js tests/browser-onboarding.test.js tests/managed-operation.test.js tests/cloud-runner.test.js tests/ci-setup.test.js tests/ci-install.test.js tests/cli.test.js tests/action.test.js tests/ci-gate.test.js tests/ci-report.test.js tests/desktop-protocol.test.js tests/ios-flow-protocol.test.js",
90
+ "test": "node --test tests/report.test.js tests/regression.test.js tests/engine.test.js tests/project-config.test.js tests/application-model.test.js tests/ui-map.test.js tests/task-runtime.test.js tests/release-contract.test.js tests/pr-selection.test.js tests/flow-runtime.test.js tests/web-explorer.test.js tests/web-session.test.js tests/web-flow.test.js tests/scenario-runtime.test.js tests/android-driver.test.js tests/android-explorer.test.js tests/android-flow.test.js tests/managed-web.test.js tests/product-operations.test.js tests/browser-product.test.js tests/browser-onboarding.test.js tests/managed-operation.test.js tests/cloud-runner.test.js tests/ci-setup.test.js tests/ci-install.test.js tests/cli.test.js tests/action.test.js tests/package-surface.test.js tests/ci-gate.test.js tests/ci-report.test.js tests/desktop-protocol.test.js tests/ios-flow-protocol.test.js",
79
91
  "test:browser-journey": "node --test tests/browser-journey.test.js",
80
92
  "test:browser-native": "TAPP_RUN_NATIVE_BROWSER=1 node --test tests/browser-native-journey.test.js"
81
93
  }
@@ -15,9 +15,9 @@
15
15
  # # bundle id is detected from the .app when omitted
16
16
  # [--actions N] # exploration budget (default 40)
17
17
  # [--timeout S] # exploration watchdog (default 600)
18
- # [--flows <glob>] # Flow YAMLs to replay (default: <app repo>/.autotap/flows/*.yml if --project-dir given)
19
- # [--scenarios <glob>] # Multi-actor Scenario YAMLs (web; default: <app repo>/.autotap/scenarios/*.yml)
20
- # [--contracts <glob>] # TypeScript release contracts (default: <app repo>/.autotap/contracts/*.contract.ts)
18
+ # [--flows <glob>] # Flow YAMLs to replay (default: <app repo>/.tapp/flows/*.yml if --project-dir given)
19
+ # [--scenarios <glob>] # Multi-actor Scenario YAMLs (web; default: <app repo>/.tapp/scenarios/*.yml)
20
+ # [--contracts <glob>] # TypeScript release contracts (default: <app repo>/.tapp/contracts/*.contract.ts)
21
21
  # [--project-dir <dir>] # the app repo checkout (for flows + baseline defaults)
22
22
  # [--pr-base <git-ref>] # select critical + diff-relevant contracts from base...head
23
23
  # [--pr-head <git-ref>] # default HEAD
@@ -79,10 +79,15 @@ if [[ -n "$PROJECT_DIR" ]]; then
79
79
  [[ -d "$PROJECT_DIR" ]] || { echo "❌ Project directory not found: $PROJECT_DIR" >&2; exit 2; }
80
80
  PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"
81
81
  fi
82
- [[ -z "$FLOWS" && -n "$PROJECT_DIR" && -d "$PROJECT_DIR/.autotap/flows" ]] && FLOWS="$PROJECT_DIR/.autotap/flows/*.yml"
83
- [[ "$PLATFORM" == "web" && -z "$SCENARIOS" && -n "$PROJECT_DIR" && -d "$PROJECT_DIR/.autotap/scenarios" ]] && SCENARIOS="$PROJECT_DIR/.autotap/scenarios/*.yml"
84
- [[ -z "$CONTRACTS" && -n "$PROJECT_DIR" && -d "$PROJECT_DIR/.autotap/contracts" ]] && CONTRACTS="$PROJECT_DIR/.autotap/contracts/*.contract.ts"
85
- [[ -z "$BASELINE" && -n "$PROJECT_DIR" && -f "$PROJECT_DIR/.autotap/baseline.json" ]] && BASELINE="$PROJECT_DIR/.autotap/baseline.json"
82
+ TAPP_PROJECT_ARTIFACTS=""
83
+ if [[ -n "$PROJECT_DIR" ]]; then
84
+ [[ -d "$PROJECT_DIR/.tapp" ]] && TAPP_PROJECT_ARTIFACTS="$PROJECT_DIR/.tapp"
85
+ [[ -z "$TAPP_PROJECT_ARTIFACTS" && -d "$PROJECT_DIR/.autotap" ]] && TAPP_PROJECT_ARTIFACTS="$PROJECT_DIR/.autotap"
86
+ fi
87
+ [[ -z "$FLOWS" && -n "$TAPP_PROJECT_ARTIFACTS" && -d "$TAPP_PROJECT_ARTIFACTS/flows" ]] && FLOWS="$TAPP_PROJECT_ARTIFACTS/flows/*.yml"
88
+ [[ "$PLATFORM" == "web" && -z "$SCENARIOS" && -n "$TAPP_PROJECT_ARTIFACTS" && -d "$TAPP_PROJECT_ARTIFACTS/scenarios" ]] && SCENARIOS="$TAPP_PROJECT_ARTIFACTS/scenarios/*.yml"
89
+ [[ -z "$CONTRACTS" && -n "$TAPP_PROJECT_ARTIFACTS" && -d "$TAPP_PROJECT_ARTIFACTS/contracts" ]] && CONTRACTS="$TAPP_PROJECT_ARTIFACTS/contracts/*.contract.ts"
90
+ [[ -z "$BASELINE" && -n "$TAPP_PROJECT_ARTIFACTS" && -f "$TAPP_PROJECT_ARTIFACTS/baseline.json" ]] && BASELINE="$TAPP_PROJECT_ARTIFACTS/baseline.json"
86
91
  [[ -z "$BASELINE" || -f "$BASELINE" ]] || { echo "❌ Baseline not found: $BASELINE" >&2; exit 2; }
87
92
  if [[ -n "$BASELINE" ]]; then
88
93
  python3 -c 'import json,sys; json.load(open(sys.argv[1]))' "$BASELINE" 2>/dev/null \
@@ -254,7 +259,7 @@ print(named[0] if named else (iphones[0] if iphones else ''))
254
259
  if [[ -z "$UDID" ]]; then
255
260
  RUNTIME="$(xcrun simctl list runtimes -j | python3 -c 'import sys,json; rts=[r for r in json.load(sys.stdin)["runtimes"] if r.get("isAvailable") and r["platform"]=="iOS"]; print(rts[-1]["identifier"] if rts else "")')"
256
261
  [[ -n "$RUNTIME" ]] || { echo "❌ No iOS simulator runtime available" >&2; exit 1; }
257
- UDID="$(xcrun simctl create "AutoTap CI" "$DEVICE" "$RUNTIME")" || { echo "❌ Could not create simulator" >&2; exit 1; }
262
+ UDID="$(xcrun simctl create "Tapp CI" "$DEVICE" "$RUNTIME")" || { echo "❌ Could not create simulator" >&2; exit 1; }
258
263
  fi
259
264
  echo "Booting $UDID …"
260
265
  xcrun simctl boot "$UDID" || true
@@ -269,7 +274,7 @@ xcrun simctl install "$UDID" "$APP_PATH" || { echo "❌ simctl install failed
269
274
  # ── Autonomous exploration (quick-capture builds the harness itself if needed).
270
275
  step "Explore ($ACTIONS actions, ${TIMEOUT}s watchdog)"
271
276
  set +e
272
- CAPTURE_ROOT="${AUTOTAP_HOME:-$ROOT}/captures"
277
+ CAPTURE_ROOT="${TAPP_HOME:-${AUTOTAP_HOME:-$ROOT}}/captures"
273
278
  mkdir -p "$CAPTURE_ROOT"
274
279
  CAPTURE_DIR="$(mktemp -d "$CAPTURE_ROOT/ci.XXXXXX")"
275
280
  TAPP_CAPTURE_DIR="$CAPTURE_DIR" OCQA_PR_TARGET_JSON="$IOS_PR_TARGET_JSON" "$ROOT/scripts/quick-capture.sh" explore "$BUNDLE_ID" --actions "$ACTIONS" --timeout "$TIMEOUT"
@@ -282,7 +287,7 @@ echo "Markers: $MARKERS"
282
287
  FLOW_LOG_ARGS=()
283
288
  if [[ "${#FLOW_FILES[@]}" -gt 0 ]]; then
284
289
  step "Flows"
285
- FLOW_LOG_DIR="$(mktemp -d /tmp/autotap-ci-flows.XXXXXX)"
290
+ FLOW_LOG_DIR="$(mktemp -d /tmp/tapp-ci-flows.XXXXXX)"
286
291
  for flow in "${FLOW_FILES[@]}"; do
287
292
  name="$(basename "$flow" .yml)"
288
293
  log="$FLOW_LOG_DIR/$name.log"
@@ -26,7 +26,7 @@ def _ctx():
26
26
 
27
27
 
28
28
  SSL_CTX = _ctx()
29
- MODEL = os.environ.get("AUTOTAP_VISION_MODEL", "claude-haiku-4-5-20251001")
29
+ MODEL = os.environ.get("TAPP_VISION_MODEL") or os.environ.get("AUTOTAP_VISION_MODEL", "claude-haiku-4-5-20251001")
30
30
  SYSTEM = (
31
31
  "You are a QA test oracle. You are shown one iOS app screenshot and a CLAIM the test asserts about "
32
32
  "it. Decide if the claim is TRUE of what is actually visible. Be strict and literal — only pass if "
@@ -23,7 +23,7 @@ def load_flow(path):
23
23
 
24
24
 
25
25
  def to_yaml(json_str):
26
- """Emit a recorded/inline Flow (JSON string) as tidy YAML for saving to .autotap/flows/*.yml."""
26
+ """Emit a recorded/inline Flow (JSON string) as tidy YAML for saving to .tapp/flows/*.yml."""
27
27
  import yaml
28
28
  flow = json.loads(json_str)
29
29
  # Order keys for readability. `platform` + `url` make the same repository-native
@@ -12,27 +12,28 @@ set -euo pipefail
12
12
  # [--timeout <secs>] # Hard timeout for explore command
13
13
  # quick-capture.sh tree <bundleId> # Dump accessibility tree
14
14
  #
15
- # All output goes to ~/repos/AutoTap/captures/<timestamp>/
15
+ # All output goes to the configured Tapp home or the repository capture directory.
16
16
 
17
17
  PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
18
18
  # TAPP_CAPTURE_DIR selects one exact run directory (used by CI so a failed run can never
19
- # accidentally reuse stale markers). AUTOTAP_HOME redirects all writable output otherwise —
20
- # set by the `autotap` CLI when running as an installed npm package, where the package dir
19
+ # accidentally reuse stale markers). TAPP_HOME redirects all writable output otherwise —
20
+ # set by the `tapp` CLI when running as an installed npm package, where the package dir
21
21
  # must stay read-only. Unset (repo dev flow), everything lands in the repo as before.
22
+ TAPP_RUNTIME_HOME="${TAPP_HOME:-${AUTOTAP_HOME:-}}"
22
23
  if [[ -n "${TAPP_CAPTURE_DIR:-}" ]]; then
23
24
  CAPTURE_DIR="$TAPP_CAPTURE_DIR"
24
- elif [[ -n "${AUTOTAP_HOME:-}" ]]; then
25
- CAPTURE_DIR="$AUTOTAP_HOME/captures/$(date +%Y%m%d-%H%M%S)"
25
+ elif [[ -n "$TAPP_RUNTIME_HOME" ]]; then
26
+ CAPTURE_DIR="$TAPP_RUNTIME_HOME/captures/$(date +%Y%m%d-%H%M%S)"
26
27
  else
27
28
  CAPTURE_DIR="$PROJECT_ROOT/captures/$(date +%Y%m%d-%H%M%S)"
28
29
  fi
29
- if [[ -n "${AUTOTAP_HOME:-}" ]]; then
30
- HARNESS_DERIVED="$AUTOTAP_HOME/harness-derived"
30
+ if [[ -n "$TAPP_RUNTIME_HOME" ]]; then
31
+ HARNESS_DERIVED="$TAPP_RUNTIME_HOME/harness-derived"
31
32
  else
32
- HARNESS_DERIVED="/tmp/autotap-harness-derived"
33
+ HARNESS_DERIVED="/tmp/tapp-harness-derived"
33
34
  fi
34
35
  HARNESS_PROJECT="$PROJECT_ROOT/Harness/OCQAHarness.xcodeproj"
35
- DEFAULT_BUNDLE="com.autotap.demoapp"
36
+ DEFAULT_BUNDLE="io.github.aarwitz.tapp.demoapp"
36
37
 
37
38
  get_booted_sim() {
38
39
  xcrun simctl list devices booted -j 2>/dev/null \
@@ -168,13 +169,13 @@ run_harness_test() {
168
169
  "OCQA_MAX_ACTIONS": "$max_actions",
169
170
  "OCQA_TIMEOUT_SECONDS": "$timeout_secs",
170
171
  "OCQA_TEST_EMAIL": "${OCQA_TEST_EMAIL:-qa@example.com}",
171
- "OCQA_TEST_PASSWORD": "${OCQA_TEST_PASSWORD:-Autotap123!}"$interactive_line$overrides_line$launch_args_line$launch_env_line$login_steps_line$pr_target_line
172
+ "OCQA_TEST_PASSWORD": "${OCQA_TEST_PASSWORD:-Tapp123!}"$interactive_line$overrides_line$launch_args_line$launch_env_line$login_steps_line$pr_target_line
172
173
  }
173
174
  CONF
174
175
 
175
176
  local xctestrun=$(find "$HARNESS_DERIVED/Build/Products" -name "*.xctestrun" 2>/dev/null | head -1)
176
177
  if [[ -z "$xctestrun" ]]; then
177
- echo "ERROR: No xctestrun found. Run: ./scripts/deploy-and-build.sh --harness" >&2
178
+ echo "ERROR: No xctestrun found. Run: tapp install" >&2
178
179
  return 1
179
180
  fi
180
181
 
@@ -382,12 +383,12 @@ case "$MODE" in
382
383
  "OCQA_SESSION_RESULT_PATH": "$SESS_RES",
383
384
  "OCQA_SESSION_TIMEOUT": "$SESS_TIMEOUT",
384
385
  "OCQA_TEST_EMAIL": "${OCQA_TEST_EMAIL:-qa@example.com}",
385
- "OCQA_TEST_PASSWORD": "${OCQA_TEST_PASSWORD:-Autotap123!}"$sess_args_line$sess_env_line
386
+ "OCQA_TEST_PASSWORD": "${OCQA_TEST_PASSWORD:-Tapp123!}"$sess_args_line$sess_env_line
386
387
  }
387
388
  CONF
388
389
  xctestrun=$(find "$HARNESS_DERIVED/Build/Products" -name "*.xctestrun" 2>/dev/null | head -1)
389
390
  if [[ -z "$xctestrun" ]]; then
390
- echo "ERROR: No xctestrun. Run: ./scripts/deploy-and-build.sh --harness" >&2
391
+ echo "ERROR: No xctestrun. Run: tapp install" >&2
391
392
  exit 1
392
393
  fi
393
394
  export TEST_RUNNER_OCQA_CONFIG_PATH=/tmp/ocqa-run-config.json
@@ -399,7 +400,7 @@ CONF
399
400
  ;;
400
401
 
401
402
  build-harness)
402
- # Just (re)build the harness cache for the booted sim — used by `autotap install`
403
+ # Just (re)build the harness cache for the booted sim — used by `tapp install`
403
404
  # for a fast first tool call later. No capture output.
404
405
  ensure_harness_built "$SIM_NAME"
405
406
  rmdir "$CAPTURE_DIR" 2>/dev/null || true
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # Run an AutoTap Flow — a deterministic, authored/recorded E2E test (see docs/flows-architecture.md).
2
+ # Run a Tapp Flow — a deterministic, authored/recorded E2E test.
3
3
  #
4
4
  # Replays the flow's steps against an app on the BOOTED simulator with wait-for-condition timing and
5
5
  # poll-with-timeout assertions (no sleeps), then prints a scannable pass/fail report. Deterministic by
@@ -24,14 +24,14 @@ APP="${2:-}"
24
24
 
25
25
  UDID="$(xcrun simctl list devices booted -j 2>/dev/null | python3 -c 'import sys,json; d=json.load(sys.stdin); print(next((x["udid"] for v in d["devices"].values() for x in v if x.get("state")=="Booted"), ""))')"
26
26
  [ -z "$UDID" ] && { echo "❌ No booted simulator."; exit 2; }
27
- # When run through the `autotap` CLI (installed npm package), the harness cache lives under
28
- # AUTOTAP_HOME — check there first, then the repo-dev locations.
27
+ # When run through the installed `tapp` CLI, the harness cache lives under TAPP_HOME.
29
28
  XCTR=""
30
- [ -n "${AUTOTAP_HOME:-}" ] && XCTR="$(find "$AUTOTAP_HOME/harness-derived/Build/Products" -name '*.xctestrun' 2>/dev/null | head -1)"
29
+ TAPP_RUNTIME_HOME="${TAPP_HOME:-${AUTOTAP_HOME:-}}"
30
+ [ -n "$TAPP_RUNTIME_HOME" ] && XCTR="$(find "$TAPP_RUNTIME_HOME/harness-derived/Build/Products" -name '*.xctestrun' 2>/dev/null | head -1)"
31
31
  [ -z "$XCTR" ] && XCTR="$(find "$HOME/Library/Developer/Xcode/DerivedData/OCQAHarness-"*/Build/Products -name '*.xctestrun' 2>/dev/null | head -1)"
32
- [ -z "$XCTR" ] && XCTR="$(find /tmp/autotap-harness-derived/Build/Products -name '*.xctestrun' 2>/dev/null | head -1)"
32
+ [ -z "$XCTR" ] && XCTR="$(find /tmp/tapp-harness-derived/Build/Products -name '*.xctestrun' 2>/dev/null | head -1)"
33
33
  [ -z "$XCTR" ] && XCTR="$(find /tmp/harness-build/Build/Products -name '*.xctestrun' 2>/dev/null | head -1)"
34
- [ -z "$XCTR" ] && { echo "❌ Harness not built. Run: scripts/deploy-and-build.sh --harness"; exit 2; }
34
+ [ -z "$XCTR" ] && { echo "❌ Harness not built. Run: tapp install"; exit 2; }
35
35
 
36
36
  NAME="$(python3 -c "import sys,json;print(json.loads(sys.argv[1]).get('name','flow'))" "$FLOW_JSON")"
37
37
  echo "▶️ Running flow \"$NAME\" against $APP …"
@@ -1,30 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Build and exercise every native Android fixture through the same release gate
3
- # customers use. A connected, booted emulator/device is required.
4
- set -euo pipefail
5
-
6
- ROOT="$(cd "$(dirname "$0")/.." && pwd)"
7
- CORPUS="$ROOT/AndroidCorpus"
8
- SERIAL="${1:-${ANDROID_SERIAL:-}}"
9
- SERIAL_ARGS=()
10
- [[ -n "$SERIAL" ]] && SERIAL_ARGS=(--serial "$SERIAL")
11
-
12
- "$CORPUS/gradlew" -p "$CORPUS" \
13
- :demoapp:assembleDebug :logindemo:assembleDebug :shopdemo:assembleDebug --no-daemon --quiet
14
-
15
- run_fixture() {
16
- local module="$1" app_id="$2" flow="$3"
17
- local apk="$CORPUS/$module/build/outputs/apk/debug/$module-debug.apk"
18
- local output_dir="${RUNNER_TEMP:-/tmp}/tapp-android-corpus-$module"
19
- mkdir -p "$output_dir"
20
- echo "Testing Android corpus fixture: $module ($app_id)"
21
- "$ROOT/scripts/ci-gate.sh" --platform android --apk "$apk" --app-id "$app_id" \
22
- "${SERIAL_ARGS[@]}" --actions 12 --timeout 180 --flows "$CORPUS/$module/.autotap/flows/$flow" \
23
- --project-dir "$CORPUS/$module" --json-out "$output_dir/report.json" --md-out "$output_dir/report.md"
24
- }
25
-
26
- run_fixture demoapp io.tapp.corpus.demo smoke.yml
27
- run_fixture logindemo io.tapp.corpus.login sign-in.yml
28
- run_fixture shopdemo io.tapp.corpus.shop checkout.yml
29
-
30
- echo "All Android corpus gates passed."