@aarwitz/tapp 0.17.0-rc.8 → 0.17.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/.claude-plugin/marketplace.json +34 -0
- package/.claude-plugin/plugin.json +33 -0
- package/AGENTS.md +34 -14
- package/Harness/OCQAHarnessUITests/ExplorerTests.swift +11 -0
- package/README.md +126 -77
- package/bin/tapp.js +187 -63
- package/docs/BROWSER-PRODUCT.md +1 -1
- package/docs/application-model.md +12 -3
- package/mcp-server/src/android-explorer.js +3 -1
- package/mcp-server/src/android-flow.js +18 -1
- package/mcp-server/src/application-model.js +83 -13
- package/mcp-server/src/ci-report.js +2 -2
- package/mcp-server/src/ci-setup.js +4 -4
- package/mcp-server/src/environment-preflight.js +43 -0
- package/mcp-server/src/html-report.js +3 -2
- package/mcp-server/src/index.js +326 -110
- package/mcp-server/src/pr-selection.js +2 -2
- package/mcp-server/src/product-operations.js +111 -6
- package/mcp-server/src/report.js +17 -4
- package/mcp-server/src/web-explorer.js +123 -10
- package/mcp-server/src/web-flow.js +17 -1
- package/package.json +6 -4
- package/scripts/ci-gate.sh +42 -0
- package/scripts/flow_lib.py +1 -1
- package/scripts/quick-capture.sh +2 -2
- package/skills/tapp/SKILL.md +75 -0
- package/skills/tapp/agents/openai.yaml +4 -0
- package/skills/tapp/references/commands.md +102 -0
|
@@ -90,6 +90,7 @@ function applyRuntimeTargetValidation(root, targets, validation) {
|
|
|
90
90
|
const bundleId = String(validation.target || validation.resolution.bundleId || "").trim();
|
|
91
91
|
if (!container || !scheme || !bundleId) return targets;
|
|
92
92
|
const captureId = String(validation.evidence?.captureId || "").trim();
|
|
93
|
+
const explored = !!captureId;
|
|
93
94
|
return targets.map((target) => {
|
|
94
95
|
if (target.platform !== "ios" || posix(target.sourcePath) !== container) return target;
|
|
95
96
|
return {
|
|
@@ -109,7 +110,7 @@ function applyRuntimeTargetValidation(root, targets, validation) {
|
|
|
109
110
|
runtimeValidation: {
|
|
110
111
|
status: "validated",
|
|
111
112
|
basis: "runtime-observed",
|
|
112
|
-
operation: "xcode-build-install-explore",
|
|
113
|
+
operation: explored ? "xcode-build-install-explore" : "xcode-build-install",
|
|
113
114
|
target: bundleId,
|
|
114
115
|
build: { container, scheme, configuration },
|
|
115
116
|
evidence: {
|
|
@@ -118,7 +119,9 @@ function applyRuntimeTargetValidation(root, targets, validation) {
|
|
|
118
119
|
findingCount: Number(validation.evidence?.findingCount || 0),
|
|
119
120
|
...(validation.evidence?.observedAt ? { observedAt: String(validation.evidence.observedAt) } : {}),
|
|
120
121
|
},
|
|
121
|
-
detail:
|
|
122
|
+
detail: explored
|
|
123
|
+
? "Tapp built this repository target with the recorded scheme, installed it, launched it, and produced UI Map evidence."
|
|
124
|
+
: "Tapp built this repository target with the recorded scheme and installed it on an iOS simulator.",
|
|
122
125
|
},
|
|
123
126
|
};
|
|
124
127
|
});
|
|
@@ -133,7 +136,7 @@ function persistedTargetValidations(root, outDir) {
|
|
|
133
136
|
if (prior?.schemaVersion !== 1 || prior.kind !== "tapp-application-model" || !Array.isArray(prior.targets)) return [];
|
|
134
137
|
return prior.targets.flatMap((target) => {
|
|
135
138
|
const validation = target?.runtimeValidation;
|
|
136
|
-
if (target?.platform !== "ios" || validation?.status !== "validated" || validation?.basis !== "runtime-observed" ||
|
|
139
|
+
if (target?.platform !== "ios" || validation?.status !== "validated" || validation?.basis !== "runtime-observed" || !["xcode-build-install", "xcode-build-install-explore"].includes(validation?.operation)) return [];
|
|
137
140
|
const capture = String(validation.evidence?.capture || "");
|
|
138
141
|
return [{
|
|
139
142
|
platform: "ios",
|
|
@@ -307,6 +310,31 @@ function loadTargetUiMaps(root, targets) {
|
|
|
307
310
|
const scopeRoot = path.join(root, scope === "." ? "" : scope);
|
|
308
311
|
const expectedPath = posix(path.join(scope === "." ? "" : scope, projectArtifactDirectory(scopeRoot), "ui-map.json"));
|
|
309
312
|
let loaded = expectedPath === rootMap.summary.path ? rootMap : loadUiMapAt(root, expectedPath);
|
|
313
|
+
// Multiple targets can legitimately share an artifact directory (for example, a root Xcode
|
|
314
|
+
// project beside a root Vite app). A file being at the target's expected path does not prove
|
|
315
|
+
// it describes that target. Never attach a web-only map to iOS merely because both resolve to
|
|
316
|
+
// `.tapp/ui-map.json`; that duplicates proposals and, worse, claims runtime evidence for the
|
|
317
|
+
// wrong application surface.
|
|
318
|
+
if (loaded.map && targets.length > 1 && !uiMapTargetsTarget(loaded.map, target, targets)) {
|
|
319
|
+
loaded = {
|
|
320
|
+
map: null,
|
|
321
|
+
summary: {
|
|
322
|
+
path: expectedPath,
|
|
323
|
+
status: "missing",
|
|
324
|
+
nodeCount: 0,
|
|
325
|
+
edgeCount: 0,
|
|
326
|
+
controlCount: 0,
|
|
327
|
+
uncoveredNodeIds: [],
|
|
328
|
+
uncoveredEdgeIds: [],
|
|
329
|
+
platforms: [],
|
|
330
|
+
rejectedArtifact: {
|
|
331
|
+
path: loaded.summary.path,
|
|
332
|
+
platforms: loaded.summary.platforms || [],
|
|
333
|
+
reason: "artifact metadata does not identify this target",
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
310
338
|
if (!loaded.map && rootMap.map && (targets.length === 1 || uiMapTargetsTarget(rootMap.map, target, targets))) loaded = rootMap;
|
|
311
339
|
return {
|
|
312
340
|
targetId: target.id,
|
|
@@ -424,7 +452,7 @@ function applicationName(root, targets) {
|
|
|
424
452
|
return pkg?.name || (targets.length === 1 ? targets[0].name : path.basename(root));
|
|
425
453
|
}
|
|
426
454
|
|
|
427
|
-
export async function inspectApplicationRepository({ projectDir, ownedUrl = "", platform = "", targetValidation = null, outDir = ".tapp" } = {}) {
|
|
455
|
+
export async function inspectApplicationRepository({ projectDir, ownedUrl = "", platform = "", targetValidation = null, defaultTargetId = "", outDir = ".tapp" } = {}) {
|
|
428
456
|
const root = fs.realpathSync(path.resolve(projectDir || process.cwd()));
|
|
429
457
|
const inventory = walk(root);
|
|
430
458
|
let targets = [
|
|
@@ -541,10 +569,10 @@ export async function inspectApplicationRepository({ projectDir, ownedUrl = "",
|
|
|
541
569
|
const requirements = [];
|
|
542
570
|
if (!targets.length) requirements.push({ id: "target", severity: "blocking", status: "missing", message: "No iOS application project, Android application module, or browser application target was detected.", remediation: "Pass the representative target explicitly or add its unavoidable build/runtime configuration." });
|
|
543
571
|
for (const target of targets) {
|
|
544
|
-
if (target.platform === "ios" && target.status !== "configured") requirements.push({ id: `${target.id}:scheme`, severity: "blocking", status: "needs-confirmation", message: `Confirm a shared build scheme for ${target.name}.`, remediation: `Run tapp build ${target.sourcePath} or provide the scheme during init/build.` });
|
|
572
|
+
if (target.platform === "ios" && target.status !== "configured") requirements.push({ id: `${target.id}:scheme`, severity: "blocking", status: "needs-confirmation", message: `Confirm a shared build scheme for ${target.name}.`, remediation: `Run npx -y @aarwitz/tapp@latest build ${target.sourcePath} or provide the scheme during init/build.` });
|
|
545
573
|
if (target.platform === "android" && !target.runtime.applicationId) requirements.push({ id: `${target.id}:application-id`, severity: "blocking", status: "missing", message: `Android application id was not statically detected for ${target.name}.`, remediation: "Provide --app-id or expose applicationId in the application module." });
|
|
546
|
-
if (target.platform === "web" && !target.runtime.ownedUrl && target.runtime.management !== "tapp-managed") requirements.push({ id: `${target.id}:owned-url`, severity: "blocking", status: "missing", message: `No safe managed runtime or owned URL is available for ${target.name}.`, remediation: "Add a deterministic start/static target or start the app and rerun tapp init with --url http://127.0.0.1:<port>." });
|
|
547
|
-
if (target.platform === "web" && target.build.dependencyStatus === "missing-lockfile") requirements.push({ id: `${target.id}:dependency-lock`, severity: "blocking", status: "missing", message: `${target.name} declares browser dependencies without an observed dependency lockfile.`, remediation: "Commit the package-manager lockfile so Tapp can install dependencies reproducibly, then rerun tapp init." });
|
|
574
|
+
if (target.platform === "web" && !target.runtime.ownedUrl && target.runtime.management !== "tapp-managed") requirements.push({ id: `${target.id}:owned-url`, severity: "blocking", status: "missing", message: `No safe managed runtime or owned URL is available for ${target.name}.`, remediation: "Add a deterministic start/static target or start the app and rerun npx -y @aarwitz/tapp@latest init with --url http://127.0.0.1:<port>." });
|
|
575
|
+
if (target.platform === "web" && target.build.dependencyStatus === "missing-lockfile") requirements.push({ id: `${target.id}:dependency-lock`, severity: "blocking", status: "missing", message: `${target.name} declares browser dependencies without an observed dependency lockfile.`, remediation: "Commit the package-manager lockfile so Tapp can install dependencies reproducibly, then rerun npx -y @aarwitz/tapp@latest init." });
|
|
548
576
|
}
|
|
549
577
|
const incompleteMaps = uiMaps.filter((record) => record.summary.status !== "observed");
|
|
550
578
|
for (const record of incompleteMaps.length ? incompleteMaps : (!targets.length && uiMap.status !== "observed" ? [{ summary: uiMap }] : [])) {
|
|
@@ -553,7 +581,7 @@ export async function inspectApplicationRepository({ projectDir, ownedUrl = "",
|
|
|
553
581
|
requirements.push({
|
|
554
582
|
id: targets.length <= 1 ? "ui-map" : `${target.id}:ui-map`, severity: "blocking", status: summary.status === "inconclusive" ? "inconclusive" : "missing",
|
|
555
583
|
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.",
|
|
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.",
|
|
584
|
+
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 npx -y @aarwitz/tapp@latest init --explore so real exploration evidence is merged into .tapp/ui-map.json.",
|
|
557
585
|
});
|
|
558
586
|
}
|
|
559
587
|
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." });
|
|
@@ -561,17 +589,20 @@ export async function inspectApplicationRepository({ projectDir, ownedUrl = "",
|
|
|
561
589
|
for (const error of projectConfiguration.errors) requirements.push({ id: stableId("project-config-error", error), severity: "blocking", status: "invalid", message: error, remediation: `Fix ${projectConfiguration.relativePath}; actor configuration must contain environment-variable bindings, never credential values.` });
|
|
562
590
|
for (const actor of actors) {
|
|
563
591
|
const missingBindings = actor.credentialRequirements.filter((credential) => !actor.credentialBindings[credential]);
|
|
564
|
-
if (missingBindings.length) requirements.push({ id: `actor:${actor.name}:credential-bindings`, severity: "blocking", status: "missing", message: `Actor '${actor.name}' has unbound credential requirements: ${missingBindings.join(", ")}.`, remediation: `Run tapp actor set ${actor.name} --credential <name>=<ENV_NAME> for each credential, then replace any literal contract credentials with $ENV_NAME placeholders.` });
|
|
592
|
+
if (missingBindings.length) requirements.push({ id: `actor:${actor.name}:credential-bindings`, severity: "blocking", status: "missing", message: `Actor '${actor.name}' has unbound credential requirements: ${missingBindings.join(", ")}.`, remediation: `Run npx -y @aarwitz/tapp@latest actor set ${actor.name} --credential <name>=<ENV_NAME> for each credential, then replace any literal contract credentials with $ENV_NAME placeholders.` });
|
|
565
593
|
if (actor.bindingConflicts.length) requirements.push({ id: `actor:${actor.name}:credential-conflicts`, severity: "blocking", status: "conflict", message: `Actor '${actor.name}' has conflicting credential environment bindings.`, remediation: `Align ${projectConfiguration.relativePath} and reviewed contracts; Tapp will not guess which secret binding is correct.` });
|
|
566
594
|
}
|
|
567
595
|
for (const error of taskErrors) requirements.push({ id: stableId("task-error", error.path), severity: "blocking", status: "invalid", message: error.error, remediation: `Fix ${error.path} before generation.` });
|
|
568
596
|
for (const error of contractErrors) requirements.push({ id: stableId("contract-error", error.path), severity: "blocking", status: "invalid", message: error.error, remediation: `Fix ${error.path} before trusting the release plan.` });
|
|
569
597
|
|
|
598
|
+
const recordedDefaultTargetId = targets.some((target) => target.id === defaultTargetId)
|
|
599
|
+
? defaultTargetId
|
|
600
|
+
: targets.length === 1 ? targets[0].id : "";
|
|
570
601
|
const model = {
|
|
571
602
|
schemaVersion: 1, kind: "tapp-application-model",
|
|
572
|
-
// defaultTargetId
|
|
573
|
-
//
|
|
574
|
-
application: { name: applicationName(root, targets), repositoryRoot: ".", platforms: [...new Set(targets.map((target) => target.platform))].sort(), targetIds: targets.map((target) => target.id), defaultTargetId:
|
|
603
|
+
// defaultTargetId is recorded only when selection is unambiguous or the user explicitly chose
|
|
604
|
+
// a target during source-connected exploration. Detection order is not user intent.
|
|
605
|
+
application: { name: applicationName(root, targets), repositoryRoot: ".", platforms: [...new Set(targets.map((target) => target.platform))].sort(), targetIds: targets.map((target) => target.id), defaultTargetId: recordedDefaultTargetId },
|
|
575
606
|
targets,
|
|
576
607
|
actors,
|
|
577
608
|
entities,
|
|
@@ -956,6 +987,45 @@ export function writeInitArtifacts({ root, model, plan, outDir = ".tapp", refres
|
|
|
956
987
|
return { modelPath, planPath, plan: mergedPlan };
|
|
957
988
|
}
|
|
958
989
|
|
|
990
|
+
export function findApplicationModelRoot(startPath) {
|
|
991
|
+
let current = path.resolve(startPath || process.cwd());
|
|
992
|
+
try {
|
|
993
|
+
if (!fs.statSync(current).isDirectory() || /\.(?:xcodeproj|xcworkspace)$/.test(current)) current = path.dirname(current);
|
|
994
|
+
} catch {
|
|
995
|
+
current = path.dirname(current);
|
|
996
|
+
}
|
|
997
|
+
while (true) {
|
|
998
|
+
const modelPath = path.join(current, projectArtifactDirectory(current), "application-model.json");
|
|
999
|
+
if (fs.existsSync(modelPath)) return current;
|
|
1000
|
+
const parent = path.dirname(current);
|
|
1001
|
+
if (parent === current) return null;
|
|
1002
|
+
current = parent;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// A successful standalone `tapp build` is runtime evidence that a proposed Xcode scheme is real.
|
|
1007
|
+
// Refresh an existing model immediately so a later `tapp init --refresh` does not forget it. This
|
|
1008
|
+
// deliberately does nothing before init: build remains a build command and does not mint a product
|
|
1009
|
+
// model unless the repository already opted into one.
|
|
1010
|
+
export async function persistIosBuildValidation({ projectDir, bundleId, container, scheme, configuration = "Debug" } = {}) {
|
|
1011
|
+
return refreshExistingInitArtifacts({
|
|
1012
|
+
projectDir: container || projectDir,
|
|
1013
|
+
targetValidation: {
|
|
1014
|
+
platform: "ios",
|
|
1015
|
+
target: bundleId,
|
|
1016
|
+
resolution: { kind: "xcode-build-installed", bundleId, build: { container, scheme, configuration } },
|
|
1017
|
+
evidence: { observedAt: new Date().toISOString() },
|
|
1018
|
+
},
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
export async function refreshExistingInitArtifacts({ projectDir, ...options } = {}) {
|
|
1023
|
+
const root = findApplicationModelRoot(projectDir);
|
|
1024
|
+
if (!root) return null;
|
|
1025
|
+
const built = await buildInitArtifacts({ projectDir: root, ...options });
|
|
1026
|
+
return { root, ...writeInitArtifacts({ ...built, refresh: true }) };
|
|
1027
|
+
}
|
|
1028
|
+
|
|
959
1029
|
export function reviewReleasePlan(plan, { approve = [], reject = [], defer = [] } = {}) {
|
|
960
1030
|
const choices = new Map();
|
|
961
1031
|
for (const [decision, values] of Object.entries({ approved: approve, rejected: reject, deferred: defer })) {
|
|
@@ -994,7 +1064,7 @@ function edgeSupportsPlatform(edge, nodes, platform) {
|
|
|
994
1064
|
function shortestObservedPath(map, platform, targetId, { startNodeId } = {}) {
|
|
995
1065
|
const nodes = new Map((map.nodes || []).map((node) => [node.id, node]));
|
|
996
1066
|
const entryId = startNodeId || map.app?.navigationRoots?.[platform] || map.app?.entryNodes?.[platform];
|
|
997
|
-
if (!entryId || !nodes.has(entryId)) throw new Error(`UI Map has no observed ${platform} navigation root; rerun tapp init --refresh --explore before generating Tasks`);
|
|
1067
|
+
if (!entryId || !nodes.has(entryId)) throw new Error(`UI Map has no observed ${platform} navigation root; rerun npx -y @aarwitz/tapp@latest init --refresh --explore before generating Tasks`);
|
|
998
1068
|
if (!nodes.has(targetId)) throw new Error(`UI Map proposal target '${targetId}' is no longer present`);
|
|
999
1069
|
if (entryId === targetId) return [];
|
|
1000
1070
|
const outgoing = new Map();
|
|
@@ -422,10 +422,10 @@ function renderMarkdown(report, regression, flows, scenarios, contracts, prPlan,
|
|
|
422
422
|
? ` at \`${target.navigation.route}\``
|
|
423
423
|
: target.navigation?.mode === "ui-map-path" ? ` through ${(target.navigation.steps || []).length} observed map edge(s)` : "";
|
|
424
424
|
lines.push(`- ${icon} PR exploration **${target.node?.name || target.id}** — ${target.execution?.status || target.status}${navigation}`);
|
|
425
|
-
if (target.coverageProposal?.status === "awaiting-explicit-adoption") lines.push(` - Reviewable coverage proposal ready; never auto-applied. Next: \`tapp pr adopt <pr-plan.json> --item ${target.id} --project-dir .\``);
|
|
425
|
+
if (target.coverageProposal?.status === "awaiting-explicit-adoption") lines.push(` - Reviewable coverage proposal ready; never auto-applied. Next: \`npx -y @aarwitz/tapp@latest pr adopt <pr-plan.json> --item ${target.id} --project-dir .\``);
|
|
426
426
|
if (target.existingReleasePlanItem) {
|
|
427
427
|
lines.push(` - Existing release-plan item \`${target.existingReleasePlanItem.name}\` remains ${target.existingReleasePlanItem.decision}; no duplicate or decision change was made.`);
|
|
428
|
-
lines.push(` - Optional explicit evidence reconciliation: \`tapp pr adopt <pr-plan.json> --item ${target.id} --project-dir .\``);
|
|
428
|
+
lines.push(` - Optional explicit evidence reconciliation: \`npx -y @aarwitz/tapp@latest pr adopt <pr-plan.json> --item ${target.id} --project-dir .\``);
|
|
429
429
|
}
|
|
430
430
|
}
|
|
431
431
|
for (const candidate of prPlan.maintenanceCandidates || []) {
|
|
@@ -17,7 +17,7 @@ export function targetSlug(value) {
|
|
|
17
17
|
|
|
18
18
|
export function selectApplicationTarget(model, { platform = "", target = "", useDefault = false } = {}) {
|
|
19
19
|
if (model?.kind !== "tapp-application-model" || !Array.isArray(model.targets)) {
|
|
20
|
-
throw new Error("Expected a Tapp application model; run tapp init first");
|
|
20
|
+
throw new Error("Expected a Tapp application model; run npx -y @aarwitz/tapp@latest init first");
|
|
21
21
|
}
|
|
22
22
|
const selectedPlatform = String(platform || "").toLowerCase();
|
|
23
23
|
let candidates = model.targets.filter((item) => !selectedPlatform || item.platform === selectedPlatform);
|
|
@@ -215,7 +215,7 @@ function credentialConfiguration(model, targetContracts = []) {
|
|
|
215
215
|
if (requirements.has("email")) bindings.add("TAPP_TEST_EMAIL");
|
|
216
216
|
if (requirements.has("password")) bindings.add("TAPP_TEST_PASSWORD");
|
|
217
217
|
}
|
|
218
|
-
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`);
|
|
218
|
+
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 npx -y @aarwitz/tapp@latest init from valid .tapp/project.json`);
|
|
219
219
|
const inputs = {};
|
|
220
220
|
const primaryEmail = primary.credentialBindings?.email || (!primary.credentialBindings && requirements.has("email") ? "TAPP_TEST_EMAIL" : "");
|
|
221
221
|
const primaryPassword = primary.credentialBindings?.password || (!primary.credentialBindings && requirements.has("password") ? "TAPP_TEST_PASSWORD" : "");
|
|
@@ -241,7 +241,7 @@ function targetInputs(root, model, target) {
|
|
|
241
241
|
const androidProjectPath = path.resolve(root, androidProject);
|
|
242
242
|
const androidProjectRelative = path.relative(root, androidProjectPath);
|
|
243
243
|
if (path.isAbsolute(androidProjectRelative) || androidProjectRelative === ".." || androidProjectRelative.startsWith(`..${path.sep}`)) unresolved.push("Android Gradle project resolves outside the repository");
|
|
244
|
-
else if (!fs.existsSync(path.join(androidProjectPath, "gradlew"))) unresolved.push(`Gradle wrapper is missing from ${androidProject}; commit gradlew or rerun tapp init from the Gradle repository root`);
|
|
244
|
+
else if (!fs.existsSync(path.join(androidProjectPath, "gradlew"))) unresolved.push(`Gradle wrapper is missing from ${androidProject}; commit gradlew or rerun npx -y @aarwitz/tapp@latest init from the Gradle repository root`);
|
|
245
245
|
inputs["android-project"] = androidProject;
|
|
246
246
|
inputs["android-task"] = target.build?.task || "assembleDebug";
|
|
247
247
|
if (target.runtime?.applicationId) inputs["android-app-id"] = target.runtime.applicationId;
|
|
@@ -282,7 +282,7 @@ export function renderGithubWorkflow({ projectDir, model, actionRef, defaultBran
|
|
|
282
282
|
const root = fs.realpathSync(path.resolve(projectDir || process.cwd()));
|
|
283
283
|
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+@[A-Za-z0-9_.-]+$/.test(String(actionRef || ""))) throw new Error("--action-ref must be owner/repository@release-tag-or-sha");
|
|
284
284
|
if (!/^[A-Za-z0-9._\/-]+$/.test(defaultBranch) || defaultBranch.startsWith("/")) throw new Error("--default-branch contains unsupported characters");
|
|
285
|
-
if (model?.kind !== "tapp-application-model" || !Array.isArray(model.targets) || !model.targets.length) throw new Error("Application model has no targets; run tapp init first");
|
|
285
|
+
if (model?.kind !== "tapp-application-model" || !Array.isArray(model.targets) || !model.targets.length) throw new Error("Application model has no targets; run npx -y @aarwitz/tapp@latest init first");
|
|
286
286
|
const occupied = new Set();
|
|
287
287
|
const jobs = [];
|
|
288
288
|
const manifestTargets = [];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const STORAGE_BLOCK_BYTES = 256 * 1024 * 1024;
|
|
5
|
+
export const STORAGE_WARN_BYTES = 5 * 1024 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
function existingAncestor(candidate) {
|
|
8
|
+
let current = path.resolve(candidate || process.cwd());
|
|
9
|
+
while (!fs.existsSync(current)) {
|
|
10
|
+
const parent = path.dirname(current);
|
|
11
|
+
if (parent === current) return null;
|
|
12
|
+
current = parent;
|
|
13
|
+
}
|
|
14
|
+
return current;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function formatStorage(bytes) {
|
|
18
|
+
const gib = Number(bytes || 0) / (1024 ** 3);
|
|
19
|
+
return gib >= 1 ? `${gib.toFixed(gib >= 10 ? 0 : 1)} GiB` : `${Math.round(Number(bytes || 0) / (1024 ** 2))} MiB`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function storagePreflight(candidate, { statfs = fs.statfsSync, blockBytes = STORAGE_BLOCK_BYTES, warnBytes = STORAGE_WARN_BYTES } = {}) {
|
|
23
|
+
const checkedPath = existingAncestor(candidate);
|
|
24
|
+
if (!checkedPath || typeof statfs !== "function") return { ok: true, level: "unknown", path: checkedPath || path.resolve(candidate || process.cwd()), freeBytes: null };
|
|
25
|
+
try {
|
|
26
|
+
const stats = statfs(checkedPath);
|
|
27
|
+
const freeBytes = Number(stats.bavail ?? stats.bfree ?? 0) * Number(stats.bsize ?? 0);
|
|
28
|
+
const level = freeBytes < blockBytes ? "blocked" : freeBytes < warnBytes ? "warning" : "ok";
|
|
29
|
+
return {
|
|
30
|
+
ok: level !== "blocked",
|
|
31
|
+
level,
|
|
32
|
+
path: checkedPath,
|
|
33
|
+
freeBytes,
|
|
34
|
+
message: level === "blocked"
|
|
35
|
+
? `Only ${formatStorage(freeBytes)} is free where Tapp writes evidence. Free disk space before testing; the run was not started and no app-crash finding was created.`
|
|
36
|
+
: level === "warning"
|
|
37
|
+
? `Only ${formatStorage(freeBytes)} is free where Tapp writes builds and evidence; iOS builds can require several GiB.`
|
|
38
|
+
: `${formatStorage(freeBytes)} free for Tapp builds and evidence`,
|
|
39
|
+
};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return { ok: true, level: "unknown", path: checkedPath, freeBytes: null, message: `Free disk space could not be checked: ${error.message || String(error)}` };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -18,7 +18,7 @@ const VALID_PLATFORMS = new Set(["web", "android", "ios"]);
|
|
|
18
18
|
|
|
19
19
|
// Recover a capture's platform when no in-memory report is available. Prefer the run's persisted
|
|
20
20
|
// metadata (ui-map.json → app.platforms) — it is authoritative and survives a renamed folder — and
|
|
21
|
-
// fall back to the capture-id prefix (web-*/android-*,
|
|
21
|
+
// fall back to the capture-id prefix (ios-*/web-*/android-*, with unprefixed iOS as a legacy case) only for captures that
|
|
22
22
|
// predate the map. Defaults to ios if nothing is resolvable, matching buildQaReport's own default.
|
|
23
23
|
export function capturePlatform(captureDir) {
|
|
24
24
|
try {
|
|
@@ -81,7 +81,7 @@ export function writeHtmlReport(captureDir, { report, label = "", recordingWarni
|
|
|
81
81
|
.map(
|
|
82
82
|
(f) => `<li>
|
|
83
83
|
<span class="sev" style="background:${SEV_COLOR[f.severity] || "#57606a"}">${esc(f.severity)}</span>
|
|
84
|
-
${esc(f.title)}${f.screen ? ` <span class="dim">— on ${esc(f.screen)}</span>` : ""}
|
|
84
|
+
${esc(f.title)}${f.screen ? ` <span class="dim">— on ${esc(f.screen)}</span>` : ""}${f.url ? ` <span class="dim">— ${esc(f.url)}</span>` : ""}
|
|
85
85
|
${f.aiAnalysis ? `<div class="ai">why: ${esc(f.aiAnalysis)}</div>` : ""}
|
|
86
86
|
${f.suggestedFix ? `<div class="ai">fix: ${esc(f.suggestedFix)}</div>` : ""}
|
|
87
87
|
</li>`
|
|
@@ -138,6 +138,7 @@ ${r.conditionsNotReached?.length ? `<div><h2>Conditions not reached</h2><ul>${sc
|
|
|
138
138
|
<div class="meta">${esc(label)} · evidence page (observation, not a release decision)</div>
|
|
139
139
|
${r.platform === "web" ? `<div class="meta">Deterministic basis: ${r.deterministicFindingCounts?.total || 0} deterministic finding(s); ${r.sampledFindingCounts?.total || 0} sampled probe finding(s) are advisory.</div>` : ""}
|
|
140
140
|
<div class="headline">${esc(r.headline)}</div>
|
|
141
|
+
${r.credentialWarning ? `<div class="warning">${esc(r.credentialWarning)}</div>` : ""}
|
|
141
142
|
${scopeHtml}
|
|
142
143
|
<h2>Findings</h2>
|
|
143
144
|
<ul class="findings">
|