@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.
- package/AGENTS.md +123 -0
- package/Harness/OCQAHarness/AppDelegate.swift +21 -0
- package/Harness/OCQAHarness/Info.plist +26 -0
- package/Harness/OCQAHarness.xcodeproj/project.pbxproj +199 -0
- package/Harness/OCQAHarness.xcodeproj/xcshareddata/xcschemes/OCQAHarnessUITests.xcscheme +22 -0
- package/Harness/OCQAHarnessUITests/ExplorerTests.swift +4526 -0
- package/Harness/OCQAHarnessUITests/Info.plist +22 -0
- package/Harness/generate-harness-xcodeproj.rb +254 -0
- package/LICENSE +21 -0
- package/README.md +374 -0
- package/bin/tapp.js +1382 -0
- package/browser/app.css +227 -0
- package/browser/app.js +675 -0
- package/browser/index.html +195 -0
- package/browser/product-contract.js +25 -0
- package/browser/view-model.js +16 -0
- package/docs/BROWSER-PRODUCT.md +72 -0
- package/docs/PRODUCT-ENGINE.md +102 -0
- package/docs/application-model.md +276 -0
- package/docs/scenarios.md +95 -0
- package/mcp-server/src/android-driver.js +287 -0
- package/mcp-server/src/android-explorer.js +197 -0
- package/mcp-server/src/android-flow.js +89 -0
- package/mcp-server/src/application-model.js +1597 -0
- package/mcp-server/src/browser-product.js +659 -0
- package/mcp-server/src/browser-workspaces.js +234 -0
- package/mcp-server/src/ci-report.js +557 -0
- package/mcp-server/src/ci-setup.js +359 -0
- package/mcp-server/src/contract-authoring.js +10 -0
- package/mcp-server/src/enrich.js +57 -0
- package/mcp-server/src/flow-runtime.js +127 -0
- package/mcp-server/src/html-report.js +124 -0
- package/mcp-server/src/index.js +3775 -0
- package/mcp-server/src/maintenance-proposal.js +178 -0
- package/mcp-server/src/managed-operation.js +61 -0
- package/mcp-server/src/pr-selection.js +841 -0
- package/mcp-server/src/product-execution.js +155 -0
- package/mcp-server/src/product-operations.js +526 -0
- package/mcp-server/src/project-config.js +101 -0
- package/mcp-server/src/release-contract.d.ts +81 -0
- package/mcp-server/src/release-contract.js +226 -0
- package/mcp-server/src/report.js +363 -0
- package/mcp-server/src/scenario-runtime.js +139 -0
- package/mcp-server/src/static-server.js +44 -0
- package/mcp-server/src/task-runtime.js +266 -0
- package/mcp-server/src/ui-map.js +661 -0
- package/mcp-server/src/web-explorer.js +493 -0
- package/mcp-server/src/web-flow.js +238 -0
- package/package.json +82 -0
- package/scripts/android-corpus-e2e.sh +30 -0
- package/scripts/ci-gate.sh +323 -0
- package/scripts/cleanup-xcode.sh +157 -0
- package/scripts/compile-contract.js +27 -0
- package/scripts/compile-flow.js +18 -0
- package/scripts/corpus-apps.txt +9 -0
- package/scripts/corpus-sweep.sh +121 -0
- package/scripts/coverage-eval.sh +92 -0
- package/scripts/coverage_eval_parse.py +95 -0
- package/scripts/deploy-and-build.sh +99 -0
- package/scripts/flow-platform.js +18 -0
- package/scripts/flow_ai_judge.py +102 -0
- package/scripts/flow_lib.py +154 -0
- package/scripts/mutation-recall-desktop.sh +186 -0
- package/scripts/mutation-recall.sh +121 -0
- package/scripts/mutation_lib.py +128 -0
- package/scripts/mutation_operators.py +144 -0
- package/scripts/platform-gate.js +186 -0
- package/scripts/pr-plan.js +68 -0
- package/scripts/quick-capture.sh +419 -0
- package/scripts/run-android-flow.js +27 -0
- package/scripts/run-flow.sh +90 -0
- package/scripts/run-web-flow.js +28 -0
- package/scripts/run-web-scenario.js +23 -0
- package/scripts/validation-matrix.sh +146 -0
- package/scripts/vision-fp-eval.sh +206 -0
- package/scripts/vision_escalation_responder.py +147 -0
- package/scripts/vision_fp_probe.py +221 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Vision false-positive probe — reviews screenshots with the SAME prompt/model AutoTap's vision
|
|
3
|
+
pass uses, and tallies the visual findings it returns.
|
|
4
|
+
|
|
5
|
+
Why: the vision pass (VisionInspector.swift) is disabled by default. Before defaulting it on we must
|
|
6
|
+
know its FALSE-POSITIVE rate — how often it invents a "defect" on a screen that's actually fine. Our
|
|
7
|
+
corpus apps are standard SwiftUI (visually clean; their fixtures are LOGICAL/interaction bugs, not
|
|
8
|
+
visual ones), so on that corpus every returned finding is a candidate false positive. This probe
|
|
9
|
+
reports that count so the rate is measurable.
|
|
10
|
+
|
|
11
|
+
Source of truth for the prompt/parse is AutoTap/Services/VisionInspector.swift — kept in sync here
|
|
12
|
+
(same pattern as coverage_eval_parse.py mirroring the Swift parse, and the MCP regression mirror).
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
ANTHROPIC_API_KEY=... python3 vision_fp_probe.py IMG.png [IMG2.png ...]
|
|
16
|
+
ANTHROPIC_API_KEY=... python3 vision_fp_probe.py --title-map manifest.json IMG.png ...
|
|
17
|
+
|
|
18
|
+
Emits a JSON report to stdout: per-image findings + an aggregate {reviewed, flagged, findings,
|
|
19
|
+
by_severity}. Exit 0 always (diagnostic, not a gate).
|
|
20
|
+
"""
|
|
21
|
+
import base64
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import ssl
|
|
25
|
+
import sys
|
|
26
|
+
import urllib.request
|
|
27
|
+
|
|
28
|
+
# macOS system Python often can't verify TLS against the system keychain — use certifi's CA bundle so
|
|
29
|
+
# the API calls actually connect. Without this, every request fails SSL and the probe would silently
|
|
30
|
+
# report "0 findings" (a false clean). If certifi is missing, fall back to SSL_CERT_FILE/default.
|
|
31
|
+
def _ssl_context():
|
|
32
|
+
try:
|
|
33
|
+
import certifi
|
|
34
|
+
return ssl.create_default_context(cafile=certifi.where())
|
|
35
|
+
except Exception:
|
|
36
|
+
return ssl.create_default_context()
|
|
37
|
+
|
|
38
|
+
_SSL_CTX = _ssl_context()
|
|
39
|
+
|
|
40
|
+
# Mirror of AnthropicVisionInspector.systemPrompt (VisionInspector.swift) — keep in sync.
|
|
41
|
+
SYSTEM_PROMPT = (
|
|
42
|
+
"You are a meticulous mobile-app visual QA inspector reviewing ONE iOS app screenshot captured "
|
|
43
|
+
"during automated exploration. Report only real, SHIPPED visual defects a user would hit: text "
|
|
44
|
+
"overlapping other text, text clipped mid-character with no ellipsis, text running off-screen, "
|
|
45
|
+
"broken or missing images/icons, controls misaligned or overlapping other controls, unreadable "
|
|
46
|
+
"contrast, lorem/placeholder copy shipped in labels, or a broken empty/error state.\n\n"
|
|
47
|
+
"The screen was captured MID-INTERACTION, so do NOT report these EXPECTED, non-defect states:\n"
|
|
48
|
+
"- The on-screen keyboard covering the lower part of the screen (normal when a field is focused).\n"
|
|
49
|
+
"- An open menu, dropdown, picker, popover, or sheet overlapping the content beneath it — and "
|
|
50
|
+
"text or controls partially COVERED by that open element are occluded, not clipped or broken; "
|
|
51
|
+
"report neither the overlap nor the covered text as a defect.\n"
|
|
52
|
+
"- A navigation back button that shows the previous screen's title next to the chevron.\n"
|
|
53
|
+
"- Values typed into fields during testing (e.g. \"test\", \"test@example.com\", \"5551234567\") "
|
|
54
|
+
"— that is test input, not shipped placeholder text.\n"
|
|
55
|
+
"- A disabled or greyed-out button/control — that is a valid state, not a defect.\n"
|
|
56
|
+
"- A field showing its own label as placeholder, or text cleanly truncated with an ellipsis (…) "
|
|
57
|
+
"— that is intentional.\n"
|
|
58
|
+
"- Text or a card cut off at the very TOP or BOTTOM edge of a scrollable screen — that content "
|
|
59
|
+
"simply continues off-screen when scrolled; only flag text clipped WITHIN its own container.\n\n"
|
|
60
|
+
"You may also be given ACCESSIBILITY CONTEXT — the text/fields/actions the a11y tree reports on "
|
|
61
|
+
"this screen. Use it to REJECT false positives: if the COMPLETE text appears in that context, it "
|
|
62
|
+
"is rendered and available — NEVER report it as clipped, truncated, or cut off, no matter how it "
|
|
63
|
+
"looks to you; if a field is listed with no "
|
|
64
|
+
"value, an empty field showing just its label is expected. Report a defect only when you can SEE "
|
|
65
|
+
"it and the accessibility context does not explain it away.\n\n"
|
|
66
|
+
"Content is NEVER a rendering defect, no matter how it looks. You cannot know if a name, word, "
|
|
67
|
+
"email address, or other text is \"correct\" — you only know what pixels are rendered. NEVER "
|
|
68
|
+
"report a name or word as \"misspelled\", \"wrong\", or a data/binding error, and NEVER file it "
|
|
69
|
+
"under text_clipping/visual_regression/etc., because it looks unusual, made-up, or like a typo "
|
|
70
|
+
"(e.g. an odd client name, an auto-generated email/relay address, a random ID, two different "
|
|
71
|
+
"people sharing a first name) — that may be real content rendering exactly as intended, not a "
|
|
72
|
+
"rendering bug, even if it looks strange. Only report text_clipping when you can see an ACTUAL "
|
|
73
|
+
"rendering artifact: characters physically cut off mid-glyph, text overflowing its container "
|
|
74
|
+
"with no ellipsis, or overlapping other elements — never based on what the text says. A "
|
|
75
|
+
"text_clipping report MUST name, in \"detail\", the exact visible characters or words that are "
|
|
76
|
+
"missing or cut; if you cannot say what is missing, it is not clipped — do not report it.\n\n"
|
|
77
|
+
"You MAY separately flag content that looks unusual enough to be worth a human glance — use the "
|
|
78
|
+
"\"content_flag\" category for this, ALWAYS at \"low\" severity, and phrase it as a question, not "
|
|
79
|
+
"a diagnosis (e.g. \"Verify this client name is intentional: 'Daviad'\" — NOT \"Client name "
|
|
80
|
+
"misspelled\"). A content_flag is not a bug report: it says \"a human should confirm this is real "
|
|
81
|
+
"data\", nothing more. Use it sparingly — only for content a reasonable person would pause on, "
|
|
82
|
+
"not every name that isn't a common English word.\n\n"
|
|
83
|
+
"Do NOT report subjective style opinions or anything you are unsure is a defect. When in doubt, "
|
|
84
|
+
"do not report it. Respond with ONLY a JSON array (no prose) of objects: {\"severity\":"
|
|
85
|
+
"\"low|medium|high\",\"category\":\"layout_overlap|text_clipping|missing_asset|blank_screen|"
|
|
86
|
+
"visual_regression|content_flag\",\"title\":\"short\",\"detail\":\"what and where\"}. If the "
|
|
87
|
+
"screen looks fine, respond with []."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Sonnet default mirrors makeVisionInspector (VisionInspector.swift): Haiku hallucinated a
|
|
91
|
+
# clipping defect 6/6 trials on a clean settled screenshot across two prompt variants; Sonnet
|
|
92
|
+
# was clean 6/6 on both. Model-bound FP class — keep the eval on the product's real default.
|
|
93
|
+
MODEL = os.environ.get("AUTOTAP_VISION_MODEL", "claude-sonnet-4-6")
|
|
94
|
+
ENDPOINT = "https://api.anthropic.com/v1/messages"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def parse_findings(text):
|
|
98
|
+
"""Mirror of parseVisionFindings: extract the first top-level JSON array, tolerate fences/prose."""
|
|
99
|
+
start = text.find("[")
|
|
100
|
+
end = text.rfind("]")
|
|
101
|
+
if start < 0 or end <= start:
|
|
102
|
+
return []
|
|
103
|
+
try:
|
|
104
|
+
arr = json.loads(text[start:end + 1])
|
|
105
|
+
except Exception:
|
|
106
|
+
return []
|
|
107
|
+
out = []
|
|
108
|
+
for obj in arr if isinstance(arr, list) else []:
|
|
109
|
+
if not isinstance(obj, dict):
|
|
110
|
+
continue
|
|
111
|
+
title = str(obj.get("title", "")).strip()
|
|
112
|
+
if not title:
|
|
113
|
+
continue
|
|
114
|
+
category = str(obj.get("category", "visual_regression")).lower()
|
|
115
|
+
# content_flag is never a confirmed defect — cap it at low regardless of what the model
|
|
116
|
+
# said (mirrors parseVisionFindings' clamp in VisionInspector.swift).
|
|
117
|
+
severity = "low" if category == "content_flag" else str(obj.get("severity", "medium")).lower()
|
|
118
|
+
out.append({
|
|
119
|
+
"severity": severity,
|
|
120
|
+
"category": category,
|
|
121
|
+
"title": title,
|
|
122
|
+
"detail": str(obj.get("detail", obj.get("description", ""))).strip(),
|
|
123
|
+
})
|
|
124
|
+
return out
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def review(image_path, title, key, summary=""):
|
|
128
|
+
with open(image_path, "rb") as f:
|
|
129
|
+
b64 = base64.b64encode(f.read()).decode()
|
|
130
|
+
user_text = f"Screen title: {title}."
|
|
131
|
+
if summary:
|
|
132
|
+
user_text += f"\nAccessibility context (what the a11y tree reports here): {summary}"
|
|
133
|
+
user_text += "\nReview this screenshot and report visual defects as the JSON array."
|
|
134
|
+
body = {
|
|
135
|
+
"model": MODEL,
|
|
136
|
+
"max_tokens": 1024,
|
|
137
|
+
"system": SYSTEM_PROMPT,
|
|
138
|
+
"messages": [{
|
|
139
|
+
"role": "user",
|
|
140
|
+
"content": [
|
|
141
|
+
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": b64}},
|
|
142
|
+
{"type": "text", "text": user_text},
|
|
143
|
+
],
|
|
144
|
+
}],
|
|
145
|
+
}
|
|
146
|
+
req = urllib.request.Request(ENDPOINT, data=json.dumps(body).encode(), method="POST", headers={
|
|
147
|
+
"x-api-key": key,
|
|
148
|
+
"anthropic-version": "2023-06-01",
|
|
149
|
+
"content-type": "application/json",
|
|
150
|
+
})
|
|
151
|
+
try:
|
|
152
|
+
with urllib.request.urlopen(req, timeout=90, context=_SSL_CTX) as resp:
|
|
153
|
+
data = json.load(resp)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
return {"error": str(e)}, []
|
|
156
|
+
text = "\n".join(b.get("text", "") for b in data.get("content", []) if b.get("type") == "text")
|
|
157
|
+
return {"raw": text}, parse_findings(text)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def main():
|
|
161
|
+
args = sys.argv[1:]
|
|
162
|
+
title_map = {}
|
|
163
|
+
summary_map = {}
|
|
164
|
+
while args and args[0] in ("--title-map", "--summary-map"):
|
|
165
|
+
flag, path = args[0], args[1]
|
|
166
|
+
with open(path) as f:
|
|
167
|
+
data = json.load(f)
|
|
168
|
+
if flag == "--title-map":
|
|
169
|
+
title_map = data
|
|
170
|
+
else:
|
|
171
|
+
summary_map = data
|
|
172
|
+
args = args[2:]
|
|
173
|
+
key = os.environ.get("ANTHROPIC_API_KEY", "")
|
|
174
|
+
if not key:
|
|
175
|
+
print(json.dumps({"error": "ANTHROPIC_API_KEY not set"}))
|
|
176
|
+
return 2
|
|
177
|
+
if not args:
|
|
178
|
+
print(json.dumps({"error": "no images given"}))
|
|
179
|
+
return 2
|
|
180
|
+
|
|
181
|
+
per_image = []
|
|
182
|
+
by_sev = {"high": 0, "medium": 0, "low": 0}
|
|
183
|
+
flagged = 0
|
|
184
|
+
errors = 0
|
|
185
|
+
first_error = None
|
|
186
|
+
for path in args:
|
|
187
|
+
title = title_map.get(os.path.basename(path)) or title_map.get(path) or os.path.splitext(os.path.basename(path))[0]
|
|
188
|
+
summary = summary_map.get(os.path.basename(path)) or summary_map.get(path) or ""
|
|
189
|
+
meta, findings = review(path, title, key, summary=summary)
|
|
190
|
+
if meta.get("error"):
|
|
191
|
+
errors += 1
|
|
192
|
+
first_error = first_error or meta["error"]
|
|
193
|
+
per_image.append({"image": os.path.basename(path), "title": title, "findings": [], "error": meta["error"]})
|
|
194
|
+
continue
|
|
195
|
+
if findings:
|
|
196
|
+
flagged += 1
|
|
197
|
+
for fnd in findings:
|
|
198
|
+
by_sev[fnd["severity"]] = by_sev.get(fnd["severity"], 0) + 1
|
|
199
|
+
per_image.append({"image": os.path.basename(path), "title": title, "findings": findings})
|
|
200
|
+
|
|
201
|
+
reviewed = len(args)
|
|
202
|
+
succeeded = reviewed - errors
|
|
203
|
+
report = {
|
|
204
|
+
"reviewed": reviewed,
|
|
205
|
+
"succeeded": succeeded, # calls that actually reached the model
|
|
206
|
+
"errors": errors, # calls that failed (network/SSL/etc.) — NOT clean screens
|
|
207
|
+
"first_error": first_error,
|
|
208
|
+
"flagged": flagged,
|
|
209
|
+
"findings_total": sum(len(x["findings"]) for x in per_image),
|
|
210
|
+
"by_severity": by_sev,
|
|
211
|
+
# FP rate is over SUCCEEDED calls only — a failed call is not evidence of a clean screen.
|
|
212
|
+
"fp_rate_screens": round(flagged / succeeded, 3) if succeeded else None,
|
|
213
|
+
"images": per_image,
|
|
214
|
+
}
|
|
215
|
+
print(json.dumps(report, indent=2))
|
|
216
|
+
# Non-zero exit when NOTHING succeeded, so a total failure can't read as a pass.
|
|
217
|
+
return 3 if succeeded == 0 else 0
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
sys.exit(main())
|