@aarwitz/tapp 0.15.1 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -4
- package/README.md +27 -30
- package/bin/tapp.js +65 -27
- package/browser/app.js +2 -2
- package/browser/index.html +1 -1
- package/mcp-server/src/application-model.js +32 -26
- package/mcp-server/src/ci-report.js +5 -4
- package/mcp-server/src/ci-setup.js +16 -7
- package/mcp-server/src/index.js +28 -27
- package/mcp-server/src/maintenance-proposal.js +4 -4
- package/mcp-server/src/pr-selection.js +9 -8
- package/mcp-server/src/product-operations.js +19 -17
- package/mcp-server/src/project-config.js +8 -5
- package/mcp-server/src/project-paths.js +32 -0
- package/mcp-server/src/task-runtime.js +14 -10
- package/mcp-server/src/web-explorer.js +59 -0
- package/package.json +2 -2
- package/scripts/ci-gate.sh +12 -7
- package/scripts/flow_lib.py +1 -1
- package/docs/BROWSER-PRODUCT.md +0 -72
- package/docs/PRODUCT-ENGINE.md +0 -102
- package/docs/application-model.md +0 -271
- package/docs/scenarios.md +0 -95
|
@@ -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 =
|
|
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
|
|
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
|
|
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
|
|
69
|
-
return { root, path: configPath, relativePath
|
|
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
|
+
}
|
|
@@ -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
|
|
88
|
-
if (explicitProjectDir)
|
|
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)
|
|
92
|
-
const candidate = path.join(current,
|
|
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
|
|
101
|
-
const taskDir =
|
|
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 `.
|
|
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}
|
|
109
|
-
const proposalDir = proposalSource &&
|
|
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 .
|
|
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`);
|
|
@@ -97,6 +97,65 @@ export function webBrowserLaunchOptions(environment = process.env) {
|
|
|
97
97
|
};
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
// Focused one-screen inspection for the agent-facing `tapp open <url>` and `tapp tree <url>`
|
|
101
|
+
// commands. This deliberately does no exploration or judgment; it opens exactly one page,
|
|
102
|
+
// captures the visible semantic controls, and optionally takes one screenshot.
|
|
103
|
+
export async function inspectWebPage({ url, timeoutMs = NAV_TIMEOUT_MS, screenshot = true }) {
|
|
104
|
+
let target;
|
|
105
|
+
try { target = new URL(url); }
|
|
106
|
+
catch { throw new Error("Web inspection needs a valid http(s) URL"); }
|
|
107
|
+
if (!/^https?:$/.test(target.protocol)) throw new Error("Web inspection needs a valid http(s) URL");
|
|
108
|
+
|
|
109
|
+
const { chromium } = await loadPlaywright();
|
|
110
|
+
const browser = await chromium.launch(webBrowserLaunchOptions());
|
|
111
|
+
try {
|
|
112
|
+
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
|
113
|
+
const page = await context.newPage();
|
|
114
|
+
const boundedTimeout = Math.max(1000, Math.min(60_000, Number(timeoutMs) || NAV_TIMEOUT_MS));
|
|
115
|
+
page.setDefaultTimeout(boundedTimeout);
|
|
116
|
+
const response = await page.goto(target.href, { waitUntil: "domcontentloaded", timeout: boundedTimeout });
|
|
117
|
+
if (response && response.status() >= 400) throw new Error(`Could not open ${target.href}: HTTP ${response.status()}`);
|
|
118
|
+
await page.waitForTimeout(SETTLE_MS);
|
|
119
|
+
const observed = await page.evaluate(() => {
|
|
120
|
+
const visible = (element) => element.offsetParent !== null;
|
|
121
|
+
const controls = [...document.querySelectorAll("button, a[href], input, textarea, select, [role=button], [role=tab], [role=checkbox], [role=switch]")]
|
|
122
|
+
.filter((element) => element.type !== "hidden" && visible(element))
|
|
123
|
+
.slice(0, 80)
|
|
124
|
+
.map((element) => {
|
|
125
|
+
const tag = element.tagName.toLowerCase();
|
|
126
|
+
const field = ["input", "textarea", "select"].includes(tag);
|
|
127
|
+
const secure = element.type === "password";
|
|
128
|
+
const role = element.getAttribute("role") || (tag === "a" ? "link" : tag === "button" ? "button" : "");
|
|
129
|
+
const label = (element.labels?.[0]?.textContent || element.getAttribute("aria-label") || element.textContent || element.placeholder || element.name || element.id || "").trim().slice(0, 120);
|
|
130
|
+
return {
|
|
131
|
+
type: field ? (secure ? "SecureTextField" : "TextField") : "Button",
|
|
132
|
+
role,
|
|
133
|
+
label,
|
|
134
|
+
identifier: element.id || element.getAttribute("data-testid") || element.getAttribute("aria-label") || "",
|
|
135
|
+
isEnabled: !element.disabled && element.getAttribute("aria-disabled") !== "true",
|
|
136
|
+
hittable: true,
|
|
137
|
+
secure,
|
|
138
|
+
};
|
|
139
|
+
})
|
|
140
|
+
.filter((control) => control.label || control.identifier);
|
|
141
|
+
return {
|
|
142
|
+
heading: document.querySelector("h1")?.textContent?.trim() || "",
|
|
143
|
+
title: document.title.trim(),
|
|
144
|
+
controls,
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
const image = screenshot ? await page.screenshot({ type: "png" }) : null;
|
|
148
|
+
return {
|
|
149
|
+
url: page.url(),
|
|
150
|
+
screenTitle: webScreenTitle(observed, target.pathname || target.href),
|
|
151
|
+
elements: observed.controls,
|
|
152
|
+
image,
|
|
153
|
+
};
|
|
154
|
+
} finally {
|
|
155
|
+
await browser.close().catch(() => {});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
100
159
|
export function normalizeWebSeedRoutes(url, routes, limit = 5) {
|
|
101
160
|
const origin = new URL(url);
|
|
102
161
|
const boundedLimit = Math.max(0, Math.min(10, Number(limit) || 0));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aarwitz/tapp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
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",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"mobile"
|
|
88
88
|
],
|
|
89
89
|
"scripts": {
|
|
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/
|
|
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/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",
|
|
91
91
|
"test:browser-journey": "node --test tests/browser-journey.test.js",
|
|
92
92
|
"test:browser-native": "TAPP_RUN_NATIVE_BROWSER=1 node --test tests/browser-native-journey.test.js"
|
|
93
93
|
}
|
package/scripts/ci-gate.sh
CHANGED
|
@@ -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>/.
|
|
19
|
-
# [--scenarios <glob>] # Multi-actor Scenario YAMLs (web; default: <app repo>/.
|
|
20
|
-
# [--contracts <glob>] # TypeScript release contracts (default: <app repo>/.
|
|
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
|
-
|
|
83
|
-
[[
|
|
84
|
-
[[ -
|
|
85
|
-
[[ -z "$
|
|
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 \
|
package/scripts/flow_lib.py
CHANGED
|
@@ -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 .
|
|
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
|
package/docs/BROWSER-PRODUCT.md
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
# Browser Release Studio
|
|
2
|
-
|
|
3
|
-
Status: current local-product contract as of 2026-08-08.
|
|
4
|
-
|
|
5
|
-
The browser is Tapp's primary customer workflow. Web is also one application target beside iOS and
|
|
6
|
-
Android; it is not a separate QA product. CLI, MCP, VS Code, the Action, desktop, and future managed
|
|
7
|
-
SaaS adapt the shared product operations described in [`PRODUCT-ENGINE.md`](PRODUCT-ENGINE.md).
|
|
8
|
-
|
|
9
|
-
## Start locally
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
npx -y @aarwitz/tapp app
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
Tapp prints an authenticated one-time launch URL and opens it in the default browser. Use
|
|
16
|
-
`--no-open` when copying the URL manually and `--port 4317` only when a fixed loopback port is
|
|
17
|
-
needed. Drag/drop or Browse Folder copies source into a Tapp-owned workspace. Connect GitHub lists
|
|
18
|
-
repositories authorized to the local `gh` session and makes a shallow isolated clone.
|
|
19
|
-
`tapp app /path/to/repo` intentionally works directly in that checkout.
|
|
20
|
-
|
|
21
|
-
The local server binds to `127.0.0.1`. It owns workspace paths; browser requests cannot submit an
|
|
22
|
-
arbitrary server path. Mutations require an `HttpOnly` same-site session cookie, the exact local
|
|
23
|
-
Origin, and an in-memory CSRF token. Application runtimes, repository credentials, and evidence stay
|
|
24
|
-
in the local process/filesystem. This is a local trust boundary, not hosted multi-tenancy.
|
|
25
|
-
|
|
26
|
-
## Product journey
|
|
27
|
-
|
|
28
|
-
1. **Connect** a copied folder, an explicit checkout, or a repository authorized by local `gh`.
|
|
29
|
-
2. **Detect and choose** an iOS, Android, or web target. Continue automatically only when the target
|
|
30
|
-
and configuration are conclusive.
|
|
31
|
-
3. **Build, launch, and explore** the real simulator, emulator/device, or browser surface.
|
|
32
|
-
4. **Understand the UI Map** through observed states, transitions, controls, provenance, and gaps.
|
|
33
|
-
5. **Review intent** by approving, rejecting, deferring, or constraining a compact release plan.
|
|
34
|
-
6. **Generate drafts** of Tasks and contracts. Drafts remain visibly untrusted.
|
|
35
|
-
7. **Validate** approved drafts deterministically against the real target.
|
|
36
|
-
8. **Promote** only validated artifacts into the canonical suite and refreshed Application Model.
|
|
37
|
-
9. **Gate** with autonomous evidence plus the promoted deterministic suite.
|
|
38
|
-
10. **Baseline** only a passing, conclusive, target-scoped gate.
|
|
39
|
-
11. **Install CI** by previewing and writing a reviewable repository patch. Tapp does not commit,
|
|
40
|
-
push, create GitHub secrets, or enable branch protection.
|
|
41
|
-
|
|
42
|
-
Successful semantic actions can be saved in `.autotap/flows/`; credential values are templated to
|
|
43
|
-
environment references. Long-lived repository artifacts store binding names, not resolved secret
|
|
44
|
-
values.
|
|
45
|
-
|
|
46
|
-
## Verified reference journey
|
|
47
|
-
|
|
48
|
-
`tests/browser-journey.test.js` drives the visible local browser against a fresh CommerceDemo copy.
|
|
49
|
-
It exercises startup, a real live web surface and semantic action, UI Map creation, Flow recording
|
|
50
|
-
and replay, proposal review, generation, deterministic validation, promotion, a first gate,
|
|
51
|
-
baseline-aware rerun, and CI preview.
|
|
52
|
-
|
|
53
|
-
The opt-in `tests/browser-native-journey.test.js` passed on 2026-08-06 against a booted iOS
|
|
54
|
-
simulator: the browser built and installed a disposable DemoApp checkout, ran shared target
|
|
55
|
-
preparation and exploration, rendered an observed UI Map, drove the live surface, and saved a
|
|
56
|
-
repository-native iOS Flow. The equivalent Android browser journey was not verified in that audit
|
|
57
|
-
because no emulator/device was connected.
|
|
58
|
-
|
|
59
|
-
This evidence proves representative local journeys. It does not prove arbitrary frameworks,
|
|
60
|
-
production credentials, third-party services, hosted execution, or complete inference of business
|
|
61
|
-
intent.
|
|
62
|
-
|
|
63
|
-
## Hosted relationship
|
|
64
|
-
|
|
65
|
-
The future hosted application will present the same product journey through a different adapter: application
|
|
66
|
-
accounts/organizations, GitHub App repository authorization, private storage, a durable queue, and
|
|
67
|
-
isolated managed workers. It cannot reuse the loopback session, local `gh` authority, filesystem
|
|
68
|
-
boundary, or in-memory ownership assumptions.
|
|
69
|
-
|
|
70
|
-
The old hosted preview and `cloud/` prototype do not satisfy this boundary. Follow
|
|
71
|
-
[`SAAS-ARCHITECTURE.md`](SAAS-ARCHITECTURE.md) and do not market or accept private repositories until
|
|
72
|
-
[`SAAS-READINESS.md`](SAAS-READINESS.md) passes.
|
package/docs/PRODUCT-ENGINE.md
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
# One Tapp product engine
|
|
2
|
-
|
|
3
|
-
Status: current product-engine contract as of 2026-08-08.
|
|
4
|
-
|
|
5
|
-
Tapp has several interfaces, not several products. The source of truth for customer-critical
|
|
6
|
-
operations is [`mcp-server/src/product-operations.js`](../mcp-server/src/product-operations.js).
|
|
7
|
-
An interface may validate its transport and render a result; it must not redefine onboarding,
|
|
8
|
-
review, trust, baseline, or gate semantics.
|
|
9
|
-
|
|
10
|
-
## Product operation contract
|
|
11
|
-
|
|
12
|
-
The shared engine owns these operations:
|
|
13
|
-
|
|
14
|
-
| Operation | Authoritative result |
|
|
15
|
-
|---|---|
|
|
16
|
-
| `initializeProductProject` | detected targets, real exploration, Application Model, UI Map, release plan |
|
|
17
|
-
| `readProductProject` | one current, read-only product snapshot for any interface |
|
|
18
|
-
| `reviewProductPlan` | explicit approve/reject/defer decisions |
|
|
19
|
-
| `generateProductPlan` | compile-checked but untrusted Task/contract drafts |
|
|
20
|
-
| `validateProductPlan` | real-target, deterministic replay evidence |
|
|
21
|
-
| `promoteProductPlan` | canonical Tasks/contracts, refreshed model/plan, updated map coverage |
|
|
22
|
-
| `prepareProductCi` / `installProductCi` | target-aware workflow and machine-readable CI manifest |
|
|
23
|
-
| `runProductGate` | autonomous evidence plus the committed deterministic suite and one gate decision |
|
|
24
|
-
| `createProductBaseline` | conclusive, platform-and-target-specific comparison state |
|
|
25
|
-
|
|
26
|
-
Deterministic contract execution is in
|
|
27
|
-
[`mcp-server/src/product-execution.js`](../mcp-server/src/product-execution.js). It invokes platform
|
|
28
|
-
executors directly; MCP does not shell through the CLI, and the browser does not shell through MCP.
|
|
29
|
-
|
|
30
|
-
## Interfaces
|
|
31
|
-
|
|
32
|
-
```text
|
|
33
|
-
Browser Release Studio ─┐
|
|
34
|
-
CLI ├── product-operations ── application model / UI Map / Tasks / contracts
|
|
35
|
-
MCP ┘ │
|
|
36
|
-
└── deterministic executors / portable gate / evidence
|
|
37
|
-
|
|
38
|
-
VS Code ── MCP client
|
|
39
|
-
Desktop ── canonical artifact reader (migration to operation client remains)
|
|
40
|
-
Action ── portable gate adapter
|
|
41
|
-
Hosted ── tenant-aware SaaS adapter + queued isolated shared-operation workers (not built)
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
Current convergence:
|
|
45
|
-
|
|
46
|
-
- the browser calls only shared product operations;
|
|
47
|
-
- CLI initialization, plan lifecycle, deterministic draft validation, promotion, gate/baseline
|
|
48
|
-
lifecycle, and CI installation call the same operations. Native build preparation remains at the
|
|
49
|
-
adapter boundary and passes a resolved `.app` or APK into the shared gate;
|
|
50
|
-
- MCP initialization, plan lifecycle, deterministic draft validation, promotion, baseline, and CI
|
|
51
|
-
installation call the same operations;
|
|
52
|
-
- the GitHub Action and `runProductGate` call the same portable gate and evidence protocol;
|
|
53
|
-
- VS Code remains a thin MCP client;
|
|
54
|
-
- desktop reads the same `.autotap` artifacts but still has legacy import/build orchestration. It is
|
|
55
|
-
retained, not the launch UX, until that orchestration is removed;
|
|
56
|
-
- `cloud/runner` is retained prototype evidence for exact checkout, versioned operation envelopes,
|
|
57
|
-
leases, and cleanup. It is not the production hosted adapter or an adequate arbitrary-customer
|
|
58
|
-
isolation boundary. The new SaaS must call these shared operations only through the tenant-aware,
|
|
59
|
-
queued worker contract in [`SAAS-ARCHITECTURE.md`](SAAS-ARCHITECTURE.md).
|
|
60
|
-
|
|
61
|
-
## Canonical repository protocol
|
|
62
|
-
|
|
63
|
-
New product behavior writes only `.autotap/`:
|
|
64
|
-
|
|
65
|
-
```text
|
|
66
|
-
.autotap/
|
|
67
|
-
project.json # actors, env binding names, controlled lifecycle; never secret values
|
|
68
|
-
application-model.json # detected/observed/declared product facts
|
|
69
|
-
ui-map.json # grounded screen/action/transition graph
|
|
70
|
-
release-plan.json # proposals and explicit human decisions
|
|
71
|
-
tasks/ # reusable deterministic semantic operations
|
|
72
|
-
contracts/ # reviewed business guarantees
|
|
73
|
-
baselines/<platform>/ # conclusive target-specific comparison state
|
|
74
|
-
ci.json # generated CI installation manifest
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
`.autotap.yml` and `.tapp.yml` are legacy compatibility inputs, not parallel sources of product
|
|
78
|
-
truth. Do not add a fourth configuration format. Migration readers may normalize old input into the
|
|
79
|
-
canonical model; only an explicit reviewed operation may write new repository artifacts.
|
|
80
|
-
|
|
81
|
-
## Anti-duplication rules
|
|
82
|
-
|
|
83
|
-
1. Trust states (`pending`, `approved`, `validated-draft`, `promoted`) are computed by the engine.
|
|
84
|
-
2. Interfaces render `readProductProject`; they do not infer readiness from file existence.
|
|
85
|
-
3. Re-exploration refreshes evidence while preserving reviewed decisions everywhere.
|
|
86
|
-
4. Promotion refreshes the Application Model immediately; no interface may show stale pre-promotion
|
|
87
|
-
requirements.
|
|
88
|
-
5. Baselines are identified by platform and stable target id everywhere.
|
|
89
|
-
6. An adapter-specific feature is not complete until its engine operation is useful without that
|
|
90
|
-
adapter.
|
|
91
|
-
7. Equivalence tests should assert artifacts and structured results, not merely matching copy.
|
|
92
|
-
|
|
93
|
-
## Remaining migration
|
|
94
|
-
|
|
95
|
-
The next safe convergence work is deliberately narrow:
|
|
96
|
-
|
|
97
|
-
1. replace desktop import/build orchestration with a local product-operation client;
|
|
98
|
-
2. delete the two desktop detection/scaffolding paths only after equivalence fixtures pass;
|
|
99
|
-
3. implement managed account, organization, and tenant authorization before connecting repositories;
|
|
100
|
-
4. implement scoped GitHub authorization, private evidence, and disposable per-job
|
|
101
|
-
identity/simulator/credential isolation before accepting customer code;
|
|
102
|
-
5. preserve CLI/MCP/VS Code/Action as adapters—do not rebuild their product logic.
|