@aarwitz/tapp 0.15.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 (77) hide show
  1. package/AGENTS.md +123 -0
  2. package/Harness/OCQAHarness/AppDelegate.swift +21 -0
  3. package/Harness/OCQAHarness/Info.plist +26 -0
  4. package/Harness/OCQAHarness.xcodeproj/project.pbxproj +199 -0
  5. package/Harness/OCQAHarness.xcodeproj/xcshareddata/xcschemes/OCQAHarnessUITests.xcscheme +22 -0
  6. package/Harness/OCQAHarnessUITests/ExplorerTests.swift +4526 -0
  7. package/Harness/OCQAHarnessUITests/Info.plist +22 -0
  8. package/Harness/generate-harness-xcodeproj.rb +254 -0
  9. package/LICENSE +21 -0
  10. package/README.md +374 -0
  11. package/bin/tapp.js +1382 -0
  12. package/browser/app.css +227 -0
  13. package/browser/app.js +675 -0
  14. package/browser/index.html +195 -0
  15. package/browser/product-contract.js +25 -0
  16. package/browser/view-model.js +16 -0
  17. package/docs/BROWSER-PRODUCT.md +72 -0
  18. package/docs/PRODUCT-ENGINE.md +102 -0
  19. package/docs/application-model.md +276 -0
  20. package/docs/scenarios.md +95 -0
  21. package/mcp-server/src/android-driver.js +287 -0
  22. package/mcp-server/src/android-explorer.js +197 -0
  23. package/mcp-server/src/android-flow.js +89 -0
  24. package/mcp-server/src/application-model.js +1597 -0
  25. package/mcp-server/src/browser-product.js +659 -0
  26. package/mcp-server/src/browser-workspaces.js +234 -0
  27. package/mcp-server/src/ci-report.js +557 -0
  28. package/mcp-server/src/ci-setup.js +359 -0
  29. package/mcp-server/src/contract-authoring.js +10 -0
  30. package/mcp-server/src/enrich.js +57 -0
  31. package/mcp-server/src/flow-runtime.js +127 -0
  32. package/mcp-server/src/html-report.js +124 -0
  33. package/mcp-server/src/index.js +3775 -0
  34. package/mcp-server/src/maintenance-proposal.js +178 -0
  35. package/mcp-server/src/managed-operation.js +61 -0
  36. package/mcp-server/src/pr-selection.js +841 -0
  37. package/mcp-server/src/product-execution.js +155 -0
  38. package/mcp-server/src/product-operations.js +526 -0
  39. package/mcp-server/src/project-config.js +101 -0
  40. package/mcp-server/src/release-contract.d.ts +81 -0
  41. package/mcp-server/src/release-contract.js +226 -0
  42. package/mcp-server/src/report.js +363 -0
  43. package/mcp-server/src/scenario-runtime.js +139 -0
  44. package/mcp-server/src/static-server.js +44 -0
  45. package/mcp-server/src/task-runtime.js +266 -0
  46. package/mcp-server/src/ui-map.js +661 -0
  47. package/mcp-server/src/web-explorer.js +493 -0
  48. package/mcp-server/src/web-flow.js +238 -0
  49. package/package.json +82 -0
  50. package/scripts/android-corpus-e2e.sh +30 -0
  51. package/scripts/ci-gate.sh +323 -0
  52. package/scripts/cleanup-xcode.sh +157 -0
  53. package/scripts/compile-contract.js +27 -0
  54. package/scripts/compile-flow.js +18 -0
  55. package/scripts/corpus-apps.txt +9 -0
  56. package/scripts/corpus-sweep.sh +121 -0
  57. package/scripts/coverage-eval.sh +92 -0
  58. package/scripts/coverage_eval_parse.py +95 -0
  59. package/scripts/deploy-and-build.sh +99 -0
  60. package/scripts/flow-platform.js +18 -0
  61. package/scripts/flow_ai_judge.py +102 -0
  62. package/scripts/flow_lib.py +154 -0
  63. package/scripts/mutation-recall-desktop.sh +186 -0
  64. package/scripts/mutation-recall.sh +121 -0
  65. package/scripts/mutation_lib.py +128 -0
  66. package/scripts/mutation_operators.py +144 -0
  67. package/scripts/platform-gate.js +186 -0
  68. package/scripts/pr-plan.js +68 -0
  69. package/scripts/quick-capture.sh +419 -0
  70. package/scripts/run-android-flow.js +27 -0
  71. package/scripts/run-flow.sh +90 -0
  72. package/scripts/run-web-flow.js +28 -0
  73. package/scripts/run-web-scenario.js +23 -0
  74. package/scripts/validation-matrix.sh +146 -0
  75. package/scripts/vision-fp-eval.sh +206 -0
  76. package/scripts/vision_escalation_responder.py +147 -0
  77. package/scripts/vision_fp_probe.py +221 -0
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env bash
2
+ # Tapp CI gate — one command that answers "should this merge?" for iOS, Android, or web.
3
+ #
4
+ # Boots a simulator if needed, installs the app build under test, runs the autonomous exploration
5
+ # harness, replays every committed Flow (deterministic E2E tests), diffs the findings against a
6
+ # stored baseline, writes a GitHub Actions step summary, and exits non-zero when the gate fails
7
+ # (new high/critical findings vs. baseline, or any failed Flow). Wrapped by ../action.yml for
8
+ # GitHub Actions; equally usable from any other CI or locally.
9
+ #
10
+ # Usage:
11
+ # scripts/ci-gate.sh [--platform ios] --app <path/to/App.app> [--bundle-id <com.example.app>]
12
+ # scripts/ci-gate.sh --platform android --apk <path/to/app.apk> --app-id <com.example.app>
13
+ # scripts/ci-gate.sh --platform web [--url <http(s)://owned-app>]
14
+ # # omit --url with --project-dir to detect/build/start/stop one owned web target
15
+ # # bundle id is detected from the .app when omitted
16
+ # [--actions N] # exploration budget (default 40)
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)
21
+ # [--project-dir <dir>] # the app repo checkout (for flows + baseline defaults)
22
+ # [--pr-base <git-ref>] # select critical + diff-relevant contracts from base...head
23
+ # [--pr-head <git-ref>] # default HEAD
24
+ # [--changed-files-file <json|newline file>] # CI-provided PR paths; includes renamed paths
25
+ # [--pr-plan-out <file.json>] # persist the reviewable selection plan
26
+ # [--baseline <file.json>] # prior report to diff against (skipped if absent)
27
+ # [--target-key <stable-id>] # isolates target-specific baselines in monorepos
28
+ # [--fail-on gate|blocked|any] # gate policy (default gate; see ci-report.js)
29
+ # [--json-out <file.json>] # write the full report (use as the next baseline)
30
+ # [--md-out <file.md>] # write the rendered markdown report (for a PR comment)
31
+ # [--device <name>] # simulator device to boot if none is (default "iPhone 16 Pro")
32
+ #
33
+ # The app must be a SIMULATOR build (xcodebuild ... -destination 'generic/platform=iOS Simulator').
34
+ set -uo pipefail
35
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
36
+
37
+ usage() {
38
+ sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'
39
+ }
40
+
41
+ PLATFORM="ios" APP_PATH="" BUNDLE_ID="" APK_PATH="" APP_ID="" URL="" WEB_TARGET="" TARGET_KEY="" SERIAL="" ACTIONS=40 TIMEOUT=600 FLOWS="" SCENARIOS="" CONTRACTS="" PROJECT_DIR="" BASELINE="" FAIL_ON="gate" JSON_OUT="" MD_OUT="" DEVICE="iPhone 16 Pro" PR_BASE="" PR_HEAD="HEAD" CHANGED_FILES_FILE="" PR_PLAN_OUT=""
42
+ IOS_PR_TARGET_JSON=""
43
+ FLOWS_EXPLICIT=false SCENARIOS_EXPLICIT=false CONTRACTS_EXPLICIT=false
44
+ while [[ $# -gt 0 ]]; do
45
+ case "$1" in
46
+ --platform) PLATFORM="$2"; shift 2 ;;
47
+ --app) APP_PATH="$2"; shift 2 ;;
48
+ --bundle-id) BUNDLE_ID="$2"; shift 2 ;;
49
+ --apk) APK_PATH="$2"; shift 2 ;;
50
+ --app-id) APP_ID="$2"; shift 2 ;;
51
+ --url) URL="$2"; shift 2 ;;
52
+ --web-target) WEB_TARGET="$2"; shift 2 ;;
53
+ --target-key) TARGET_KEY="$2"; shift 2 ;;
54
+ --serial) SERIAL="$2"; shift 2 ;;
55
+ --actions) ACTIONS="$2"; shift 2 ;;
56
+ --timeout) TIMEOUT="$2"; shift 2 ;;
57
+ --flows) FLOWS="$2"; FLOWS_EXPLICIT=true; shift 2 ;;
58
+ --scenarios) SCENARIOS="$2"; SCENARIOS_EXPLICIT=true; shift 2 ;;
59
+ --contracts) CONTRACTS="$2"; CONTRACTS_EXPLICIT=true; shift 2 ;;
60
+ --project-dir) PROJECT_DIR="$2"; shift 2 ;;
61
+ --pr-base) PR_BASE="$2"; shift 2 ;;
62
+ --pr-head) PR_HEAD="$2"; shift 2 ;;
63
+ --changed-files-file) CHANGED_FILES_FILE="$2"; shift 2 ;;
64
+ --pr-plan-out) PR_PLAN_OUT="$2"; shift 2 ;;
65
+ --baseline) BASELINE="$2"; shift 2 ;;
66
+ --fail-on) FAIL_ON="$2"; shift 2 ;;
67
+ --json-out) JSON_OUT="$2"; shift 2 ;;
68
+ --md-out) MD_OUT="$2"; shift 2 ;;
69
+ --device) DEVICE="$2"; shift 2 ;;
70
+ --help|-h) usage; exit 0 ;;
71
+ *) echo "Unknown argument: $1" >&2; exit 2 ;;
72
+ esac
73
+ done
74
+ [[ "$PLATFORM" == "ios" || "$PLATFORM" == "android" || "$PLATFORM" == "web" ]] || { echo "❌ --platform must be ios|android|web" >&2; exit 2; }
75
+ [[ "$ACTIONS" =~ ^[1-9][0-9]*$ ]] || { echo "❌ --actions must be a positive integer" >&2; exit 2; }
76
+ [[ "$TIMEOUT" =~ ^[1-9][0-9]*$ ]] || { echo "❌ --timeout must be a positive integer" >&2; exit 2; }
77
+ [[ "$FAIL_ON" == "gate" || "$FAIL_ON" == "blocked" || "$FAIL_ON" == "any" ]] || { echo "❌ --fail-on must be gate|blocked|any" >&2; exit 2; }
78
+ if [[ -n "$PROJECT_DIR" ]]; then
79
+ [[ -d "$PROJECT_DIR" ]] || { echo "❌ Project directory not found: $PROJECT_DIR" >&2; exit 2; }
80
+ PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"
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"
86
+ [[ -z "$BASELINE" || -f "$BASELINE" ]] || { echo "❌ Baseline not found: $BASELINE" >&2; exit 2; }
87
+ if [[ -n "$BASELINE" ]]; then
88
+ python3 -c 'import json,sys; json.load(open(sys.argv[1]))' "$BASELINE" 2>/dev/null \
89
+ || { echo "❌ Baseline is not valid JSON: $BASELINE" >&2; exit 2; }
90
+ fi
91
+ CONTRACT_FILES=()
92
+ if [[ -n "$CONTRACTS" ]]; then
93
+ shopt -s nullglob
94
+ for contract in $CONTRACTS; do CONTRACT_FILES+=("$contract"); done
95
+ shopt -u nullglob
96
+ if [[ "${#CONTRACT_FILES[@]}" -eq 0 && "$CONTRACTS_EXPLICIT" == true ]]; then
97
+ echo "❌ --contracts matched no files: $CONTRACTS" >&2
98
+ exit 2
99
+ fi
100
+ fi
101
+
102
+ validate_json_env() {
103
+ local name="$1" expected="$2" value="${!1:-}"
104
+ [[ -z "$value" ]] && return 0
105
+ TAPP_JSON_VALUE="$value" TAPP_JSON_EXPECTED="$expected" python3 -c '
106
+ import json, os, sys
107
+ value = json.loads(os.environ["TAPP_JSON_VALUE"])
108
+ expected = list if os.environ["TAPP_JSON_EXPECTED"] == "array" else dict
109
+ sys.exit(0 if isinstance(value, expected) else 1)
110
+ ' || { echo "❌ $name must be a valid JSON $expected" >&2; exit 2; }
111
+ }
112
+ validate_json_env OCQA_APP_LAUNCH_ARGS_JSON array
113
+ validate_json_env OCQA_APP_LAUNCH_ENV_JSON object
114
+ validate_json_env OCQA_LOGIN_STEPS_JSON array
115
+
116
+ FLOW_FILES=()
117
+ if [[ -n "$FLOWS" ]]; then
118
+ shopt -s nullglob
119
+ for flow in $FLOWS; do FLOW_FILES+=("$flow"); done
120
+ shopt -u nullglob
121
+ if [[ "${#FLOW_FILES[@]}" -eq 0 && "$FLOWS_EXPLICIT" == true ]]; then
122
+ echo "❌ --flows matched no files: $FLOWS" >&2
123
+ exit 2
124
+ fi
125
+ fi
126
+ SCENARIO_FILES=()
127
+ if [[ -n "$SCENARIOS" ]]; then
128
+ shopt -s nullglob
129
+ for scenario in $SCENARIOS; do SCENARIO_FILES+=("$scenario"); done
130
+ shopt -u nullglob
131
+ if [[ "${#SCENARIO_FILES[@]}" -eq 0 && "$SCENARIOS_EXPLICIT" == true ]]; then
132
+ echo "❌ --scenarios matched no files: $SCENARIOS" >&2
133
+ exit 2
134
+ fi
135
+ if [[ "$PLATFORM" != "web" && "${#SCENARIO_FILES[@]}" -gt 0 ]]; then
136
+ echo "❌ Multi-actor Scenarios currently require --platform web" >&2
137
+ exit 2
138
+ fi
139
+ fi
140
+
141
+ # Platform-filter both default and explicitly supplied suites before any real
142
+ # surface is launched. Default discovery may legitimately find only another
143
+ # target's Flows in a monorepo; an explicit all-mismatched suite is a config
144
+ # error. Legacy platform-less Flows resolve to iOS, never "all platforms".
145
+ if [[ "${#FLOW_FILES[@]}" -gt 0 ]]; then
146
+ PLATFORM_FLOW_FILES=()
147
+ PLATFORM_FLOW_COUNT=0
148
+ for flow in "${FLOW_FILES[@]}"; do
149
+ flow_platform="$(node "$ROOT/scripts/flow-platform.js" "$flow")" || exit 2
150
+ if [[ "$flow_platform" == "$PLATFORM" ]]; then
151
+ PLATFORM_FLOW_FILES+=("$flow")
152
+ PLATFORM_FLOW_COUNT=$((PLATFORM_FLOW_COUNT + 1))
153
+ fi
154
+ done
155
+ if [[ "$PLATFORM_FLOW_COUNT" -eq 0 && "$FLOWS_EXPLICIT" == true ]]; then
156
+ echo "❌ None of the supplied Flows target platform '$PLATFORM'" >&2
157
+ exit 2
158
+ fi
159
+ FLOW_FILES=()
160
+ [[ "$PLATFORM_FLOW_COUNT" -gt 0 ]] && FLOW_FILES=("${PLATFORM_FLOW_FILES[@]}")
161
+ fi
162
+
163
+ # A PR plan is an execution manifest, not advisory prose: only critical/always
164
+ # and reviewed diff-relevant contracts advance to replay. Unknown files and
165
+ # mapped-but-uncovered UI states remain explicit in the report.
166
+ PR_PLAN_PATH=""
167
+ if [[ -n "$PR_BASE" || -n "$CHANGED_FILES_FILE" ]]; then
168
+ [[ -n "$PROJECT_DIR" && -d "$PROJECT_DIR" ]] || { echo "❌ PR selection requires --project-dir <repository>" >&2; exit 2; }
169
+ [[ -z "$PR_BASE" || -z "$CHANGED_FILES_FILE" ]] || { echo "❌ Use either --pr-base or --changed-files-file, not both" >&2; exit 2; }
170
+ [[ -z "$CHANGED_FILES_FILE" || -f "$CHANGED_FILES_FILE" ]] || { echo "❌ Changed-files file not found: $CHANGED_FILES_FILE" >&2; exit 2; }
171
+ PR_PLAN_PATH="$PR_PLAN_OUT"
172
+ [[ -n "$PR_PLAN_PATH" ]] || PR_PLAN_PATH="$(mktemp "${TMPDIR:-/tmp}/tapp-pr-plan.XXXXXX")"
173
+ PR_SELECTION_PATH="$(mktemp "${TMPDIR:-/tmp}/tapp-pr-selection.XXXXXX")"
174
+ PR_TARGET_PATH="$(mktemp "${TMPDIR:-/tmp}/tapp-pr-target.XXXXXX")"
175
+ PR_ARGS=(--project-dir "$PROJECT_DIR" --platform "$PLATFORM" --head "$PR_HEAD" --json-out "$PR_PLAN_PATH" --selection-out "$PR_SELECTION_PATH" --exploration-target-out "$PR_TARGET_PATH")
176
+ [[ -n "$PR_BASE" ]] && PR_ARGS+=(--base "$PR_BASE")
177
+ [[ -n "$CHANGED_FILES_FILE" ]] && PR_ARGS+=(--changed-files-file "$CHANGED_FILES_FILE")
178
+ for contract in ${CONTRACT_FILES[@]+"${CONTRACT_FILES[@]}"}; do PR_ARGS+=(--contract "$contract"); done
179
+ node "$ROOT/scripts/pr-plan.js" "${PR_ARGS[@]}" || exit 2
180
+ SELECTED_CONTRACT_FILES=()
181
+ SELECTED_CONTRACT_COUNT=0
182
+ while IFS= read -r -d '' contract; do SELECTED_CONTRACT_FILES+=("$contract"); SELECTED_CONTRACT_COUNT=$((SELECTED_CONTRACT_COUNT + 1)); done < <(
183
+ python3 -c 'import json,sys; [sys.stdout.buffer.write(str(p).encode()+b"\0") for p in json.load(open(sys.argv[1]))]' "$PR_SELECTION_PATH"
184
+ )
185
+ CONTRACT_FILES=()
186
+ for contract in ${SELECTED_CONTRACT_FILES[@]+"${SELECTED_CONTRACT_FILES[@]}"}; do CONTRACT_FILES+=("$contract"); done
187
+ if [[ "$PLATFORM" == "ios" ]]; then
188
+ IOS_PR_TARGET_JSON="$(python3 -c 'import json,sys; value=json.load(open(sys.argv[1])); print(json.dumps(value, separators=(",", ":")) if isinstance(value, dict) else "")' "$PR_TARGET_PATH")" || exit 2
189
+ fi
190
+ echo "PR selection: $SELECTED_CONTRACT_COUNT release contract(s); plan: $PR_PLAN_PATH"
191
+ fi
192
+
193
+ # Web and Android share the report/gate with iOS but do not need the Xcode
194
+ # simulator orchestration below. Pass already-expanded Flow paths as argv—not a
195
+ # shell expression—to keep CI inputs inert.
196
+ if [[ "$PLATFORM" != "ios" ]]; then
197
+ PLATFORM_ARGS=(--platform "$PLATFORM" --actions "$ACTIONS" --timeout "$TIMEOUT" --fail-on "$FAIL_ON")
198
+ [[ -n "$URL" ]] && PLATFORM_ARGS+=(--url "$URL")
199
+ [[ -n "$PROJECT_DIR" ]] && PLATFORM_ARGS+=(--project-dir "$PROJECT_DIR")
200
+ [[ -n "$WEB_TARGET" ]] && PLATFORM_ARGS+=(--web-target "$WEB_TARGET")
201
+ [[ -n "$TARGET_KEY" ]] && PLATFORM_ARGS+=(--target-key "$TARGET_KEY")
202
+ [[ -n "$APP_ID" ]] && PLATFORM_ARGS+=(--app-id "$APP_ID")
203
+ [[ -n "$APK_PATH" ]] && PLATFORM_ARGS+=(--apk "$APK_PATH")
204
+ [[ -n "$SERIAL" ]] && PLATFORM_ARGS+=(--serial "$SERIAL")
205
+ [[ -n "$BASELINE" ]] && PLATFORM_ARGS+=(--baseline "$BASELINE")
206
+ [[ -n "$JSON_OUT" ]] && PLATFORM_ARGS+=(--json-out "$JSON_OUT")
207
+ [[ -n "$MD_OUT" ]] && PLATFORM_ARGS+=(--md-out "$MD_OUT")
208
+ [[ -n "$PR_PLAN_PATH" ]] && PLATFORM_ARGS+=(--pr-plan "$PR_PLAN_PATH")
209
+ for flow in ${FLOW_FILES[@]+"${FLOW_FILES[@]}"}; do PLATFORM_ARGS+=(--flow "$flow"); done
210
+ for scenario in ${SCENARIO_FILES[@]+"${SCENARIO_FILES[@]}"}; do PLATFORM_ARGS+=(--scenario "$scenario"); done
211
+ for contract in ${CONTRACT_FILES[@]+"${CONTRACT_FILES[@]}"}; do PLATFORM_ARGS+=(--contract "$contract"); done
212
+ exec node "$ROOT/scripts/platform-gate.js" "${PLATFORM_ARGS[@]}"
213
+ fi
214
+
215
+ # Compile only iOS-applicable contracts before spending simulator time. The
216
+ # compiler exits 3 for reviewed contracts that target another platform.
217
+ CONTRACT_COMPILED_FILES=()
218
+ if [[ "${#CONTRACT_FILES[@]}" -gt 0 ]]; then
219
+ CONTRACT_BUILD_DIR="$(mktemp -d /tmp/tapp-ci-contracts.XXXXXX)"
220
+ for contract in "${CONTRACT_FILES[@]}"; do
221
+ compiled="$CONTRACT_BUILD_DIR/$(basename "$contract" .contract.ts).json"
222
+ set +e
223
+ node "$ROOT/scripts/compile-contract.js" "$contract" ios "$compiled"
224
+ contract_status=$?
225
+ set -e
226
+ [[ "$contract_status" -eq 3 ]] && continue
227
+ [[ "$contract_status" -eq 0 ]] || { echo "❌ Could not compile release contract: $contract" >&2; exit 2; }
228
+ CONTRACT_COMPILED_FILES+=("$compiled")
229
+ done
230
+ fi
231
+
232
+ [[ -n "$APP_PATH" && -d "$APP_PATH" ]] || { echo "❌ Required: --app <path/to/App.app> (a simulator build)" >&2; exit 2; }
233
+ if [[ -z "$BUNDLE_ID" ]]; then
234
+ BUNDLE_ID="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP_PATH/Info.plist" 2>/dev/null || true)"
235
+ [[ -n "$BUNDLE_ID" ]] || { echo "❌ Could not detect CFBundleIdentifier from $APP_PATH/Info.plist; pass --bundle-id" >&2; exit 2; }
236
+ echo "Detected bundle id: $BUNDLE_ID"
237
+ fi
238
+
239
+ step() { echo ""; echo "━━━ $1"; }
240
+
241
+ # ── Simulator: reuse a booted one, else boot (creating from the newest runtime if necessary).
242
+ step "Simulator"
243
+ UDID="$(xcrun simctl list devices booted -j | 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"), ""))')"
244
+ if [[ -z "$UDID" ]]; then
245
+ UDID="$(xcrun simctl list devices available -j | python3 -c "
246
+ import sys, json
247
+ d = json.load(sys.stdin)
248
+ want = '''$DEVICE'''
249
+ cands = [(rt, x) for rt, v in d['devices'].items() for x in v if x.get('isAvailable', True)]
250
+ named = [x['udid'] for rt, x in cands if x['name'] == want]
251
+ iphones = [x['udid'] for rt, x in sorted(cands, key=lambda p: p[0], reverse=True) if x['name'].startswith('iPhone')]
252
+ print(named[0] if named else (iphones[0] if iphones else ''))
253
+ ")"
254
+ if [[ -z "$UDID" ]]; then
255
+ 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
+ [[ -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; }
258
+ fi
259
+ echo "Booting $UDID …"
260
+ xcrun simctl boot "$UDID" || true
261
+ xcrun simctl bootstatus "$UDID" -b || { echo "❌ Simulator failed to boot" >&2; exit 1; }
262
+ fi
263
+ echo "Simulator: $UDID"
264
+
265
+ # ── Install the app build under test.
266
+ step "Install $BUNDLE_ID"
267
+ xcrun simctl install "$UDID" "$APP_PATH" || { echo "❌ simctl install failed — is $APP_PATH a SIMULATOR build?" >&2; exit 1; }
268
+
269
+ # ── Autonomous exploration (quick-capture builds the harness itself if needed).
270
+ step "Explore ($ACTIONS actions, ${TIMEOUT}s watchdog)"
271
+ set +e
272
+ CAPTURE_ROOT="${AUTOTAP_HOME:-$ROOT}/captures"
273
+ mkdir -p "$CAPTURE_ROOT"
274
+ CAPTURE_DIR="$(mktemp -d "$CAPTURE_ROOT/ci.XXXXXX")"
275
+ 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"
276
+ set -e
277
+ MARKERS="$CAPTURE_DIR/ocqa-markers.txt"
278
+ [[ -f "$MARKERS" ]] || { echo "❌ Exploration produced no markers ($MARKERS)" >&2; exit 1; }
279
+ echo "Markers: $MARKERS"
280
+
281
+ # ── Replay committed Flows (each failure becomes a gate reason).
282
+ FLOW_LOG_ARGS=()
283
+ if [[ "${#FLOW_FILES[@]}" -gt 0 ]]; then
284
+ step "Flows"
285
+ FLOW_LOG_DIR="$(mktemp -d /tmp/autotap-ci-flows.XXXXXX)"
286
+ for flow in "${FLOW_FILES[@]}"; do
287
+ name="$(basename "$flow" .yml)"
288
+ log="$FLOW_LOG_DIR/$name.log"
289
+ echo "▶️ $name"
290
+ FLOW_LOG="$log" "$ROOT/scripts/run-flow.sh" "$flow" "$BUNDLE_ID" || true # verdict comes from the log
291
+ FLOW_LOG_ARGS+=(--flow-log "$log")
292
+ done
293
+ elif [[ -n "$FLOWS" ]]; then
294
+ echo "No committed Flows found; continuing with autonomous exploration only."
295
+ fi
296
+
297
+ if [[ "${#CONTRACT_COMPILED_FILES[@]}" -gt 0 ]]; then
298
+ step "Release Contracts"
299
+ CONTRACT_LOG_DIR="$(mktemp -d /tmp/tapp-ci-contract-logs.XXXXXX)"
300
+ for contract in "${CONTRACT_COMPILED_FILES[@]}"; do
301
+ name="$(basename "$contract" .json)"
302
+ log="$CONTRACT_LOG_DIR/$name.log"
303
+ echo "▶️ $name"
304
+ FLOW_LOG="$log" "$ROOT/scripts/run-flow.sh" "$contract" "$BUNDLE_ID" || true
305
+ FLOW_LOG_ARGS+=(--contract-log "$log")
306
+ done
307
+ fi
308
+
309
+ # ── Report + gate.
310
+ step "Gate"
311
+ BASELINE_ARGS=()
312
+ [[ -n "$BASELINE" ]] && BASELINE_ARGS=(--baseline "$BASELINE")
313
+ JSON_ARGS=()
314
+ [[ -n "$JSON_OUT" ]] && JSON_ARGS=(--json-out "$JSON_OUT")
315
+ MD_ARGS=()
316
+ [[ -n "$MD_OUT" ]] && MD_ARGS=(--md-out "$MD_OUT")
317
+ PR_PLAN_ARGS=()
318
+ [[ -n "$PR_PLAN_PATH" ]] && PR_PLAN_ARGS=(--pr-plan "$PR_PLAN_PATH")
319
+ TARGET_KEY_ARGS=()
320
+ [[ -n "$TARGET_KEY" ]] && TARGET_KEY_ARGS=(--target-key "$TARGET_KEY")
321
+ node "$ROOT/mcp-server/src/ci-report.js" --markers "$MARKERS" --fail-on "$FAIL_ON" \
322
+ --html-dir "$CAPTURE_DIR" --label "$BUNDLE_ID" \
323
+ ${BASELINE_ARGS[@]+"${BASELINE_ARGS[@]}"} ${JSON_ARGS[@]+"${JSON_ARGS[@]}"} ${MD_ARGS[@]+"${MD_ARGS[@]}"} ${PR_PLAN_ARGS[@]+"${PR_PLAN_ARGS[@]}"} ${TARGET_KEY_ARGS[@]+"${TARGET_KEY_ARGS[@]}"} ${FLOW_LOG_ARGS[@]+"${FLOW_LOG_ARGS[@]}"}
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env bash
2
+ # Reclaim disk space consumed by Xcode build artifacts.
3
+ #
4
+ # Default run (no flags): deletes unavailable simulators and stale DerivedData
5
+ # from unrelated projects. Flags unlock more aggressive passes.
6
+ #
7
+ # Usage:
8
+ # scripts/cleanup-xcode.sh # safe defaults
9
+ # scripts/cleanup-xcode.sh --dry-run # preview only, nothing deleted
10
+ # scripts/cleanup-xcode.sh --device-support # also wipe iOS DeviceSupport caches
11
+ # scripts/cleanup-xcode.sh --all # everything above + Xcode caches
12
+ #
13
+ # AutoTap-related DerivedData (OCQAHarness, DemoApp, LoginDemo, etc.) is always
14
+ # preserved. The local build/ directory in this repo is never touched.
15
+ set -uo pipefail
16
+
17
+ DRY_RUN=0
18
+ CLEAN_DEVICE_SUPPORT=0
19
+ CLEAN_ALL=0
20
+
21
+ for arg in "$@"; do
22
+ case "$arg" in
23
+ --dry-run) DRY_RUN=1 ;;
24
+ --device-support) CLEAN_DEVICE_SUPPORT=1 ;;
25
+ --all) CLEAN_DEVICE_SUPPORT=1; CLEAN_ALL=1 ;;
26
+ -h|--help)
27
+ sed -n '/^# /s/^# //p' "$0" | head -15
28
+ exit 0 ;;
29
+ *) echo "Unknown flag: $arg (try --help)"; exit 1 ;;
30
+ esac
31
+ done
32
+
33
+ DERIVED="$HOME/Library/Developer/Xcode/DerivedData"
34
+ SIMS="$HOME/Library/Developer/CoreSimulator/Devices"
35
+ DEVICE_SUPPORT="$HOME/Library/Developer/Xcode/iOS DeviceSupport"
36
+ XCODE_CACHES="$HOME/Library/Caches/com.apple.dt.Xcode"
37
+
38
+ # Projects to preserve in DerivedData (all AutoTap-related build products).
39
+ KEEP_PREFIXES=("AutoTap" "OCQAHarness" "DemoApp" "LoginDemo" "WizardDemo"
40
+ "ShopDemo" "RestaurantDemo" "SymbolCache")
41
+
42
+ hr() { printf '%0.s─' {1..60}; echo; }
43
+ human() { du -sh "$1" 2>/dev/null | cut -f1; }
44
+
45
+ run() {
46
+ if [[ $DRY_RUN -eq 1 ]]; then
47
+ echo " [dry-run] $*"
48
+ else
49
+ eval "$@"
50
+ fi
51
+ }
52
+
53
+ echo ""
54
+ hr
55
+ echo " Xcode Cleanup $(date '+%Y-%m-%d %H:%M')"
56
+ [[ $DRY_RUN -eq 1 ]] && echo " DRY RUN — nothing will be deleted"
57
+ hr
58
+
59
+ # ── Before snapshot ──────────────────────────────────────────────────────────
60
+ echo ""
61
+ echo "Before:"
62
+ printf " %-38s %s\n" "CoreSimulator/Devices" "$(human "$SIMS")"
63
+ printf " %-38s %s\n" "Xcode/DerivedData" "$(human "$DERIVED")"
64
+ printf " %-38s %s\n" "Xcode/iOS DeviceSupport" "$(human "$DEVICE_SUPPORT")"
65
+ printf " %-38s %s\n" "Xcode caches" "$(human "$XCODE_CACHES")"
66
+ echo ""
67
+
68
+ # ── 1. Unavailable simulators ─────────────────────────────────────────────────
69
+ echo "1/3 Removing unavailable simulators …"
70
+ UNAVAIL=$(xcrun simctl list devices --json 2>/dev/null | \
71
+ python3 -c '
72
+ import sys, json
73
+ d = json.load(sys.stdin)
74
+ n = sum(1 for v in d["devices"].values() for x in v if not x.get("isAvailable", True))
75
+ print(n)
76
+ ')
77
+ echo " Found $UNAVAIL unavailable device(s)."
78
+ if [[ "$UNAVAIL" -gt 0 ]]; then
79
+ run "xcrun simctl delete unavailable"
80
+ fi
81
+
82
+ # ── 2. Unrelated DerivedData ──────────────────────────────────────────────────
83
+ echo ""
84
+ echo "2/3 Scanning DerivedData for unrelated projects …"
85
+ STALE=()
86
+ if [[ -d "$DERIVED" ]]; then
87
+ while IFS= read -r -d '' entry; do
88
+ base="$(basename "$entry")"
89
+ keep=0
90
+ for prefix in "${KEEP_PREFIXES[@]}"; do
91
+ if [[ "$base" == "$prefix"* ]]; then
92
+ keep=1; break
93
+ fi
94
+ done
95
+ [[ $keep -eq 0 ]] && STALE+=("$entry")
96
+ done < <(find "$DERIVED" -maxdepth 1 -mindepth 1 -type d -print0)
97
+ fi
98
+
99
+ if [[ ${#STALE[@]} -eq 0 ]]; then
100
+ echo " Nothing to remove."
101
+ else
102
+ echo " Will remove ${#STALE[@]} unrelated build folder(s):"
103
+ for entry in "${STALE[@]}"; do
104
+ printf " %-8s %s\n" "$(human "$entry")" "$(basename "$entry")"
105
+ done
106
+ for entry in "${STALE[@]}"; do
107
+ run "rm -rf \"$entry\""
108
+ done
109
+ fi
110
+
111
+ # ── 3. iOS DeviceSupport (optional) ──────────────────────────────────────────
112
+ echo ""
113
+ echo "3/3 iOS DeviceSupport …"
114
+ if [[ $CLEAN_DEVICE_SUPPORT -eq 1 ]]; then
115
+ if [[ -d "$DEVICE_SUPPORT" ]]; then
116
+ echo " Removing $(human "$DEVICE_SUPPORT") of device symbol caches …"
117
+ echo " (Xcode re-downloads these automatically when a device is connected.)"
118
+ run "rm -rf \"$DEVICE_SUPPORT\""
119
+ else
120
+ echo " Directory not found — nothing to do."
121
+ fi
122
+ else
123
+ SIZE=$(human "$DEVICE_SUPPORT")
124
+ echo " Skipped ($SIZE on disk). Pass --device-support to remove."
125
+ fi
126
+
127
+ # ── 4. Xcode caches (--all only) ─────────────────────────────────────────────
128
+ if [[ $CLEAN_ALL -eq 1 ]]; then
129
+ echo ""
130
+ echo "4/4 Xcode caches …"
131
+ if [[ -d "$XCODE_CACHES" ]]; then
132
+ echo " Removing $(human "$XCODE_CACHES") …"
133
+ run "rm -rf \"$XCODE_CACHES\""
134
+ fi
135
+ fi
136
+
137
+ # ── Simulator data cache (always safe) ───────────────────────────────────────
138
+ SIM_CACHES="$HOME/Library/Developer/CoreSimulator/Caches"
139
+ if [[ -d "$SIM_CACHES" ]] && [[ "$(du -sk "$SIM_CACHES" 2>/dev/null | cut -f1)" -gt 0 ]]; then
140
+ echo ""
141
+ echo " Removing simulator caches ($(human "$SIM_CACHES")) …"
142
+ run "rm -rf \"$SIM_CACHES\""
143
+ fi
144
+
145
+ # ── After snapshot ─────────────────────────────────────────────────────────────
146
+ echo ""
147
+ if [[ $DRY_RUN -eq 0 ]]; then
148
+ hr
149
+ echo "After:"
150
+ printf " %-38s %s\n" "CoreSimulator/Devices" "$(human "$SIMS")"
151
+ printf " %-38s %s\n" "Xcode/DerivedData" "$(human "$DERIVED")"
152
+ printf " %-38s %s\n" "Xcode/iOS DeviceSupport" "$(human "$DEVICE_SUPPORT")"
153
+ printf " %-38s %s\n" "Xcode caches" "$(human "$XCODE_CACHES")"
154
+ echo ""
155
+ fi
156
+
157
+ echo "Done."
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ // Compile one TypeScript release contract for a concrete CI platform. Exit 3
3
+ // means the reviewed contract does not apply to this platform and should be
4
+ // skipped; every other non-zero exit is a configuration error.
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { compileReleaseContract, loadReleaseContractFile } from "../mcp-server/src/release-contract.js";
8
+
9
+ const [source, platform, output] = process.argv.slice(2);
10
+ if (!source || !platform || !output) {
11
+ console.error("usage: compile-contract.js <name.contract.ts> <ios|android|web> <output.json>");
12
+ process.exit(2);
13
+ }
14
+ try {
15
+ const contract = await loadReleaseContractFile(source);
16
+ if (!contract.platforms.includes(platform)) {
17
+ console.log(`skip ${contract.name}: not applicable to ${platform}`);
18
+ process.exit(3);
19
+ }
20
+ const compiled = compileReleaseContract(contract, { platform, sourcePath: path.resolve(source) });
21
+ fs.mkdirSync(path.dirname(path.resolve(output)), { recursive: true });
22
+ fs.writeFileSync(path.resolve(output), JSON.stringify(compiled, null, 2) + "\n");
23
+ console.log(`compiled ${contract.name}: ${compiled.steps.length} deterministic steps`);
24
+ } catch (error) {
25
+ console.error(error.message || String(error));
26
+ process.exit(2);
27
+ }
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ // Expand repository-native Task calls into the shared deterministic Flow JSON
3
+ // consumed by every platform driver. No model or customer secret is involved.
4
+ import path from "node:path";
5
+ import { loadFlowFile } from "../mcp-server/src/flow-runtime.js";
6
+
7
+ const input = process.argv[2] ? path.resolve(process.argv[2]) : "";
8
+ const platform = String(process.argv[3] || "").toLowerCase();
9
+ if (!input) {
10
+ console.error("usage: compile-flow.js <flow.yml|flow.json> [ios|android|web]");
11
+ process.exit(2);
12
+ }
13
+ try {
14
+ process.stdout.write(JSON.stringify(loadFlowFile(input, { platform })));
15
+ } catch (error) {
16
+ console.error(error.message || String(error));
17
+ process.exit(2);
18
+ }
@@ -0,0 +1,9 @@
1
+ # Corpus sweep list — curated from dkhamsing/open-source-ios-apps across backend classes.
2
+ # Class coverage: self-hostable-service client (login wall), public API (no key),
3
+ # public API (keyed), no-backend/local-first, heavyweight real-world.
4
+ https://github.com/Dimillian/IceCubesApp
5
+ https://github.com/weiran/Hackers
6
+ https://github.com/Ranchero-Software/NetNewsWire
7
+ https://github.com/yattee/yattee
8
+ https://github.com/wikimedia/wikipedia-ios
9
+ https://github.com/mastodon/mastodon-ios