@aarwitz/tapp 0.15.1 → 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.
@@ -8,6 +8,7 @@ import crypto from "node:crypto";
8
8
  import { loadReleaseContractFile } from "./release-contract.js";
9
9
  import { loadTaskRegistry } from "./task-runtime.js";
10
10
  import { replayableUiMapNavigation, semanticUiKey } from "./ui-map.js";
11
+ import { existingProjectArtifactPath, isProjectArtifactDirectory } from "./project-paths.js";
11
12
 
12
13
  function posix(value) {
13
14
  return String(value || "").replaceAll("\\", "/").replace(/^\.\//, "").replace(/^\/+/, "");
@@ -28,8 +29,8 @@ export function sourcePathMatches(changedFile, ownershipPath) {
28
29
  function repoRootFor(sourcePath) {
29
30
  let current = path.dirname(path.resolve(sourcePath));
30
31
  while (current !== path.dirname(current)) {
31
- if (path.basename(current) === ".autotap") return path.dirname(current);
32
- if (fs.existsSync(path.join(current, ".autotap"))) return current;
32
+ if (isProjectArtifactDirectory(path.basename(current))) return path.dirname(current);
33
+ if (fs.existsSync(existingProjectArtifactPath(current))) return current;
33
34
  current = path.dirname(current);
34
35
  }
35
36
  return process.cwd();
@@ -403,11 +404,11 @@ function mergeGroundingEvidence(existing, incoming) {
403
404
  return merged;
404
405
  }
405
406
 
406
- export function adoptPrCoverageProposal({ projectDir, prPlanPath, item, releasePlanPath = ".autotap/release-plan.json" } = {}) {
407
+ export function adoptPrCoverageProposal({ projectDir, prPlanPath, item, releasePlanPath = ".tapp/release-plan.json" } = {}) {
407
408
  const root = fs.realpathSync(path.resolve(projectDir || process.cwd()));
408
409
  const source = path.resolve(prPlanPath || "");
409
410
  if (!prPlanPath || !fs.existsSync(source)) throw new Error(`PR plan not found: ${source || "(missing path)"}`);
410
- const targetPath = path.resolve(root, releasePlanPath);
411
+ const targetPath = releasePlanPath === ".tapp/release-plan.json" ? existingProjectArtifactPath(root, "release-plan.json") : path.resolve(root, releasePlanPath);
411
412
  if (!inside(root, targetPath)) throw new Error("Release plan path must stay inside the project directory");
412
413
  if (!fs.existsSync(targetPath)) throw new Error(`Release plan not found: ${targetPath}; run tapp init first`);
413
414
  const prPlan = JSON.parse(fs.readFileSync(source, "utf8"));
@@ -423,7 +424,7 @@ export function adoptPrCoverageProposal({ projectDir, prPlanPath, item, releaseP
423
424
  const proposed = structuredClone(proposal.operation.item);
424
425
  if (proposed?.origin !== "deterministic-ui-map-proposal" || proposed?.decision !== "pending") throw new Error("Coverage proposal is not a pending UI-Map-grounded release-plan item");
425
426
  const ground = (proposed.groundedBy || []).find((entry) => entry.type === "ui-map-node");
426
- const mapPath = path.join(root, ".autotap", "ui-map.json");
427
+ const mapPath = existingProjectArtifactPath(root, "ui-map.json");
427
428
  if (!ground || !fs.existsSync(mapPath)) throw new Error("Coverage proposal requires the repository's persistent UI Map");
428
429
  const map = JSON.parse(fs.readFileSync(mapPath, "utf8"));
429
430
  const node = (map.nodes || []).find((candidate) => candidate.id === ground.id && candidate.status !== "proposed");
@@ -583,7 +584,7 @@ export async function buildPrContractPlan({
583
584
  ...(Array.isArray(changedSymbolEvidence) ? changedSymbolEvidence : []),
584
585
  ]);
585
586
  const evidenceByFile = new Map(diffEvidence.map((item) => [item.file, item]));
586
- const contractDir = path.join(root, ".autotap", "contracts");
587
+ const contractDir = existingProjectArtifactPath(root, "contracts");
587
588
  const discovered = discoverContracts && fs.existsSync(contractDir)
588
589
  ? fs.readdirSync(contractDir).filter((name) => /\.contract\.(?:ts|mts|mjs|js|json)$/i.test(name)).map((name) => path.join(contractDir, name)) : [];
589
590
  const files = [...new Set([...discovered, ...contractPaths.map((item) => path.resolve(root, item))])];
@@ -594,10 +595,10 @@ export async function buildPrContractPlan({
594
595
  }
595
596
 
596
597
  let tasks = new Map();
597
- const registrySource = files[0] || path.join(root, ".autotap", "contracts", "contract.ts");
598
+ const registrySource = files[0] || existingProjectArtifactPath(root, "contracts", "contract.ts");
598
599
  try { tasks = loadTaskRegistry({ sourcePath: registrySource, projectDir: root }); } catch {}
599
600
  let map = null;
600
- const resolvedMapPath = mapPath ? path.resolve(root, mapPath) : path.join(root, ".autotap", "ui-map.json");
601
+ const resolvedMapPath = mapPath ? path.resolve(root, mapPath) : existingProjectArtifactPath(root, "ui-map.json");
601
602
  if (fs.existsSync(resolvedMapPath)) map = JSON.parse(fs.readFileSync(resolvedMapPath, "utf8"));
602
603
 
603
604
  const impactedNodes = map ? map.nodes.filter((node) => matchedFiles(changes, node.sourcePaths || []).length) : [];
@@ -16,8 +16,9 @@ import {
16
16
  reviewReleasePlan,
17
17
  writeInitArtifacts,
18
18
  } from "./application-model.js";
19
- import { baselinePathForTarget, renderGithubWorkflow, selectApplicationTarget, writeCiInstallation, writeTargetBaseline } from "./ci-setup.js";
19
+ import { baselinePathForTarget, existingBaselinePathForTarget, renderGithubWorkflow, selectApplicationTarget, writeCiInstallation, writeTargetBaseline } from "./ci-setup.js";
20
20
  import { executeReleaseContract, runProductProcess } from "./product-execution.js";
21
+ import { projectArtifactDirectory } from "./project-paths.js";
21
22
 
22
23
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
23
24
  const packageVersion = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")).version;
@@ -50,8 +51,9 @@ function atomicJson(file, value) {
50
51
  fs.renameSync(temporary, file);
51
52
  }
52
53
 
53
- function artifactPaths(root, outDir = ".autotap") {
54
- const dir = path.resolve(root, outDir);
54
+ function artifactPaths(root, outDir = ".tapp") {
55
+ const selectedOutDir = projectArtifactDirectory(root, outDir);
56
+ const dir = path.resolve(root, selectedOutDir);
55
57
  if (!inside(root, dir)) throw new Error("Artifact directory must remain inside the repository");
56
58
  return {
57
59
  dir,
@@ -94,7 +96,7 @@ function listProductRuns(root) {
94
96
  }
95
97
 
96
98
  function listRepositoryFlows(root) {
97
- const directory = path.join(root, ".autotap", "flows");
99
+ const directory = path.join(root, projectArtifactDirectory(root), "flows");
98
100
  if (!fs.existsSync(directory)) return [];
99
101
  return fs.readdirSync(directory, { withFileTypes:true })
100
102
  .filter((entry) => entry.isFile() && /\.(?:ya?ml|json)$/i.test(entry.name))
@@ -127,7 +129,7 @@ function listRepositoryFlows(root) {
127
129
  .sort((left, right) => left.path.localeCompare(right.path));
128
130
  }
129
131
 
130
- export function readProductProject({ projectDir, outDir = ".autotap" } = {}) {
132
+ export function readProductProject({ projectDir, outDir = ".tapp" } = {}) {
131
133
  const root = realProject(projectDir);
132
134
  const paths = artifactPaths(root, outDir);
133
135
  const model = readJson(paths.model);
@@ -137,7 +139,7 @@ export function readProductProject({ projectDir, outDir = ".autotap" } = {}) {
137
139
  const requirements = model?.requirements || [];
138
140
  const planItems = plan?.items || [];
139
141
  const baselines = (model?.targets || []).map((target) => {
140
- const file = baselinePathForTarget(root, target);
142
+ const file = existingBaselinePathForTarget(root, target);
141
143
  return { targetId: target.id, platform: target.platform, path: file, relativePath: path.relative(root, file).replaceAll(path.sep, "/"), exists: fs.existsSync(file) };
142
144
  });
143
145
  return {
@@ -177,7 +179,7 @@ export function readProductProject({ projectDir, outDir = ".autotap" } = {}) {
177
179
  // target identity or configuration rules.
178
180
  export async function prepareProductTarget({
179
181
  projectDir,
180
- outDir = ".autotap",
182
+ outDir = ".tapp",
181
183
  platform = "",
182
184
  target = "",
183
185
  appPath = "",
@@ -262,7 +264,7 @@ export async function prepareProductTarget({
262
264
  export async function initializeProductProject({
263
265
  projectDir,
264
266
  mode = "inspect",
265
- outDir = ".autotap",
267
+ outDir = ".tapp",
266
268
  ownedUrl = "",
267
269
  platform = "",
268
270
  target = "",
@@ -326,7 +328,7 @@ function resolvePlan(root, outDir, planPath = "") {
326
328
  return { file, plan: readJson(file, { required: true }) };
327
329
  }
328
330
 
329
- export function reviewProductPlan({ projectDir, outDir = ".autotap", planPath = "", approve = [], reject = [], defer = [] } = {}) {
331
+ export function reviewProductPlan({ projectDir, outDir = ".tapp", planPath = "", approve = [], reject = [], defer = [] } = {}) {
330
332
  const root = realProject(projectDir);
331
333
  const resolved = resolvePlan(root, outDir, planPath);
332
334
  const plan = reviewReleasePlan(resolved.plan, { approve, reject, defer });
@@ -334,7 +336,7 @@ export function reviewProductPlan({ projectDir, outDir = ".autotap", planPath =
334
336
  return { operation: "review-plan", plan, planPath: resolved.file, project: readProductProject({ projectDir: root, outDir }) };
335
337
  }
336
338
 
337
- export async function generateProductPlan({ projectDir, outDir = ".autotap", planPath = "" } = {}) {
339
+ export async function generateProductPlan({ projectDir, outDir = ".tapp", planPath = "" } = {}) {
338
340
  const root = realProject(projectDir);
339
341
  const resolved = resolvePlan(root, outDir, planPath);
340
342
  const result = await generateApprovedContractProposals(resolved.plan, { projectDir: root });
@@ -344,7 +346,7 @@ export async function generateProductPlan({ projectDir, outDir = ".autotap", pla
344
346
 
345
347
  export async function validateProductPlan({
346
348
  projectDir,
347
- outDir = ".autotap",
349
+ outDir = ".tapp",
348
350
  planPath = "",
349
351
  items = [],
350
352
  platform = "",
@@ -411,7 +413,7 @@ export async function validateProductPlan({
411
413
  return { operation: "validate-plan", platform: selectedPlatform, passed: results.every((item) => item.passed), results, plan, planPath: resolved.file, project: readProductProject({ projectDir: root, outDir }) };
412
414
  }
413
415
 
414
- export async function promoteProductPlan({ projectDir, outDir = ".autotap", planPath = "", items = [] } = {}) {
416
+ export async function promoteProductPlan({ projectDir, outDir = ".tapp", planPath = "", items = [] } = {}) {
415
417
  const root = realProject(projectDir);
416
418
  const resolved = resolvePlan(root, outDir, planPath);
417
419
  const result = await promoteValidatedProposals(resolved.plan, { projectDir: root, ids: items || [] });
@@ -423,7 +425,7 @@ export async function promoteProductPlan({ projectDir, outDir = ".autotap", plan
423
425
  return { operation: "promote-plan", ...result, plan: written.plan, planPath: written.planPath, modelPath: written.modelPath, project: readProductProject({ projectDir: root, outDir }) };
424
426
  }
425
427
 
426
- export function prepareProductCi({ projectDir, outDir = ".autotap", modelPath = "", actionRef = DEFAULT_ACTION_REF, defaultBranch = "main" } = {}) {
428
+ export function prepareProductCi({ projectDir, outDir = ".tapp", modelPath = "", actionRef = DEFAULT_ACTION_REF, defaultBranch = "main" } = {}) {
427
429
  const root = realProject(projectDir);
428
430
  const modelFile = modelPath ? path.resolve(root, modelPath) : artifactPaths(root, outDir).model;
429
431
  if (!inside(root, modelFile)) throw new Error("Application model must remain inside the repository");
@@ -433,7 +435,7 @@ export function prepareProductCi({ projectDir, outDir = ".autotap", modelPath =
433
435
  return { operation: "prepare-ci", ...rendered, project: readProductProject({ projectDir: root, outDir }) };
434
436
  }
435
437
 
436
- export function installProductCi({ projectDir, outDir = ".autotap", modelPath = "", actionRef = DEFAULT_ACTION_REF, defaultBranch = "main", workflowPath = ".github/workflows/tapp.yml", manifestPath = ".autotap/ci.json", replace = false, allowUnresolved = false } = {}) {
438
+ export function installProductCi({ projectDir, outDir = ".tapp", modelPath = "", actionRef = DEFAULT_ACTION_REF, defaultBranch = "main", workflowPath = ".github/workflows/tapp.yml", manifestPath = ".tapp/ci.json", replace = false, allowUnresolved = false } = {}) {
437
439
  const root = realProject(projectDir);
438
440
  const rendered = prepareProductCi({ projectDir: root, outDir, modelPath, actionRef, defaultBranch });
439
441
  if (rendered.manifest.unresolved.length && !allowUnresolved) throw new Error(`CI installation is unresolved: ${rendered.manifest.unresolved.map((item) => `${item.platform}:${item.message}`).join("; ")}`);
@@ -443,7 +445,7 @@ export function installProductCi({ projectDir, outDir = ".autotap", modelPath =
443
445
 
444
446
  export async function runProductGate({
445
447
  projectDir,
446
- outDir = ".autotap",
448
+ outDir = ".tapp",
447
449
  platform = "web",
448
450
  target = "",
449
451
  url = "",
@@ -501,7 +503,7 @@ export async function runProductGate({
501
503
  if (!inside(root, baselinePath) || !fs.existsSync(baselinePath)) throw new Error("Baseline must be an existing file inside the repository");
502
504
  args.push("--baseline", baselinePath);
503
505
  } else {
504
- const targetBaseline = baselinePathForTarget(root, selected);
506
+ const targetBaseline = existingBaselinePathForTarget(root, selected);
505
507
  if (fs.existsSync(targetBaseline)) args.push("--baseline", targetBaseline);
506
508
  }
507
509
  onProgress({ phase: "gate", text: `Running ${selected.platform}:${selected.name} release gate` });
@@ -515,7 +517,7 @@ export async function runProductGate({
515
517
  return { operation: "run-gate", passed: execution.code === 0, code: execution.code, stdout: execution.stdout, stderr: execution.stderr, selectedTarget: selected, report, reportPath, markdownPath, runDir, project: readProductProject({ projectDir: root, outDir }) };
516
518
  }
517
519
 
518
- export function createProductBaseline({ projectDir, outDir = ".autotap", reportPath, platform = "web", target = "", replace = false, baselinePath = "" } = {}) {
520
+ export function createProductBaseline({ projectDir, outDir = ".tapp", reportPath, platform = "web", target = "", replace = false, baselinePath = "" } = {}) {
519
521
  const root = realProject(projectDir);
520
522
  const project = readProductProject({ projectDir: root, outDir });
521
523
  const selected = selectApplicationTarget(project.model, { platform, target });
@@ -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 = ".autotap/project.json";
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 configPath = path.join(root, PROJECT_CONFIG_RELATIVE_PATH);
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: PROJECT_CONFIG_RELATIVE_PATH, config: { kind: "tapp-project-config", schemaVersion: 1, actors: {} }, exists: false, errors: [] };
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: PROJECT_CONFIG_RELATIVE_PATH, config: null, exists: true, errors: [`Invalid JSON: ${error.message}`] }; }
69
- return { root, path: configPath, relativePath: PROJECT_CONFIG_RELATIVE_PATH, config, exists: true, errors: validateProjectConfig(config) };
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 findAutotapDir(sourcePath, explicitProjectDir = "") {
88
- if (explicitProjectDir) return path.join(path.resolve(explicitProjectDir), ".autotap");
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) === ".autotap") return current;
92
- const candidate = path.join(current, ".autotap");
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 autotapDir = findAutotapDir(sourcePath, projectDir);
101
- const taskDir = autotapDir ? path.join(autotapDir, "tasks") : "";
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 `.autotap/proposals/contracts` may compile
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}.autotap${path.sep}proposals${path.sep}`);
109
- const proposalDir = proposalSource && autotapDir ? path.join(autotapDir, "proposals", "tasks") : "";
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 .autotap/tasks`);
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`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aarwitz/tapp",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
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",
@@ -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>/.autotap/flows/*.yml if --project-dir given)
19
- # [--scenarios <glob>] # Multi-actor Scenario YAMLs (web; default: <app repo>/.autotap/scenarios/*.yml)
20
- # [--contracts <glob>] # TypeScript release contracts (default: <app repo>/.autotap/contracts/*.contract.ts)
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
- [[ -z "$FLOWS" && -n "$PROJECT_DIR" && -d "$PROJECT_DIR/.autotap/flows" ]] && FLOWS="$PROJECT_DIR/.autotap/flows/*.yml"
83
- [[ "$PLATFORM" == "web" && -z "$SCENARIOS" && -n "$PROJECT_DIR" && -d "$PROJECT_DIR/.autotap/scenarios" ]] && SCENARIOS="$PROJECT_DIR/.autotap/scenarios/*.yml"
84
- [[ -z "$CONTRACTS" && -n "$PROJECT_DIR" && -d "$PROJECT_DIR/.autotap/contracts" ]] && CONTRACTS="$PROJECT_DIR/.autotap/contracts/*.contract.ts"
85
- [[ -z "$BASELINE" && -n "$PROJECT_DIR" && -f "$PROJECT_DIR/.autotap/baseline.json" ]] && BASELINE="$PROJECT_DIR/.autotap/baseline.json"
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 \
@@ -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 .autotap/flows/*.yml."""
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