@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 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. In a human TTY, Tapp displays a numbered selector and continues in the same
26
- command. A non-interactive CLI prints the exact choices and exits before building. MCP returns
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
- On a fresh repository containing multiple apps (for example, iOS plus web), bare
84
- `tapp init . --explore` does not guess from detection order. A human terminal gets a numbered
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({ model: built.model, plan: written?.plan || built.plan, ...(exploration ? { exploration } : {}) }, null, 2) + "\n");
391
- }
392
- const blocking = built.model.requirements.filter((item) => item.severity === "blocking");
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
- console.log(` release plan: ${(written?.plan || built.plan).items.length} item(s) · ${pending.length} pending review · ${blocking.length} blocking requirement(s)`);
400
- for (const requirement of built.model.requirements) console.log(` ${requirement.severity === "blocking" ? "❌" : "⚠️"} ${requirement.message} Next: ${requirement.remediation}`);
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 fresh repository with more than one detected application target is never resolved by detection
20
- order. Bare `tapp init . --explore` prompts in a human TTY; non-interactive CLI/MCP callers receive
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 exploration, but the application model retains the repository's other detected targets
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
 
@@ -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 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)${exploration ? ` · real ${exploration.platform} exploration: ${(exploration.findings || []).length} finding(s)${exploration.inconclusive ? " (inconclusive)" : ""}` : ""}`;
3074
- return richResult(summary, { model, plan, written: written ? { modelPath: written.modelPath, planPath: written.planPath } : null, exploration });
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(resolutionModel, {
100
+ return selectApplicationTarget(model, {
106
101
  platform: selectedPlatform,
107
102
  target: requested,
108
- useDefault: !selectedPlatform && !requested && !!defaultTargetId,
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
- return { operation: "initialize", mode, model: built.model, plan: written?.plan || built.plan, exploration, written, project: readProductProject({ projectDir: root, outDir }) };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aarwitz/tapp",
3
- "version": "0.17.0-rc.10",
3
+ "version": "0.17.0-rc.11",
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",