@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.
- package/AGENTS.md +4 -4
- package/README.md +13 -10
- package/bin/tapp.js +14 -14
- package/browser/app.js +2 -2
- package/browser/index.html +1 -1
- package/docs/BROWSER-PRODUCT.md +1 -1
- package/docs/PRODUCT-ENGINE.md +7 -6
- package/docs/application-model.md +19 -19
- package/docs/scenarios.md +7 -7
- 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/package.json +1 -1
- package/scripts/ci-gate.sh +12 -7
- package/scripts/flow_lib.py +1 -1
|
@@ -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)
|
|
32
|
-
if (fs.existsSync(
|
|
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 = ".
|
|
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 =
|
|
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 =
|
|
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] ||
|
|
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) :
|
|
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 = ".
|
|
54
|
-
const
|
|
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,
|
|
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 = ".
|
|
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 =
|
|
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 = ".
|
|
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 = ".
|
|
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 = ".
|
|
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 = ".
|
|
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 = ".
|
|
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 = ".
|
|
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 = ".
|
|
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 = ".
|
|
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 = ".
|
|
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 =
|
|
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 = ".
|
|
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 =
|
|
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`);
|
package/package.json
CHANGED
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
|