@aarwitz/tapp 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -4
- package/Harness/OCQAHarness/AppDelegate.swift +1 -1
- package/Harness/OCQAHarness.xcodeproj/project.pbxproj +2 -2
- package/Harness/OCQAHarnessUITests/ExplorerTests.swift +2 -2
- package/Harness/generate-harness-xcodeproj.rb +3 -3
- package/README.md +17 -15
- package/bin/tapp.js +28 -28
- package/browser/app.js +2 -2
- package/browser/index.html +1 -1
- package/docs/BROWSER-PRODUCT.md +2 -2
- package/docs/PRODUCT-ENGINE.md +7 -6
- package/docs/application-model.md +20 -25
- package/docs/scenarios.md +7 -7
- package/mcp-server/src/application-model.js +32 -26
- package/mcp-server/src/browser-product.js +1 -1
- package/mcp-server/src/ci-report.js +5 -4
- package/mcp-server/src/ci-setup.js +16 -7
- package/mcp-server/src/enrich.js +1 -1
- package/mcp-server/src/index.js +63 -62
- package/mcp-server/src/maintenance-proposal.js +4 -4
- package/mcp-server/src/pr-selection.js +9 -8
- package/mcp-server/src/product-execution.js +1 -1
- package/mcp-server/src/product-operations.js +20 -18
- package/mcp-server/src/project-config.js +8 -5
- package/mcp-server/src/project-paths.js +32 -0
- package/mcp-server/src/report.js +3 -3
- package/mcp-server/src/task-runtime.js +14 -10
- package/package.json +15 -3
- package/scripts/ci-gate.sh +15 -10
- package/scripts/flow_ai_judge.py +1 -1
- package/scripts/flow_lib.py +1 -1
- package/scripts/quick-capture.sh +15 -14
- package/scripts/run-flow.sh +6 -6
- package/scripts/android-corpus-e2e.sh +0 -30
- package/scripts/cleanup-xcode.sh +0 -157
- package/scripts/corpus-apps.txt +0 -9
- package/scripts/corpus-sweep.sh +0 -121
- package/scripts/coverage-eval.sh +0 -92
- package/scripts/coverage_eval_parse.py +0 -95
- package/scripts/deploy-and-build.sh +0 -99
- package/scripts/mutation-recall-desktop.sh +0 -186
- package/scripts/mutation-recall.sh +0 -121
- package/scripts/mutation_lib.py +0 -128
- package/scripts/mutation_operators.py +0 -144
- package/scripts/validation-matrix.sh +0 -146
- package/scripts/vision-fp-eval.sh +0 -206
- package/scripts/vision_escalation_responder.py +0 -147
- package/scripts/vision_fp_probe.py +0 -221
package/docs/scenarios.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Multi-actor Scenarios
|
|
2
2
|
|
|
3
|
-
A Scenario is Tapp's low-level deterministic multi-actor execution format in `.
|
|
3
|
+
A Scenario is Tapp's low-level deterministic multi-actor execution format in `.tapp/scenarios/*.yml`. It uses the same semantic actions, polling assertions, timeouts, evidence markers, and merge policy as a Flow, but adds isolated named actors, shared variables, and explicit lifecycle steps. The customer-facing business authoring layer is a TypeScript [release contract](release-contracts.md), which composes reusable Tasks and compiles to this runtime instead of duplicating UI steps.
|
|
4
4
|
|
|
5
5
|
Ordinary replay is keyless. AI may propose a Scenario during authoring, but no model, API key, or coding agent participates when CI executes it.
|
|
6
6
|
|
|
@@ -61,13 +61,13 @@ teardown:
|
|
|
61
61
|
## Run it
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
|
-
tapp scenario validate .
|
|
64
|
+
tapp scenario validate .tapp/scenarios/social-system.yml
|
|
65
65
|
ALICE_EMAIL=alice@example.test ALICE_PASSWORD=demo \
|
|
66
66
|
BOB_EMAIL=bob@example.test BOB_PASSWORD=demo \
|
|
67
|
-
tapp scenario run .
|
|
67
|
+
tapp scenario run .tapp/scenarios/social-system.yml
|
|
68
68
|
|
|
69
69
|
tapp ci --platform web --url http://127.0.0.1:4180 \
|
|
70
|
-
--scenarios '.
|
|
70
|
+
--scenarios '.tapp/scenarios/*.yml' \
|
|
71
71
|
--json-out tapp-report.json --md-out tapp-report.md
|
|
72
72
|
```
|
|
73
73
|
|
|
@@ -78,16 +78,16 @@ GitHub Action:
|
|
|
78
78
|
with:
|
|
79
79
|
platform: web
|
|
80
80
|
url: http://127.0.0.1:4180
|
|
81
|
-
scenarios: .
|
|
81
|
+
scenarios: .tapp/scenarios/*.yml
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
MCP clients call `tapp_scenario_run` with `scenarioPath`, or an inline reviewed Scenario. The structured result identifies `kind: scenario`, actual executed/total steps, and actor-tagged steps.
|
|
85
85
|
|
|
86
86
|
## Verified fixture and boundaries
|
|
87
87
|
|
|
88
|
-
`SocialDemo/.
|
|
88
|
+
`SocialDemo/.tapp/contracts/social-system.contract.ts` is the reference system guarantee; the Scenario remains its low-level execution proof and backwards-compatible escape hatch. On 2026-08-04 the contract passed 41/41 compiled steps using two isolated contexts against one delayed shared backend. With `SOCIAL_DEMO_FAULT=hide-cross-actor-posts`, the unchanged contract failed for Bob at 19/41 and the portable merge gate exited non-zero specifically because one release contract failed.
|
|
89
89
|
|
|
90
|
-
The fixture's `.
|
|
90
|
+
The fixture's `.tapp/project.json` is the central actor contract. It records Alice and Bob's
|
|
91
91
|
roles, isolated sessions, seeded provisioning, reset lifecycle, and four environment-variable
|
|
92
92
|
names. The release contract and Scenario consume those names; neither stores the public fixture
|
|
93
93
|
values. Customer values belong in the local environment or CI secret store.
|
|
@@ -8,6 +8,7 @@ import { credentialBindingsFromValue, readProjectConfig } from "./project-config
|
|
|
8
8
|
import { applyReleaseContractCoverage, compileReleaseContract, loadReleaseContractFile, validateReleaseContractAgainstUiMap } from "./release-contract.js";
|
|
9
9
|
import { applyTaskCoverage, loadTaskFile, validateTaskAgainstUiMap } from "./task-runtime.js";
|
|
10
10
|
import { semanticUiKey } from "./ui-map.js";
|
|
11
|
+
import { isProjectArtifactDirectory, projectArtifactDirectory } from "./project-paths.js";
|
|
11
12
|
|
|
12
13
|
const SKIP = new Set([".git", ".build", ".gradle", ".next", ".swiftpm", "Pods", "Carthage", "DerivedData", "build", "dist", "node_modules", "vendor"]);
|
|
13
14
|
|
|
@@ -25,7 +26,7 @@ function walk(root, maxDepth = 4) {
|
|
|
25
26
|
let entries;
|
|
26
27
|
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
27
28
|
for (const entry of entries) {
|
|
28
|
-
if (SKIP.has(entry.name) || (entry.name.startsWith(".") && entry.name
|
|
29
|
+
if (SKIP.has(entry.name) || (entry.name.startsWith(".") && !isProjectArtifactDirectory(entry.name))) continue;
|
|
29
30
|
const absolute = path.join(dir, entry.name);
|
|
30
31
|
if (entry.isDirectory()) {
|
|
31
32
|
directories.push(absolute);
|
|
@@ -124,7 +125,8 @@ function applyRuntimeTargetValidation(root, targets, validation) {
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
function persistedTargetValidations(root, outDir) {
|
|
127
|
-
const
|
|
128
|
+
const requested = String(outDir || ".tapp");
|
|
129
|
+
const artifactDir = path.resolve(root, projectArtifactDirectory(root, requested));
|
|
128
130
|
const relativeArtifactDir = path.relative(root, artifactDir);
|
|
129
131
|
if (path.isAbsolute(relativeArtifactDir) || relativeArtifactDir === ".." || relativeArtifactDir.startsWith(`..${path.sep}`)) return [];
|
|
130
132
|
const prior = readJson(path.join(artifactDir, "application-model.json"));
|
|
@@ -299,10 +301,11 @@ function uiMapTargetsTarget(map, target, targets) {
|
|
|
299
301
|
}
|
|
300
302
|
|
|
301
303
|
function loadTargetUiMaps(root, targets) {
|
|
302
|
-
const rootMap = loadUiMapAt(root, path.join(
|
|
304
|
+
const rootMap = loadUiMapAt(root, path.join(projectArtifactDirectory(root), "ui-map.json"));
|
|
303
305
|
const records = targets.map((target) => {
|
|
304
306
|
const scope = targetArtifactScope(target);
|
|
305
|
-
const
|
|
307
|
+
const scopeRoot = path.join(root, scope === "." ? "" : scope);
|
|
308
|
+
const expectedPath = posix(path.join(scope === "." ? "" : scope, projectArtifactDirectory(scopeRoot), "ui-map.json"));
|
|
306
309
|
let loaded = expectedPath === rootMap.summary.path ? rootMap : loadUiMapAt(root, expectedPath);
|
|
307
310
|
if (!loaded.map && rootMap.map && (targets.length === 1 || uiMapTargetsTarget(rootMap.map, target, targets))) loaded = rootMap;
|
|
308
311
|
return {
|
|
@@ -328,7 +331,7 @@ function loadTargetUiMaps(root, targets) {
|
|
|
328
331
|
const prefixCoverage = unique.length > 1;
|
|
329
332
|
const coverageValues = (field) => unique.flatMap((record) => (record.summary[field] || []).map((id) => prefixCoverage ? `${record.summary.targetId}:${id}` : id));
|
|
330
333
|
const summary = {
|
|
331
|
-
path: unique.length === 1 ? unique[0].summary.path : ".
|
|
334
|
+
path: unique.length === 1 ? unique[0].summary.path : ".tapp/ui-map.json",
|
|
332
335
|
paths: unique.map((record) => record.summary.path).sort(),
|
|
333
336
|
status: allObserved ? "observed" : someObserved ? "partial" : someInconclusive ? "inconclusive" : "missing",
|
|
334
337
|
nodeCount: unique.reduce((total, record) => total + record.summary.nodeCount, 0),
|
|
@@ -348,14 +351,14 @@ function loadTargetUiMaps(root, targets) {
|
|
|
348
351
|
|
|
349
352
|
function artifactScope(root, file) {
|
|
350
353
|
const parts = relative(root, file).split("/");
|
|
351
|
-
const index = parts.
|
|
354
|
+
const index = parts.findIndex(isProjectArtifactDirectory);
|
|
352
355
|
return index > 0 ? parts.slice(0, index).join("/") : ".";
|
|
353
356
|
}
|
|
354
357
|
|
|
355
358
|
function artifactFiles(root, inventory, kind, pattern) {
|
|
356
359
|
return inventory.files.filter((file) => {
|
|
357
360
|
const parts = relative(root, file).split("/");
|
|
358
|
-
const index = parts.
|
|
361
|
+
const index = parts.findIndex(isProjectArtifactDirectory);
|
|
359
362
|
return index >= 0 && parts[index + 1] === kind && pattern.test(path.basename(file));
|
|
360
363
|
}).sort();
|
|
361
364
|
}
|
|
@@ -421,7 +424,7 @@ function applicationName(root, targets) {
|
|
|
421
424
|
return pkg?.name || (targets.length === 1 ? targets[0].name : path.basename(root));
|
|
422
425
|
}
|
|
423
426
|
|
|
424
|
-
export async function inspectApplicationRepository({ projectDir, ownedUrl = "", platform = "", targetValidation = null, outDir = ".
|
|
427
|
+
export async function inspectApplicationRepository({ projectDir, ownedUrl = "", platform = "", targetValidation = null, outDir = ".tapp" } = {}) {
|
|
425
428
|
const root = fs.realpathSync(path.resolve(projectDir || process.cwd()));
|
|
426
429
|
const inventory = walk(root);
|
|
427
430
|
let targets = [
|
|
@@ -550,7 +553,7 @@ export async function inspectApplicationRepository({ projectDir, ownedUrl = "",
|
|
|
550
553
|
requirements.push({
|
|
551
554
|
id: targets.length <= 1 ? "ui-map" : `${target.id}:ui-map`, severity: "blocking", status: summary.status === "inconclusive" ? "inconclusive" : "missing",
|
|
552
555
|
message: target ? (summary.status === "inconclusive" ? `The UI Map for ${target.name} is inconclusive.` : `No grounded UI Map exists for ${target.name}.`) : "No repository UI Map has been grounded in a real run.",
|
|
553
|
-
remediation: target ? `Build/launch ${target.name}, explore the real target, and retain its map at ${summary.expectedPath || summary.path}.` : "Build/launch the target and run tapp init --explore so real exploration evidence is merged into .
|
|
556
|
+
remediation: target ? `Build/launch ${target.name}, explore the real target, and retain its map at ${summary.expectedPath || summary.path}.` : "Build/launch the target and run tapp init --explore so real exploration evidence is merged into .tapp/ui-map.json.",
|
|
554
557
|
});
|
|
555
558
|
}
|
|
556
559
|
if (!contracts.length) requirements.push({ id: "contracts", severity: "warning", status: "missing", message: "No reviewed release contracts exist yet.", remediation: "Review the proposed release plan, then generate and validate a compact set of contracts." });
|
|
@@ -925,7 +928,7 @@ function invalidateGeneratedTaskFiles(root, plan) {
|
|
|
925
928
|
for (const record of plan.generation?.generatedTasks || []) {
|
|
926
929
|
if (!record.path) continue;
|
|
927
930
|
const absolute = path.resolve(root, record.path);
|
|
928
|
-
const proposalRoot = path.join(root,
|
|
931
|
+
const proposalRoot = path.join(root, projectArtifactDirectory(root), "proposals", "tasks");
|
|
929
932
|
if (!isInsideRoot(proposalRoot, absolute) || !fs.existsSync(absolute)) continue;
|
|
930
933
|
const task = readJson(absolute);
|
|
931
934
|
if (!task || task.generation?.origin !== "deterministic-ui-map") continue;
|
|
@@ -937,8 +940,8 @@ function invalidateGeneratedTaskFiles(root, plan) {
|
|
|
937
940
|
}
|
|
938
941
|
}
|
|
939
942
|
|
|
940
|
-
export function writeInitArtifacts({ root, model, plan, outDir = ".
|
|
941
|
-
const directory = path.resolve(root, outDir);
|
|
943
|
+
export function writeInitArtifacts({ root, model, plan, outDir = ".tapp", refresh = false, invalidateValidation = false } = {}) {
|
|
944
|
+
const directory = path.resolve(root, projectArtifactDirectory(root, outDir));
|
|
942
945
|
const modelPath = path.join(directory, "application-model.json");
|
|
943
946
|
const planPath = path.join(directory, "release-plan.json");
|
|
944
947
|
if (!refresh && (fs.existsSync(modelPath) || fs.existsSync(planPath))) throw new Error(`Init artifacts already exist under ${relative(root, directory)}; inspect them or rerun with --refresh to preserve reviewed decisions while updating evidence`);
|
|
@@ -1069,7 +1072,7 @@ function generatedTaskName(node, entryOnly, occupied, identity) {
|
|
|
1069
1072
|
return `${base}${crypto.createHash("sha256").update(identity).digest("hex").slice(0, 6)}`;
|
|
1070
1073
|
}
|
|
1071
1074
|
|
|
1072
|
-
function prepareMapBackedItem(item, map, existingTasks, taskDrafts, { targetId = "", mapPath = ".
|
|
1075
|
+
function prepareMapBackedItem(item, map, existingTasks, taskDrafts, { targetId = "", mapPath = ".tapp/ui-map.json" } = {}) {
|
|
1073
1076
|
const ground = (item.groundedBy || []).find((entry) => entry.type === "ui-map-node");
|
|
1074
1077
|
if (!ground) throw new Error("Approved UI-only proposal is not grounded by a UI Map node");
|
|
1075
1078
|
const nodes = new Map((map.nodes || []).map((node) => [node.id, node]));
|
|
@@ -1168,7 +1171,7 @@ function writeGeneratedTaskDrafts(root, taskDrafts) {
|
|
|
1168
1171
|
for (const draft of [...taskDrafts.values()].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1169
1172
|
const scopeRoot = path.resolve(root, draft.scope === "." || !draft.scope ? "" : draft.scope);
|
|
1170
1173
|
if (!isInsideRoot(root, scopeRoot)) throw new Error(`Generated Task scope escapes repository: ${draft.scope}`);
|
|
1171
|
-
const output = path.join(scopeRoot,
|
|
1174
|
+
const output = path.join(scopeRoot, projectArtifactDirectory(scopeRoot), "proposals", "tasks", `${kebab(draft.name)}.task.json`);
|
|
1172
1175
|
const definition = {
|
|
1173
1176
|
kind: "task", version: 1, name: draft.name, description: draft.description,
|
|
1174
1177
|
...(Object.keys(draft.inputs || {}).length ? { inputs: draft.inputs } : {}),
|
|
@@ -1189,7 +1192,7 @@ function writeGeneratedTaskDrafts(root, taskDrafts) {
|
|
|
1189
1192
|
}
|
|
1190
1193
|
try {
|
|
1191
1194
|
const task = loadTaskFile(output);
|
|
1192
|
-
const mapPath = path.resolve(root, draft.mapPath || ".
|
|
1195
|
+
const mapPath = path.resolve(root, draft.mapPath || ".tapp/ui-map.json");
|
|
1193
1196
|
if (!isInsideRoot(root, mapPath)) throw new Error(`Generated Task UI Map escapes repository: ${draft.mapPath}`);
|
|
1194
1197
|
const map = readJson(mapPath);
|
|
1195
1198
|
if (!map || map.schemaVersion !== 1) throw new Error(`Generated Task UI Map is missing or invalid: ${draft.mapPath}`);
|
|
@@ -1304,7 +1307,7 @@ export async function generateApprovedContractProposals(plan, { projectDir } = {
|
|
|
1304
1307
|
let prepared = item;
|
|
1305
1308
|
if (item.origin === "deterministic-ui-map-proposal") {
|
|
1306
1309
|
const grounding = (item.groundedBy || []).find((entry) => entry.type === "ui-map-node");
|
|
1307
|
-
const mapPath = grounding?.mapPath || ".
|
|
1310
|
+
const mapPath = grounding?.mapPath || ".tapp/ui-map.json";
|
|
1308
1311
|
const absoluteMapPath = path.resolve(root, mapPath);
|
|
1309
1312
|
const itemMap = isInsideRoot(root, absoluteMapPath) ? readJson(absoluteMapPath) : null;
|
|
1310
1313
|
if (!itemMap || itemMap.schemaVersion !== 1) {
|
|
@@ -1329,7 +1332,7 @@ export async function generateApprovedContractProposals(plan, { projectDir } = {
|
|
|
1329
1332
|
if (!prepared) continue;
|
|
1330
1333
|
const scopeRoot = path.resolve(root, item.scope === "." || !item.scope ? "" : item.scope);
|
|
1331
1334
|
if (scopeRoot !== root && !scopeRoot.startsWith(root + path.sep)) throw new Error(`Plan scope escapes repository: ${item.scope}`);
|
|
1332
|
-
const output = path.join(scopeRoot,
|
|
1335
|
+
const output = path.join(scopeRoot, projectArtifactDirectory(scopeRoot), "proposals", "contracts", `${kebab(item.name)}.contract.ts`);
|
|
1333
1336
|
const source = draftContractSource(prepared, projectConfiguration.config || {});
|
|
1334
1337
|
if (fs.existsSync(output) && fs.readFileSync(output, "utf8") !== source) throw new Error(`Draft contract already exists with different content and was not overwritten: ${relative(root, output)}`);
|
|
1335
1338
|
const created = !fs.existsSync(output);
|
|
@@ -1412,13 +1415,13 @@ export function recordContractProposalValidation(plan, { id = "", name = "", pla
|
|
|
1412
1415
|
export function recordGeneratedTaskProposalValidation({ projectDir, item, platform, evidence = "", detail = "" } = {}) {
|
|
1413
1416
|
if (!["ios", "android", "web"].includes(platform)) throw new Error("platform must be ios|android|web");
|
|
1414
1417
|
const root = fs.realpathSync(path.resolve(projectDir || process.cwd()));
|
|
1415
|
-
const
|
|
1416
|
-
const
|
|
1418
|
+
const proposalMarkers = [".tapp", ".autotap"].map((directory) => `${path.sep}${directory}${path.sep}proposals${path.sep}tasks${path.sep}`);
|
|
1419
|
+
const reviewedMarkers = [".tapp", ".autotap"].map((directory) => `${path.sep}${directory}${path.sep}tasks${path.sep}`);
|
|
1417
1420
|
const updated = [];
|
|
1418
1421
|
for (const taskPath of item?.generation?.taskPaths || []) {
|
|
1419
1422
|
const absolute = path.resolve(root, taskPath);
|
|
1420
|
-
const proposed = isInsideRoot(root, absolute) && absolute.includes(
|
|
1421
|
-
const reviewed = isInsideRoot(root, absolute) &&
|
|
1423
|
+
const proposed = isInsideRoot(root, absolute) && proposalMarkers.some((marker) => absolute.includes(marker));
|
|
1424
|
+
const reviewed = isInsideRoot(root, absolute) && reviewedMarkers.some((marker) => absolute.includes(marker)) && !proposed;
|
|
1422
1425
|
if ((!proposed && !reviewed) || !fs.existsSync(absolute)) throw new Error(`Generated Task is missing or outside Tapp Task directories: ${taskPath}`);
|
|
1423
1426
|
const task = readJson(absolute);
|
|
1424
1427
|
if (!task || task.kind !== "task" || task.generation?.origin !== "deterministic-ui-map") throw new Error(`Generated Task draft has invalid provenance: ${taskPath}`);
|
|
@@ -1458,10 +1461,13 @@ export function mergeGeneratedTaskProposalValidation(plan, updates = []) {
|
|
|
1458
1461
|
}
|
|
1459
1462
|
|
|
1460
1463
|
function promotedDestination(root, source, kind) {
|
|
1461
|
-
const marker =
|
|
1464
|
+
const marker = [".tapp", ".autotap"]
|
|
1465
|
+
.map((directory) => `${path.sep}${directory}${path.sep}proposals${path.sep}${kind}${path.sep}`)
|
|
1466
|
+
.find((candidate) => source.includes(candidate));
|
|
1467
|
+
if (!marker) throw new Error(`Proposal ${kind.slice(0, -1)} is outside .tapp/proposals/${kind}: ${relative(root, source)}`);
|
|
1462
1468
|
const index = source.indexOf(marker);
|
|
1463
|
-
|
|
1464
|
-
const destination = `${source.slice(0, index)}${path.sep}
|
|
1469
|
+
const directory = marker.split(path.sep).filter(Boolean)[0];
|
|
1470
|
+
const destination = `${source.slice(0, index)}${path.sep}${directory}${path.sep}${kind}${path.sep}${source.slice(index + marker.length)}`;
|
|
1465
1471
|
if (!isInsideRoot(root, destination)) throw new Error(`Promotion destination escapes repository: ${destination}`);
|
|
1466
1472
|
return destination;
|
|
1467
1473
|
}
|
|
@@ -1480,7 +1486,7 @@ export async function promoteValidatedProposals(plan, { projectDir, ids = [] } =
|
|
|
1480
1486
|
|
|
1481
1487
|
const mapCache = new Map();
|
|
1482
1488
|
const mapForItem = (item) => {
|
|
1483
|
-
const relativeMapPath = item.generation?.mapPath || (item.groundedBy || []).find((entry) => entry.type === "ui-map-node")?.mapPath || ".
|
|
1489
|
+
const relativeMapPath = item.generation?.mapPath || (item.groundedBy || []).find((entry) => entry.type === "ui-map-node")?.mapPath || ".tapp/ui-map.json";
|
|
1484
1490
|
const absolute = path.resolve(root, relativeMapPath);
|
|
1485
1491
|
if (!isInsideRoot(root, absolute)) throw new Error(`UI Map for '${item.name}' escapes the repository: ${relativeMapPath}`);
|
|
1486
1492
|
if (!mapCache.has(absolute)) {
|
|
@@ -1499,7 +1505,7 @@ export async function promoteValidatedProposals(plan, { projectDir, ids = [] } =
|
|
|
1499
1505
|
for (const taskPath of item.generation.taskPaths || []) {
|
|
1500
1506
|
const source = path.resolve(root, taskPath);
|
|
1501
1507
|
if (!fs.existsSync(source)) throw new Error(`Generated Task is missing: ${taskPath}`);
|
|
1502
|
-
if (!String(source).includes(`${path.sep}
|
|
1508
|
+
if (![".tapp", ".autotap"].some((directory) => String(source).includes(`${path.sep}${directory}${path.sep}proposals${path.sep}tasks${path.sep}`))) continue;
|
|
1503
1509
|
const destination = promotedDestination(root, source, "tasks");
|
|
1504
1510
|
moves.set(source, destination);
|
|
1505
1511
|
let task = taskRecords.get(source);
|
|
@@ -109,7 +109,7 @@ function assetPath(pathname) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
function captureRoot() {
|
|
112
|
-
return path.join(process.env.
|
|
112
|
+
return path.join(process.env.TAPP_HOME || process.env.AUTOTAP_HOME || path.join(os.homedir(), ".tapp"), "captures");
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
function capturePath(captureId, relative = "report.html") {
|
|
@@ -31,6 +31,7 @@ import { writeHtmlReport } from "./html-report.js";
|
|
|
31
31
|
import { buildUiMapFromMarkers, writeUiMap } from "./ui-map.js";
|
|
32
32
|
import { proposeSelectorMaintenance, validateWebMaintenanceProposal } from "./maintenance-proposal.js";
|
|
33
33
|
import { isBusinessUiMapNode, releasePlanCandidateFromUiMapNode } from "./application-model.js";
|
|
34
|
+
import { existingProjectArtifactPath } from "./project-paths.js";
|
|
34
35
|
|
|
35
36
|
function parseArgs(argv) {
|
|
36
37
|
const args = { flowLogs: [], failOn: "gate" };
|
|
@@ -201,14 +202,14 @@ function targetCoverageDisposition(target, currentNode, projectDir = "") {
|
|
|
201
202
|
provenance: "runtime-observed",
|
|
202
203
|
});
|
|
203
204
|
if (projectDir) {
|
|
204
|
-
const releasePlanPath =
|
|
205
|
+
const releasePlanPath = existingProjectArtifactPath(path.resolve(projectDir), "release-plan.json");
|
|
205
206
|
try {
|
|
206
207
|
const releasePlan = JSON.parse(fs.readFileSync(releasePlanPath, "utf8"));
|
|
207
208
|
const existing = (releasePlan.items || []).find((candidate) => candidate.id === item.id || candidate.name === item.name ||
|
|
208
209
|
(candidate.groundedBy || []).some((ground) => ground.type === "ui-map-node" && ground.id === currentNode.id));
|
|
209
210
|
if (existing) return {
|
|
210
211
|
existingReleasePlanItem: {
|
|
211
|
-
path: ".
|
|
212
|
+
path: ".tapp/release-plan.json",
|
|
212
213
|
id: existing.id,
|
|
213
214
|
name: existing.name,
|
|
214
215
|
decision: existing.decision,
|
|
@@ -219,7 +220,7 @@ function targetCoverageDisposition(target, currentNode, projectDir = "") {
|
|
|
219
220
|
kind: "release-plan-item-proposal",
|
|
220
221
|
status: "matches-existing-release-plan",
|
|
221
222
|
autoApply: false,
|
|
222
|
-
targetPath: ".
|
|
223
|
+
targetPath: ".tapp/release-plan.json",
|
|
223
224
|
operation: { op: "reconcile-item", item },
|
|
224
225
|
reason: "Fresh PR runtime and changed-file evidence can be attached to the existing grounded item only through explicit adoption; its current human decision is preserved.",
|
|
225
226
|
requiredValidation: "After explicit evidence reconciliation, keep the normal review, generation, deterministic replay, and promotion requirements.",
|
|
@@ -231,7 +232,7 @@ function targetCoverageDisposition(target, currentNode, projectDir = "") {
|
|
|
231
232
|
kind: "release-plan-item-proposal",
|
|
232
233
|
status: "awaiting-explicit-adoption",
|
|
233
234
|
autoApply: false,
|
|
234
|
-
targetPath: ".
|
|
235
|
+
targetPath: ".tapp/release-plan.json",
|
|
235
236
|
operation: { op: "add-item", item },
|
|
236
237
|
reason: `The changed ${currentNode.name} surface was observed in this PR run but is not covered by a selected release contract.`,
|
|
237
238
|
requiredValidation: "Explicitly adopt and review this item, generate reusable UI-Map-backed Tasks, then replay the resulting contract against the real target before promotion.",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { existingProjectArtifactPath, projectArtifactDirectory } from "./project-paths.js";
|
|
3
4
|
|
|
4
5
|
function inside(root, candidate) {
|
|
5
6
|
const value = path.relative(root, candidate);
|
|
@@ -36,7 +37,12 @@ export function selectApplicationTarget(model, { platform = "", target = "" } =
|
|
|
36
37
|
|
|
37
38
|
export function baselinePathForTarget(projectDir, target) {
|
|
38
39
|
const root = fs.realpathSync(path.resolve(projectDir));
|
|
39
|
-
return path.join(root, ".
|
|
40
|
+
return path.join(root, ".tapp", "baselines", target.platform, `${targetSlug(target.id)}.json`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function existingBaselinePathForTarget(projectDir, target) {
|
|
44
|
+
const root = fs.realpathSync(path.resolve(projectDir));
|
|
45
|
+
return existingProjectArtifactPath(root, "baselines", target.platform, `${targetSlug(target.id)}.json`);
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
export function validateBaselineReport(report, { platform, targetId } = {}) {
|
|
@@ -134,8 +140,11 @@ function posix(value) {
|
|
|
134
140
|
}
|
|
135
141
|
|
|
136
142
|
function suiteDirectories(root, target, kind) {
|
|
137
|
-
const candidates = [path.join(root,
|
|
138
|
-
if (target.sourcePath && target.sourcePath !== ".")
|
|
143
|
+
const candidates = [path.join(root, projectArtifactDirectory(root), kind)];
|
|
144
|
+
if (target.sourcePath && target.sourcePath !== ".") {
|
|
145
|
+
const targetRoot = path.join(root, target.sourcePath);
|
|
146
|
+
candidates.push(path.join(targetRoot, projectArtifactDirectory(targetRoot), kind));
|
|
147
|
+
}
|
|
139
148
|
return [...new Set(candidates)].filter((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isDirectory())
|
|
140
149
|
.map((candidate) => `${posix(path.relative(root, candidate))}/*.yml`);
|
|
141
150
|
}
|
|
@@ -176,7 +185,7 @@ function credentialConfiguration(model, targetContracts = []) {
|
|
|
176
185
|
if (requirements.has("email")) bindings.add("TAPP_TEST_EMAIL");
|
|
177
186
|
if (requirements.has("password")) bindings.add("TAPP_TEST_PASSWORD");
|
|
178
187
|
}
|
|
179
|
-
for (const name of bindings) if (!/^[A-Z_][A-Z0-9_]{0,127}$/.test(String(name))) throw new Error(`Actor credential binding '${name}' is not a safe environment-variable name; rerun tapp init from valid .
|
|
188
|
+
for (const name of bindings) if (!/^[A-Z_][A-Z0-9_]{0,127}$/.test(String(name))) throw new Error(`Actor credential binding '${name}' is not a safe environment-variable name; rerun tapp init from valid .tapp/project.json`);
|
|
180
189
|
const inputs = {};
|
|
181
190
|
const primaryEmail = primary.credentialBindings?.email || (!primary.credentialBindings && requirements.has("email") ? "TAPP_TEST_EMAIL" : "");
|
|
182
191
|
const primaryPassword = primary.credentialBindings?.password || (!primary.credentialBindings && requirements.has("password") ? "TAPP_TEST_PASSWORD" : "");
|
|
@@ -220,7 +229,7 @@ function targetInputs(root, model, target) {
|
|
|
220
229
|
if (flows.length) inputs.flows = flows.join(" ");
|
|
221
230
|
if (scenarios.length) inputs.scenarios = scenarios.join(" ");
|
|
222
231
|
if (contracts.length) inputs.contracts = contracts.join(" ");
|
|
223
|
-
const baselinePath =
|
|
232
|
+
const baselinePath = existingBaselinePathForTarget(root, target);
|
|
224
233
|
if (fs.existsSync(baselinePath)) inputs.baseline = posix(path.relative(root, baselinePath));
|
|
225
234
|
const credentials = credentialConfiguration(model, targetContracts);
|
|
226
235
|
Object.assign(inputs, credentials.inputs);
|
|
@@ -292,7 +301,7 @@ export function renderGithubWorkflow({ projectDir, model, actionRef, defaultBran
|
|
|
292
301
|
jobs.push(lines.join("\n"));
|
|
293
302
|
}
|
|
294
303
|
const workflow = [
|
|
295
|
-
"# Generated by `tapp ci install` from .
|
|
304
|
+
"# Generated by `tapp ci install` from .tapp/application-model.json.",
|
|
296
305
|
"# Review this patch. Tapp never overwrites it silently.",
|
|
297
306
|
"name: Tapp release gate",
|
|
298
307
|
"",
|
|
@@ -341,7 +350,7 @@ export function renderGithubWorkflow({ projectDir, model, actionRef, defaultBran
|
|
|
341
350
|
return { workflow, manifest };
|
|
342
351
|
}
|
|
343
352
|
|
|
344
|
-
export function writeCiInstallation({ projectDir, workflow, manifest, workflowPath = ".github/workflows/tapp.yml", manifestPath = ".
|
|
353
|
+
export function writeCiInstallation({ projectDir, workflow, manifest, workflowPath = ".github/workflows/tapp.yml", manifestPath = ".tapp/ci.json", replace = false } = {}) {
|
|
345
354
|
const root = fs.realpathSync(path.resolve(projectDir || process.cwd()));
|
|
346
355
|
const destinations = [path.resolve(root, workflowPath), path.resolve(root, manifestPath)];
|
|
347
356
|
for (const destination of destinations) if (!inside(root, destination)) throw new Error("CI installation outputs must remain inside the repository");
|
package/mcp-server/src/enrich.js
CHANGED
|
@@ -40,7 +40,7 @@ export async function enrichFindings(findings, { backend, callModel, screens = [
|
|
|
40
40
|
`Title: ${f.title}`;
|
|
41
41
|
try {
|
|
42
42
|
const res = await Promise.race([
|
|
43
|
-
callModel(backend, { system, userText, model: process.env.AUTOTAP_FINDING_MODEL || "claude-haiku-4-5-20251001", maxTokens: 300 }),
|
|
43
|
+
callModel(backend, { system, userText, model: process.env.TAPP_FINDING_MODEL || process.env.AUTOTAP_FINDING_MODEL || "claude-haiku-4-5-20251001", maxTokens: 300 }),
|
|
44
44
|
new Promise((r) => setTimeout(() => r({ error: "timeout" }), TIMEOUT_MS)),
|
|
45
45
|
]);
|
|
46
46
|
const parsed = res && !res.error ? parseEnrichment(res.text) : null;
|