@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,178 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { semanticUiKey } from "./ui-map.js";
|
|
6
|
+
import { compileReleaseContract, loadReleaseContractFile } from "./release-contract.js";
|
|
7
|
+
import { loadTaskFile } from "./task-runtime.js";
|
|
8
|
+
import { runWebFlow } from "./web-flow.js";
|
|
9
|
+
|
|
10
|
+
const STABLE_SELECTOR_PRIORITY = ["testId", "accessibilityId", "resourceId", "cssId"];
|
|
11
|
+
|
|
12
|
+
function sameNode(nodes, baseline) {
|
|
13
|
+
return (nodes || []).find((node) => node.id === baseline.nodeId)
|
|
14
|
+
|| (nodes || []).find((node) => semanticUiKey(node.semanticKey || node.name) === semanticUiKey(baseline.nodeSemanticKey || baseline.nodeName));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function stableIntersection(before, after) {
|
|
18
|
+
for (const kind of STABLE_SELECTOR_PRIORITY) {
|
|
19
|
+
const oldValues = new Set((before.selectors || []).filter((selector) => selector.kind === kind).map((selector) => selector.value));
|
|
20
|
+
const match = (after.selectors || []).find((selector) => selector.kind === kind && oldValues.has(selector.value));
|
|
21
|
+
if (match) return match;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function currentStillMatchesTarget(control, target) {
|
|
27
|
+
return [control.id, control.semanticKey, control.label, ...(control.selectors || []).map((selector) => selector.value)]
|
|
28
|
+
.some((value) => String(value || "") === String(target || ""));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function proposeSelectorMaintenance({ candidate, execution, currentMap, platform = "" } = {}) {
|
|
32
|
+
const failure = execution?.firstFailure;
|
|
33
|
+
if (!candidate || execution?.status !== "failed" || !failure || !["tap", "type"].includes(failure.action) || !failure.task || !currentMap?.nodes) return null;
|
|
34
|
+
const references = (candidate.selectorReferences || []).filter((reference) =>
|
|
35
|
+
reference.task === failure.task
|
|
36
|
+
&& reference.action === failure.action
|
|
37
|
+
&& semanticUiKey(reference.target) === semanticUiKey(failure.target)
|
|
38
|
+
&& (!platform || reference.platform === platform || reference.platform === "shared"));
|
|
39
|
+
const operations = [];
|
|
40
|
+
for (const reference of references) for (const baseline of reference.baselineControls || []) {
|
|
41
|
+
const node = sameNode(currentMap.nodes, baseline);
|
|
42
|
+
if (!node) continue;
|
|
43
|
+
for (const control of node.controls || []) {
|
|
44
|
+
const shared = stableIntersection(baseline, control);
|
|
45
|
+
if (!shared || currentStillMatchesTarget(control, reference.target) || shared.value === reference.target) continue;
|
|
46
|
+
operations.push({
|
|
47
|
+
op: "replace",
|
|
48
|
+
task: reference.task,
|
|
49
|
+
taskPath: reference.taskPath,
|
|
50
|
+
taskSha256: reference.taskSha256,
|
|
51
|
+
pointer: reference.pointer,
|
|
52
|
+
before: reference.target,
|
|
53
|
+
after: shared.value,
|
|
54
|
+
selector: shared,
|
|
55
|
+
evidence: {
|
|
56
|
+
nodeId: node.id,
|
|
57
|
+
nodeSemanticKey: node.semanticKey,
|
|
58
|
+
baselineLabel: baseline.label,
|
|
59
|
+
currentLabel: control.label,
|
|
60
|
+
baselineControlId: baseline.controlId,
|
|
61
|
+
currentControlId: control.id,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const unique = [...new Map(operations.map((operation) => [`${operation.taskPath}|${operation.pointer}|${operation.after}`, operation])).values()];
|
|
67
|
+
if (unique.length !== 1) return null;
|
|
68
|
+
return {
|
|
69
|
+
kind: "task-maintenance-patch",
|
|
70
|
+
schemaVersion: 1,
|
|
71
|
+
status: "proposed-unvalidated",
|
|
72
|
+
classification: "stable-selector-maintenance-candidate",
|
|
73
|
+
deterministic: true,
|
|
74
|
+
autoApply: false,
|
|
75
|
+
contractIntent: { name: candidate.contract, path: candidate.contractPath, sha256: candidate.contractIntentSha256 },
|
|
76
|
+
failureEvidence: failure,
|
|
77
|
+
operations: unique,
|
|
78
|
+
reason: "The unchanged contract failed on a Task selector. The baseline and current UI Maps preserve one non-label selector on the same semantic state while the visible label changed.",
|
|
79
|
+
requiredReview: "Confirm the UI rename is intentional. The patch may change only the cited Task selector and must preserve the contract digest.",
|
|
80
|
+
requiredValidation: "Apply in a review branch or disposable checkout, replay the unchanged contract on the real target, and accept only with passing evidence. The current gate remains failed.",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function digest(file) {
|
|
85
|
+
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function inside(root, candidate) {
|
|
89
|
+
return candidate === root || candidate.startsWith(root + path.sep);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function pointerParts(pointer) {
|
|
93
|
+
if (typeof pointer !== "string" || !pointer.startsWith("/")) throw new Error("maintenance operation has an invalid pointer");
|
|
94
|
+
return pointer.slice(1).split("/").map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~"));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function applyOperation(document, operation) {
|
|
98
|
+
if (operation?.op !== "replace") throw new Error("only a replace maintenance operation can be validated");
|
|
99
|
+
const parts = pointerParts(operation.pointer);
|
|
100
|
+
let parent = document;
|
|
101
|
+
for (const part of parts.slice(0, -1)) {
|
|
102
|
+
if (!parent || typeof parent !== "object" || !Object.hasOwn(parent, part)) throw new Error(`maintenance pointer does not resolve at '${part}'`);
|
|
103
|
+
parent = parent[part];
|
|
104
|
+
}
|
|
105
|
+
const key = parts.at(-1);
|
|
106
|
+
if (!parent || typeof parent !== "object" || !Object.hasOwn(parent, key)) throw new Error("maintenance pointer does not resolve to a Task value");
|
|
107
|
+
if (parent[key] !== operation.before) throw new Error("Task value no longer matches the digest-pinned maintenance proposal");
|
|
108
|
+
parent[key] = operation.after;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function copyReviewedTasks(sourceDir, destinationDir) {
|
|
112
|
+
fs.mkdirSync(destinationDir, { recursive: true });
|
|
113
|
+
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
|
|
114
|
+
if (!entry.isFile() || !/\.ya?ml$|\.json$/i.test(entry.name)) continue;
|
|
115
|
+
fs.copyFileSync(path.join(sourceDir, entry.name), path.join(destinationDir, entry.name));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Validate a proposal against the still-running changed web application while
|
|
120
|
+
// keeping the repository read-only. Only reviewed Task documents are copied;
|
|
121
|
+
// contract intent is loaded from, hashed in, and never rewritten in the source
|
|
122
|
+
// checkout. A passing replay remains advisory until the user accepts the patch.
|
|
123
|
+
export async function validateWebMaintenanceProposal({ proposal, projectDir, url, evidenceDir = "" } = {}) {
|
|
124
|
+
if (proposal?.kind !== "task-maintenance-patch" || proposal.operations?.length !== 1) throw new Error("validation requires exactly one constrained Task maintenance operation");
|
|
125
|
+
if (!projectDir || !url) throw new Error("web maintenance validation requires projectDir and the running target URL");
|
|
126
|
+
const root = fs.realpathSync(path.resolve(projectDir));
|
|
127
|
+
const operation = proposal.operations[0];
|
|
128
|
+
const taskRoot = fs.realpathSync(path.join(root, ".autotap", "tasks"));
|
|
129
|
+
const sourceTask = fs.realpathSync(path.resolve(root, operation.taskPath));
|
|
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 .autotap/tasks");
|
|
132
|
+
if (!inside(root, sourceContract)) throw new Error("maintenance contract must remain inside the project");
|
|
133
|
+
if (digest(sourceTask) !== operation.taskSha256) throw new Error("Task digest changed after the proposal was created");
|
|
134
|
+
if (digest(sourceContract) !== proposal.contractIntent.sha256) throw new Error("release-contract intent digest changed after the proposal was created");
|
|
135
|
+
|
|
136
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "tapp-maintenance-validation-"));
|
|
137
|
+
const tempTasks = path.join(tempRoot, ".autotap", "tasks");
|
|
138
|
+
const stem = String(proposal.contractIntent.name || "contract").replace(/[^A-Za-z0-9._-]/g, "-");
|
|
139
|
+
const outputDir = evidenceDir ? path.resolve(evidenceDir, stem) : path.join(tempRoot, "evidence");
|
|
140
|
+
const logPath = path.join(outputDir, "validation.log");
|
|
141
|
+
try {
|
|
142
|
+
copyReviewedTasks(taskRoot, tempTasks);
|
|
143
|
+
const tempTask = path.join(tempTasks, path.basename(sourceTask));
|
|
144
|
+
if (digest(tempTask) !== operation.taskSha256) throw new Error("Task changed while the disposable validation registry was being prepared");
|
|
145
|
+
const loadedTask = loadTaskFile(tempTask);
|
|
146
|
+
const { __path: _taskPath, ...taskDocument } = loadedTask;
|
|
147
|
+
applyOperation(taskDocument, operation);
|
|
148
|
+
fs.writeFileSync(tempTask, JSON.stringify(taskDocument, null, 2) + "\n");
|
|
149
|
+
|
|
150
|
+
const contract = await loadReleaseContractFile(sourceContract);
|
|
151
|
+
if (digest(sourceContract) !== proposal.contractIntent.sha256) throw new Error("release-contract intent changed while validation was being prepared");
|
|
152
|
+
if (Object.keys(contract.actors || {}).length !== 1) throw new Error("automatic disposable maintenance validation currently requires a single-actor web contract");
|
|
153
|
+
if (!(contract.setup || []).length || !(contract.teardown || []).length) {
|
|
154
|
+
throw new Error("automatic disposable maintenance validation requires controlled contract setup and teardown");
|
|
155
|
+
}
|
|
156
|
+
const pseudoContractPath = path.join(tempRoot, ".autotap", "contracts", path.basename(sourceContract));
|
|
157
|
+
const execution = compileReleaseContract(contract, { platform: "web", sourcePath: pseudoContractPath });
|
|
158
|
+
const result = await runWebFlow({ flow: execution, url, logPath, screenshotDir: outputDir });
|
|
159
|
+
const contractUnchanged = digest(sourceContract) === proposal.contractIntent.sha256;
|
|
160
|
+
const taskUnchanged = digest(sourceTask) === operation.taskSha256;
|
|
161
|
+
return {
|
|
162
|
+
status: result.passed && contractUnchanged && taskUnchanged ? "passed" : "failed",
|
|
163
|
+
passed: result.passed && contractUnchanged && taskUnchanged,
|
|
164
|
+
platform: "web",
|
|
165
|
+
disposable: true,
|
|
166
|
+
autoApplied: false,
|
|
167
|
+
sourceArtifactsUnchanged: contractUnchanged && taskUnchanged,
|
|
168
|
+
contractIntentSha256: proposal.contractIntent.sha256,
|
|
169
|
+
taskBeforeSha256: operation.taskSha256,
|
|
170
|
+
executed: result.executed,
|
|
171
|
+
total: result.total,
|
|
172
|
+
failed: result.failed,
|
|
173
|
+
evidence: { logPath, screenshotDir: outputDir },
|
|
174
|
+
};
|
|
175
|
+
} finally {
|
|
176
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Versioned transport contract between a Tapp control plane and a managed
|
|
2
|
+
// runner. This carries identity and intent only; target detection, builds,
|
|
3
|
+
// baselines, and verdicts remain owned by shared product operations.
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
export const MANAGED_OPERATION_SCHEMA_VERSION = 1;
|
|
10
|
+
|
|
11
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
12
|
+
const engineVersion = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")).version;
|
|
13
|
+
|
|
14
|
+
function cleanCapabilities(values) {
|
|
15
|
+
return [...new Set((Array.isArray(values) ? values : []).map((value) => String(value || "").trim().toLowerCase()).filter((value) => /^[a-z0-9][a-z0-9._-]{1,63}$/.test(value)))].sort();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createManagedOperationEnvelope({
|
|
19
|
+
id = crypto.randomBytes(12).toString("hex"),
|
|
20
|
+
repository,
|
|
21
|
+
installationId,
|
|
22
|
+
revision,
|
|
23
|
+
operation = "gate",
|
|
24
|
+
platform = "",
|
|
25
|
+
targetId = "",
|
|
26
|
+
capabilities = [],
|
|
27
|
+
inputs = {},
|
|
28
|
+
} = {}) {
|
|
29
|
+
const envelope = {
|
|
30
|
+
kind:"tapp-managed-operation",
|
|
31
|
+
schemaVersion:MANAGED_OPERATION_SCHEMA_VERSION,
|
|
32
|
+
id:String(id || ""),
|
|
33
|
+
repository:{ provider:"github", nameWithOwner:String(repository || ""), installationId:Number(installationId), revision:String(revision || "").toLowerCase() },
|
|
34
|
+
operation:{ name:String(operation || ""), platform:String(platform || "").toLowerCase(), targetId:String(targetId || "") },
|
|
35
|
+
engine:{ version:engineVersion, artifactSchemaVersion:1 },
|
|
36
|
+
capabilities:cleanCapabilities(capabilities),
|
|
37
|
+
inputs:{ actions:Math.max(1, Math.min(200, Number(inputs.actions) || 40)), timeout:Math.max(30, Math.min(3600, Number(inputs.timeout) || 600)), failOn:String(inputs.failOn || "gate") },
|
|
38
|
+
};
|
|
39
|
+
validateManagedOperationEnvelope(envelope);
|
|
40
|
+
return envelope;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function validateManagedOperationEnvelope(envelope) {
|
|
44
|
+
if (!envelope || envelope.kind !== "tapp-managed-operation" || envelope.schemaVersion !== MANAGED_OPERATION_SCHEMA_VERSION) throw new Error(`Unsupported managed-operation envelope; expected schema ${MANAGED_OPERATION_SCHEMA_VERSION}`);
|
|
45
|
+
if (!/^[a-f0-9]{16,64}$/.test(String(envelope.id || ""))) throw new Error("Managed operation id is invalid");
|
|
46
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(String(envelope.repository?.nameWithOwner || ""))) throw new Error("Managed operation repository identity is invalid");
|
|
47
|
+
if (!Number.isSafeInteger(envelope.repository?.installationId) || envelope.repository.installationId < 0) throw new Error("Managed operation installation identity is invalid");
|
|
48
|
+
if (!/^[a-f0-9]{40}$/.test(String(envelope.repository?.revision || ""))) throw new Error("Managed operation requires an exact 40-character Git revision");
|
|
49
|
+
if (!new Set(["inspect", "gate"]).has(envelope.operation?.name)) throw new Error(`Unsupported managed product operation '${envelope.operation?.name || ""}'`);
|
|
50
|
+
if (envelope.operation?.platform && !new Set(["ios", "android", "web"]).has(envelope.operation.platform)) throw new Error("Managed operation platform must be ios, android, or web");
|
|
51
|
+
if (!Array.isArray(envelope.capabilities)) throw new Error("Managed operation capabilities must be an array");
|
|
52
|
+
if (!new Set(["gate", "high", "critical"]).has(envelope.inputs?.failOn)) throw new Error("Managed operation failOn policy is invalid");
|
|
53
|
+
return envelope;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function verifyManagedCheckout(envelope, checkoutRoot) {
|
|
57
|
+
validateManagedOperationEnvelope(envelope);
|
|
58
|
+
const gitHead = path.join(checkoutRoot, ".git", "HEAD");
|
|
59
|
+
if (!fs.existsSync(gitHead)) throw new Error("Managed operation checkout is not a Git worktree");
|
|
60
|
+
return envelope.repository.revision;
|
|
61
|
+
}
|