@aarwitz/tapp 0.17.0-rc.10 → 0.17.0-rc.11
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 -2
- package/README.md +6 -3
- package/bin/tapp.js +17 -5
- package/docs/application-model.md +6 -4
- package/mcp-server/src/index.js +7 -4
- package/mcp-server/src/product-operations.js +24 -9
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -22,8 +22,10 @@ npx -y @aarwitz/tapp flow run .tapp/flows/smoke.yml # committed, keyless E2E re
|
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
If repository onboarding detects multiple application targets, target detection is deterministic but
|
|
25
|
-
the choice is the user's.
|
|
26
|
-
|
|
25
|
+
the choice is the user's. Explicit `init --explore` asks even when the model has a saved default; a
|
|
26
|
+
later bare `explore` may consume that default. In a human TTY, Tapp displays a numbered selector and
|
|
27
|
+
continues in the same command. A non-interactive CLI prints the exact choices and exits before
|
|
28
|
+
building. MCP returns
|
|
27
29
|
`reason: "target-selection-required"` with structured `choices[]` (`platform`, `name`, `sourcePath`,
|
|
28
30
|
`selector`, and exact `command`). **Do not pick one yourself.** Present those choices to the user
|
|
29
31
|
with the client's native multiple-choice question UI when available, then rerun using the selected
|
package/README.md
CHANGED
|
@@ -80,11 +80,14 @@ npx -y @aarwitz/tapp baseline create . --platform web
|
|
|
80
80
|
npx -y @aarwitz/tapp ci install .
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
`tapp init . --explore` does not guess from detection order
|
|
83
|
+
In a repository containing multiple apps (for example, iOS plus web),
|
|
84
|
+
`tapp init . --explore` without an explicit target does not guess from detection order—even when a
|
|
85
|
+
prior choice is recorded. A human terminal gets a numbered
|
|
85
86
|
selector; a non-interactive CLI prints exact target-selection commands, while MCP also returns
|
|
86
87
|
structured choices. Neither builds or writes before the choice. After you choose one, the model
|
|
87
|
-
retains every detected target and records the choice as the default for the next bare `tapp explore
|
|
88
|
+
retains every detected target and records the choice as the default for the next bare `tapp explore`;
|
|
89
|
+
explicit `init --explore` continues to ask because it is the onboarding/refresh operation. Setup gaps
|
|
90
|
+
belonging only to unselected targets are shown as informational, not as failures of the selected run.
|
|
88
91
|
|
|
89
92
|
The baseline command writes only after exploration and every selected deterministic suite pass
|
|
90
93
|
conclusively. It stores `.tapp/baselines/<platform>/<target-id>.json`; the generated workflow
|
package/bin/tapp.js
CHANGED
|
@@ -387,17 +387,29 @@ switch (command) {
|
|
|
387
387
|
if (typeof flags["json-out"] === "string") {
|
|
388
388
|
const out = path.resolve(flags["json-out"]);
|
|
389
389
|
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
390
|
-
fs.writeFileSync(out, JSON.stringify({
|
|
391
|
-
|
|
392
|
-
|
|
390
|
+
fs.writeFileSync(out, JSON.stringify({
|
|
391
|
+
model: built.model,
|
|
392
|
+
plan: written?.plan || built.plan,
|
|
393
|
+
...(exploration ? { exploration } : {}),
|
|
394
|
+
...(result.selectedTarget ? { selectedTarget: result.selectedTarget } : {}),
|
|
395
|
+
requirementScope: result.requirementScope,
|
|
396
|
+
}, null, 2) + "\n");
|
|
397
|
+
}
|
|
398
|
+
const activeRequirements = result.requirementScope?.active || built.model.requirements;
|
|
399
|
+
const deferredRequirements = result.requirementScope?.deferred || [];
|
|
400
|
+
const blocking = activeRequirements.filter((item) => item.severity === "blocking");
|
|
401
|
+
const deferredBlocking = deferredRequirements.filter((item) => item.severity === "blocking");
|
|
393
402
|
const pending = (written?.plan || built.plan).items.filter((item) => item.decision === "pending");
|
|
394
403
|
console.log(`🧭 Tapp init — ${built.model.application.name}`);
|
|
395
404
|
console.log(` targets: ${built.model.targets.length ? built.model.targets.map((target) => `${target.platform}:${target.name}`).join(", ") : "none"}`);
|
|
396
405
|
console.log(` UI Map: ${built.model.uiMap.status} · ${built.model.uiMap.nodeCount} states · ${built.model.uiMap.edgeCount} transitions`);
|
|
397
406
|
if (exploration) console.log(` Exploration: ${(exploration.findings || []).length} finding(s)${exploration.inconclusive ? " (inconclusive)" : ""} · ${exploration.uiMap.nodeCount} states · evidence: ${exploration.reportHtml || exploration.capture?.path || "capture recorded"}`);
|
|
398
407
|
if (exploration?.managedRuntime) console.log(` Managed web runtime: built/started ${exploration.target} for exploration and stopped it afterward · log: ${exploration.runtime.logPath}`);
|
|
399
|
-
|
|
400
|
-
|
|
408
|
+
const selectedLabel = result.selectedTarget ? ` for ${result.selectedTarget.platform}:${result.selectedTarget.name}` : "";
|
|
409
|
+
const deferredLabel = deferredBlocking.length ? ` · ${deferredBlocking.length} setup gap(s) on unselected target(s)` : "";
|
|
410
|
+
console.log(` release plan: ${(written?.plan || built.plan).items.length} item(s) · ${pending.length} pending review · ${blocking.length} blocking requirement(s)${selectedLabel}${deferredLabel}`);
|
|
411
|
+
for (const requirement of activeRequirements) console.log(` ${requirement.severity === "blocking" ? "❌" : "⚠️"} ${requirement.message} Next: ${requirement.remediation}`);
|
|
412
|
+
for (const requirement of deferredRequirements) console.log(` ℹ️ Unselected ${requirement.targetPlatform}:${requirement.targetName} setup gap: ${requirement.message} Next: ${requirement.remediation}`);
|
|
401
413
|
if (written) console.log(` model: ${written.modelPath}\n plan: ${written.planPath}`);
|
|
402
414
|
else console.log(" dry run: repository files were not changed");
|
|
403
415
|
break;
|
|
@@ -16,12 +16,14 @@ resolution actually builds and installs the detected Xcode container, the model
|
|
|
16
16
|
scheme as runtime-observed validation and removes the corresponding confirmation blocker. Merely
|
|
17
17
|
supplying a bundle id or prebuilt `.app` does not prove repository build configuration.
|
|
18
18
|
|
|
19
|
-
A
|
|
20
|
-
|
|
19
|
+
A repository with more than one detected application target is never resolved by detection order
|
|
20
|
+
during explicit initialization, even when a prior default exists. Bare `tapp init . --explore`
|
|
21
|
+
prompts in a human TTY; non-interactive CLI/MCP callers receive
|
|
21
22
|
exact `--platform`/`--target` commands before any build or write, and MCP also carries them as
|
|
22
23
|
structured `target-selection-required` choices. The selected run records that target as the default
|
|
23
|
-
for later bare
|
|
24
|
-
and their unmet coverage.
|
|
24
|
+
for later bare `tapp explore`, but the application model retains the repository's other detected
|
|
25
|
+
targets and their unmet coverage. The selected init run reports other-target setup gaps as deferred
|
|
26
|
+
information rather than presenting them as failures of the target that was actually explored.
|
|
25
27
|
|
|
26
28
|
## First inspection
|
|
27
29
|
|
package/mcp-server/src/index.js
CHANGED
|
@@ -3067,11 +3067,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3067
3067
|
server.notification({ method: "notifications/progress", params: { progressToken, progress: progress.action || 0, total: progress.max || budget, message: `Import exploration · ${progress.states} state(s) reached` } }).catch(() => {});
|
|
3068
3068
|
},
|
|
3069
3069
|
});
|
|
3070
|
-
const { model, plan, written, exploration } = result;
|
|
3071
|
-
const blocking = model.requirements.filter((item) => item.severity === "blocking");
|
|
3070
|
+
const { model, plan, written, exploration, selectedTarget, requirementScope } = result;
|
|
3071
|
+
const blocking = (requirementScope?.active || model.requirements).filter((item) => item.severity === "blocking");
|
|
3072
|
+
const deferredBlocking = (requirementScope?.deferred || []).filter((item) => item.severity === "blocking");
|
|
3072
3073
|
const pending = plan.items.filter((item) => item.decision === "pending");
|
|
3073
|
-
const
|
|
3074
|
-
|
|
3074
|
+
const selectedLabel = selectedTarget ? ` for ${selectedTarget.platform}:${selectedTarget.name}` : "";
|
|
3075
|
+
const deferredLabel = deferredBlocking.length ? ` · ${deferredBlocking.length} setup gap(s) on unselected target(s)` : "";
|
|
3076
|
+
const summary = `🧭 Tapp init — ${model.application.name} · ${model.targets.length} target(s) · UI Map ${model.uiMap.status} (${model.uiMap.nodeCount} states/${model.uiMap.edgeCount} transitions) · ${plan.items.length} plan item(s), ${pending.length} pending · ${blocking.length} blocking requirement(s)${selectedLabel}${deferredLabel}${exploration ? ` · real ${exploration.platform} exploration: ${(exploration.findings || []).length} finding(s)${exploration.inconclusive ? " (inconclusive)" : ""}` : ""}`;
|
|
3077
|
+
return richResult(summary, { model, plan, selectedTarget, requirementScope, written: written ? { modelPath: written.modelPath, planPath: written.planPath } : null, exploration });
|
|
3075
3078
|
} catch (error) {
|
|
3076
3079
|
const detail = error.message || String(error);
|
|
3077
3080
|
const message = error.details?.reason === "target-selection-required"
|
|
@@ -88,7 +88,6 @@ function selectInitExplorationTarget(model, {
|
|
|
88
88
|
platform = "",
|
|
89
89
|
target = "",
|
|
90
90
|
appId = "",
|
|
91
|
-
priorDefaultTargetId = "",
|
|
92
91
|
} = {}) {
|
|
93
92
|
const selectedPlatform = String(platform || "").trim().toLowerCase();
|
|
94
93
|
if (selectedPlatform && !["ios", "android", "web"].includes(selectedPlatform)) throw new Error("platform must be ios|android|web");
|
|
@@ -97,15 +96,13 @@ function selectInitExplorationTarget(model, {
|
|
|
97
96
|
const androidMatch = (model.targets || []).find((candidate) => candidate.platform === "android" && candidate.runtime?.applicationId === appId);
|
|
98
97
|
if (androidMatch) requested = androidMatch.id;
|
|
99
98
|
}
|
|
100
|
-
const defaultTargetId = (model.targets || []).some((candidate) => candidate.id === priorDefaultTargetId)
|
|
101
|
-
? priorDefaultTargetId
|
|
102
|
-
: model.application?.defaultTargetId || "";
|
|
103
|
-
const resolutionModel = { ...model, application: { ...(model.application || {}), defaultTargetId } };
|
|
104
99
|
try {
|
|
105
|
-
return selectApplicationTarget(
|
|
100
|
+
return selectApplicationTarget(model, {
|
|
106
101
|
platform: selectedPlatform,
|
|
107
102
|
target: requested,
|
|
108
|
-
|
|
103
|
+
// `init --explore` is the explicit onboarding/refresh operation: it asks whenever several
|
|
104
|
+
// targets are plausible. Only a later bare `tapp explore` consumes the recorded default.
|
|
105
|
+
useDefault: false,
|
|
109
106
|
});
|
|
110
107
|
} catch (error) {
|
|
111
108
|
const choices = initTargetChoices(model, selectedPlatform);
|
|
@@ -133,6 +130,24 @@ function selectInitExplorationTarget(model, {
|
|
|
133
130
|
}
|
|
134
131
|
}
|
|
135
132
|
|
|
133
|
+
export function scopeProductRequirements(model, { selectedTargetId = "" } = {}) {
|
|
134
|
+
const targets = Array.isArray(model?.targets) ? model.targets : [];
|
|
135
|
+
const requirements = Array.isArray(model?.requirements) ? model.requirements : [];
|
|
136
|
+
const selected = String(selectedTargetId || "").trim();
|
|
137
|
+
const enriched = requirements.map((requirement) => {
|
|
138
|
+
const target = targets.find((candidate) => requirement.targetId === candidate.id || String(requirement.id || "").startsWith(`${candidate.id}:`));
|
|
139
|
+
return target
|
|
140
|
+
? { ...requirement, targetId: target.id, targetPlatform: target.platform, targetName: target.name }
|
|
141
|
+
: { ...requirement };
|
|
142
|
+
});
|
|
143
|
+
if (!selected) return { selectedTargetId: "", active: enriched, deferred: [] };
|
|
144
|
+
return {
|
|
145
|
+
selectedTargetId: selected,
|
|
146
|
+
active: enriched.filter((requirement) => !requirement.targetId || requirement.targetId === selected),
|
|
147
|
+
deferred: enriched.filter((requirement) => requirement.targetId && requirement.targetId !== selected),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
136
151
|
function productRunRoot(root) {
|
|
137
152
|
const home = process.env.TAPP_HOME || path.join(os.homedir(), ".tapp");
|
|
138
153
|
const identity = crypto.createHash("sha256").update(root).digest("hex").slice(0, 16);
|
|
@@ -374,7 +389,6 @@ export async function initializeProductProject({
|
|
|
374
389
|
platform: selectedPlatform,
|
|
375
390
|
target,
|
|
376
391
|
appId,
|
|
377
|
-
priorDefaultTargetId: priorModel?.application?.defaultTargetId || "",
|
|
378
392
|
});
|
|
379
393
|
const sourceTarget = selectedTarget.sourcePath === "." ? root : path.resolve(root, selectedTarget.sourcePath);
|
|
380
394
|
exploration = await runExploration({
|
|
@@ -406,7 +420,8 @@ export async function initializeProductProject({
|
|
|
406
420
|
invalidateValidation: mode === "explore",
|
|
407
421
|
});
|
|
408
422
|
}
|
|
409
|
-
|
|
423
|
+
const requirementScope = scopeProductRequirements(built.model, { selectedTargetId: selectedTarget?.id || "" });
|
|
424
|
+
return { operation: "initialize", mode, model: built.model, plan: written?.plan || built.plan, exploration, selectedTarget, requirementScope, written, project: readProductProject({ projectDir: root, outDir }) };
|
|
410
425
|
}
|
|
411
426
|
|
|
412
427
|
function resolvePlan(root, outDir, planPath = "") {
|
package/package.json
CHANGED