@tayo-dev/rtl 1.3.1 → 1.4.1

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.
Files changed (87) hide show
  1. package/README.md +38 -97
  2. package/assets/claude/commands/@tayo-dev/rtl/generate.md +43 -6
  3. package/assets/claude/commands/@tayo-dev/rtl/help.md +2 -2
  4. package/assets/codex/@tayo-dev/rtl-conventions/SKILL.md +38 -6
  5. package/assets/codex/@tayo-dev/rtl-generate/SKILL.md +125 -13
  6. package/assets/codex/@tayo-dev/rtl-generate/references/assertion-markers.md +62 -0
  7. package/assets/codex/@tayo-dev/rtl-generate/references/auth.md +92 -0
  8. package/assets/codex/@tayo-dev/rtl-generate/references/conventions-schema.md +184 -0
  9. package/assets/codex/@tayo-dev/rtl-generate/references/entry-path-fidelity.md +68 -0
  10. package/assets/codex/@tayo-dev/rtl-generate/references/intent-model.md +232 -0
  11. package/assets/codex/@tayo-dev/rtl-generate/references/mock-store.md +18 -0
  12. package/assets/codex/@tayo-dev/rtl-generate/references/quality-scoring.md +189 -0
  13. package/assets/codex/@tayo-dev/rtl-generate/references/state-schema.md +119 -0
  14. package/assets/codex/@tayo-dev/rtl-generate/references/test-index.md +12 -0
  15. package/assets/codex/@tayo-dev/rtl-generate/references/verification-gate.md +93 -0
  16. package/assets/codex/@tayo-dev/rtl-help/SKILL.md +21 -7
  17. package/assets/codex/@tayo-dev/rtl-mocks/SKILL.md +55 -9
  18. package/assets/gemini/commands/@tayo-dev/rtl/generate.toml +28 -6
  19. package/assets/gemini/commands/@tayo-dev/rtl/help.toml +2 -2
  20. package/assets/opencode/commands/@tayo-dev/rtl-generate.md +32 -6
  21. package/assets/opencode/commands/@tayo-dev/rtl-help.md +2 -2
  22. package/dist/cli/commands/generate.d.ts +1 -7
  23. package/dist/cli/commands/generate.d.ts.map +1 -1
  24. package/dist/cli/commands/generate.js +264 -101
  25. package/dist/cli/commands/generate.js.map +1 -1
  26. package/dist/cli/commands/install.js +6 -6
  27. package/dist/core/baseline-normalizer.d.ts.map +1 -1
  28. package/dist/core/baseline-normalizer.js +42 -0
  29. package/dist/core/baseline-normalizer.js.map +1 -1
  30. package/dist/core/generator.d.ts +0 -2
  31. package/dist/core/generator.d.ts.map +1 -1
  32. package/dist/core/generator.js +81 -8
  33. package/dist/core/generator.js.map +1 -1
  34. package/dist/core/input-loader.d.ts +2 -2
  35. package/dist/core/input-loader.d.ts.map +1 -1
  36. package/dist/core/input-loader.js +7 -16
  37. package/dist/core/input-loader.js.map +1 -1
  38. package/dist/core/js-parser.d.ts +2 -1
  39. package/dist/core/js-parser.d.ts.map +1 -1
  40. package/dist/core/js-parser.js +70 -1
  41. package/dist/core/js-parser.js.map +1 -1
  42. package/dist/core/orchestrator.d.ts +1 -1
  43. package/dist/core/orchestrator.js +4 -4
  44. package/dist/core/parser.js +2 -2
  45. package/dist/core/recording-intelligence.d.ts +1 -1
  46. package/dist/core/recording-intelligence.d.ts.map +1 -1
  47. package/dist/core/recording-intelligence.js +298 -4
  48. package/dist/core/recording-intelligence.js.map +1 -1
  49. package/dist/core/resolver.d.ts +2 -1
  50. package/dist/core/resolver.d.ts.map +1 -1
  51. package/dist/core/resolver.js +334 -4
  52. package/dist/core/resolver.js.map +1 -1
  53. package/dist/core/scanner.d.ts +3 -3
  54. package/dist/core/scanner.js +9 -9
  55. package/dist/core/scorer.d.ts +6 -2
  56. package/dist/core/scorer.d.ts.map +1 -1
  57. package/dist/core/scorer.js +75 -7
  58. package/dist/core/scorer.js.map +1 -1
  59. package/dist/core/suite-planner.d.ts.map +1 -1
  60. package/dist/core/suite-planner.js +186 -17
  61. package/dist/core/suite-planner.js.map +1 -1
  62. package/dist/core/writer.d.ts +0 -1
  63. package/dist/core/writer.d.ts.map +1 -1
  64. package/dist/core/writer.js +3 -3
  65. package/dist/core/writer.js.map +1 -1
  66. package/dist/index.d.ts +2 -2
  67. package/dist/index.js +19 -15
  68. package/dist/index.js.map +1 -1
  69. package/dist/install/planner.js +1 -1
  70. package/dist/install/runtimes/codex.d.ts.map +1 -1
  71. package/dist/install/runtimes/codex.js +18 -0
  72. package/dist/install/runtimes/codex.js.map +1 -1
  73. package/dist/install/types.d.ts +1 -1
  74. package/dist/learner/index.d.ts +2 -2
  75. package/dist/learner/index.js +3 -3
  76. package/dist/learner/storage.d.ts +1 -1
  77. package/dist/learner/storage.js +2 -2
  78. package/dist/templates/test-template.d.ts +4 -0
  79. package/dist/templates/test-template.d.ts.map +1 -1
  80. package/dist/templates/test-template.js +10 -1
  81. package/dist/templates/test-template.js.map +1 -1
  82. package/dist/types/recording.d.ts +118 -0
  83. package/dist/types/recording.d.ts.map +1 -1
  84. package/dist/types/recording.js.map +1 -1
  85. package/dist/types/score.d.ts +15 -0
  86. package/dist/types/score.d.ts.map +1 -1
  87. package/package.json +5 -5
@@ -1,14 +1,12 @@
1
1
  /**
2
2
  * Generate command
3
- * Full pipeline: parse validate generate → write
4
- * Converts Recorder exports into React Testing Library test files.
3
+ * Internal runtime-only generation pipeline for Testing Library Recorder JS exports.
5
4
  */
6
5
  import { Command } from 'commander';
7
6
  import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
8
7
  import { basename, dirname, join, resolve } from 'node:path';
9
8
  import { cwd } from 'node:process';
10
9
  import pc from 'picocolors';
11
- import { generateTest } from '../../core/generator.js';
12
10
  import { writeTestFile } from '../../core/writer.js';
13
11
  import { captureVisualState, resolveSelector, } from '../../core/resolver.js';
14
12
  import { scoreGeneratedTest } from '../../core/scorer.js';
@@ -21,26 +19,106 @@ import { generateTestFromGroups, emitQuerySummary } from '../../core/generator.j
21
19
  import { loadInput } from '../../core/input-loader.js';
22
20
  import { normalizeJsBaseline } from '../../core/baseline-normalizer.js';
23
21
  import { planJsSuite } from '../../core/suite-planner.js';
22
+ const EMPTY_MARKER_COVERAGE = {
23
+ detected: 0,
24
+ emitted: 0,
25
+ unresolved: 0,
26
+ };
27
+ const UNRESOLVED_MARKER_REASON_GUIDANCE = {
28
+ 'missing-marker-candidate': 'Semantic marker candidate metadata is missing. Re-record or keep marker metadata intact.',
29
+ 'missing-anchor': 'Marker has no reliable anchor step. Re-record with marker near the intended assertion moment.',
30
+ 'missing-query': 'Recorder evidence is missing an accessible query. Capture a clearer role/name or visible text.',
31
+ 'unsupported-proof-subject': 'Marker proof subject is unsupported for safe RTL conversion. Use role/name or visible text proof.',
32
+ 'ambiguous-field-context': 'Field context is ambiguous. Capture a single, specific field label or value target.',
33
+ 'unsupported-field-context': 'Field context could not map to a trusted RTL field query. Record a clearer label/placeholder.',
34
+ 'generic-container': 'Marker points to a generic container. Capture the concrete user-facing element instead.',
35
+ 'css-only-evidence': 'Marker is backed only by CSS-like evidence. Capture semantic role/name or visible text evidence.',
36
+ 'icon-only-target': 'Marker target is icon-only and ambiguous. Capture surrounding accessible text context.',
37
+ 'hidden-evidence': 'Marker evidence depends on hidden/implementation selectors. Capture user-visible evidence instead.',
38
+ };
24
39
  function deriveOutputPath(inputPath) {
25
40
  const dir = dirname(inputPath);
26
- const name = basename(inputPath).replace(/\.(json|[cm]?[jt]sx?)$/, '');
41
+ const name = basename(inputPath).replace(/\.[cm]?[jt]sx?$/, '');
27
42
  return join(dir, `${name}.test.tsx`);
28
43
  }
29
44
  function logScore(scoreResult) {
30
- console.log(pc.dim('[taro]') +
45
+ const markerCoverageSummary = `markers: detected=${scoreResult.markerCoverage.detected}, ` +
46
+ `emitted=${scoreResult.markerCoverage.emitted}, ` +
47
+ `unresolved=${scoreResult.markerCoverage.unresolved}`;
48
+ console.log(pc.dim('[tayo]') +
31
49
  ` Score: ${scoreResult.total}/100 (${scoreResult.grade}) — ` +
32
50
  `query: ${scoreResult.dimensions.queryQuality}, ` +
33
51
  `assertions: ${scoreResult.dimensions.assertionSpecificity}, ` +
34
52
  `structure: ${scoreResult.dimensions.testStructure}, ` +
35
- `boundary: ${scoreResult.dimensions.boundaryIsolation}`);
53
+ `boundary: ${scoreResult.dimensions.boundaryIsolation}, ` +
54
+ markerCoverageSummary);
55
+ }
56
+ function emitMarkerCoverageSection(scoreResult) {
57
+ const gateStatus = scoreResult.markerQualityGate.failing ? pc.red('FAIL') : pc.green('PASS');
58
+ console.log(pc.dim('[tayo]') + ' Marker coverage:');
59
+ console.log(pc.dim('[tayo]') + ` detected: ${scoreResult.markerCoverage.detected}`);
60
+ console.log(pc.dim('[tayo]') + ` emitted: ${scoreResult.markerCoverage.emitted}`);
61
+ console.log(pc.dim('[tayo]') + ` unresolved: ${scoreResult.markerCoverage.unresolved}`);
62
+ console.log(pc.dim('[tayo]') +
63
+ ` QUAL-02 gate: ${gateStatus} (${scoreResult.markerQualityGate.reason})`);
64
+ if (scoreResult.markerQualityGate.failing) {
65
+ console.error(pc.red(`[tayo] QUAL-02 FAIL: ${scoreResult.markerQualityGate.message}`));
66
+ }
67
+ }
68
+ function normalizeUnresolvedMarkerHint(marker) {
69
+ const hint = marker.proofText ?? marker.target ?? marker.query?.raw ?? marker.selector?.selector;
70
+ const normalized = hint?.replace(/\s+/g, ' ').trim();
71
+ return normalized && normalized.length > 0 ? normalized : 'none';
72
+ }
73
+ function formatUnresolvedMarkerLine(marker) {
74
+ const line = marker.line ?? marker.sourceContext.line;
75
+ return Number.isFinite(line) ? String(line) : 'unknown';
76
+ }
77
+ function formatUnresolvedMarkerWarning(marker) {
78
+ const line = formatUnresolvedMarkerLine(marker);
79
+ const hint = normalizeUnresolvedMarkerHint(marker);
80
+ const guidance = UNRESOLVED_MARKER_REASON_GUIDANCE[marker.reason];
81
+ return (`MKR-03 unresolved-marker marker=${marker.markerStepId} ` +
82
+ `line: ${line} reason=${marker.reason} ` +
83
+ `detail="${guidance}" hint="${hint}"`);
84
+ }
85
+ function collectUnresolvedMarkerAssertions(suitePlan) {
86
+ const seenMarkerStepIds = new Set();
87
+ const unresolvedMarkers = [];
88
+ for (const scenario of suitePlan.scenarios) {
89
+ for (const unresolvedMarker of scenario.unresolvedMarkerAssertions ?? []) {
90
+ if (seenMarkerStepIds.has(unresolvedMarker.markerStepId)) {
91
+ continue;
92
+ }
93
+ seenMarkerStepIds.add(unresolvedMarker.markerStepId);
94
+ unresolvedMarkers.push(unresolvedMarker);
95
+ }
96
+ }
97
+ return unresolvedMarkers;
98
+ }
99
+ function emitUnresolvedMarkerWarnings(suitePlan) {
100
+ if (!suitePlan) {
101
+ return;
102
+ }
103
+ const unresolvedMarkers = collectUnresolvedMarkerAssertions(suitePlan);
104
+ for (const unresolvedMarker of unresolvedMarkers) {
105
+ console.warn(pc.yellow(`[tayo] ${formatUnresolvedMarkerWarning(unresolvedMarker)}`));
106
+ }
107
+ }
108
+ function enforceMarkerGateExit(scoreResult) {
109
+ if (!scoreResult.markerQualityGate.failing) {
110
+ return;
111
+ }
112
+ process.exitCode = 1;
113
+ console.error(pc.red('[tayo] Exiting with code 1: QUAL-02 gate failed after generation.'));
36
114
  }
37
115
  function emitLowConfidenceBanner(scoreResult) {
38
116
  if (!scoreResult.requiresReview) {
39
117
  return;
40
118
  }
41
- console.warn(pc.yellow(`[taro] Manual review required — this generated test is still a draft (${scoreResult.total}/100, ${scoreResult.grade}).`));
119
+ console.warn(pc.yellow(`[tayo] Manual review required — this generated test is still a draft (${scoreResult.total}/100, ${scoreResult.grade}).`));
42
120
  if (scoreResult.blockers.length > 0) {
43
- console.warn(pc.yellow(`[taro] Top blockers: ${scoreResult.blockers.join(' | ')}`));
121
+ console.warn(pc.yellow(`[tayo] Top blockers: ${scoreResult.blockers.join(' | ')}`));
44
122
  }
45
123
  }
46
124
  function emitScoreHints(scoreResult, queryResults = [], boundaryIssues = analyzeBoundaryIsolation('')) {
@@ -48,18 +126,18 @@ function emitScoreHints(scoreResult, queryResults = [], boundaryIssues = analyze
48
126
  const testIdCount = queryResults.filter((queryResult) => {
49
127
  return queryResult.method === 'getByTestId';
50
128
  }).length;
51
- console.log(pc.yellow(`[taro] Tip: ${testIdCount} getByTestId queries — consider adding aria-label`));
129
+ console.log(pc.yellow(`[tayo] Tip: ${testIdCount} getByTestId queries — consider adding aria-label`));
52
130
  }
53
131
  if (scoreResult.dimensions.assertionSpecificity < 60) {
54
- console.log(pc.yellow('[taro] Tip: Add specific matchers like toHaveValue() for better assertions'));
132
+ console.log(pc.yellow('[tayo] Tip: Add specific matchers like toHaveValue() for better assertions'));
55
133
  }
56
134
  if (scoreResult.dimensions.testStructure < 60) {
57
- console.log(pc.yellow('[taro] Tip: Split into multiple it() blocks for better test organization'));
135
+ console.log(pc.yellow('[tayo] Tip: Split into multiple it() blocks for better test organization'));
58
136
  }
59
137
  if (scoreResult.dimensions.boundaryIsolation < 60) {
60
138
  for (const issue of boundaryIssues) {
61
- console.warn(pc.yellow(`[taro] Boundary: ${issue.message}`));
62
- console.warn(pc.yellow(`[taro] Tip: ${issue.suggestion}`));
139
+ console.warn(pc.yellow(`[tayo] Boundary: ${issue.message}`));
140
+ console.warn(pc.yellow(`[tayo] Tip: ${issue.suggestion}`));
63
141
  }
64
142
  }
65
143
  }
@@ -69,6 +147,12 @@ function summarizeCleanup(analyzedRecording) {
69
147
  if (diagnostics.removedRedundantClicks > 0) {
70
148
  parts.push(`${diagnostics.removedRedundantClicks} redundant click(s)`);
71
149
  }
150
+ if ((diagnostics.preservedSemanticMarkers ?? 0) > 0) {
151
+ parts.push(`${diagnostics.preservedSemanticMarkers} preserved semantic marker(s)`);
152
+ }
153
+ if ((diagnostics.unresolvedSemanticMarkers ?? 0) > 0) {
154
+ parts.push(`${diagnostics.unresolvedSemanticMarkers} unresolved semantic marker(s)`);
155
+ }
72
156
  if (diagnostics.removedDoubleClickNoise > 0) {
73
157
  parts.push(`${diagnostics.removedDoubleClickNoise} dblClick noise event(s)`);
74
158
  }
@@ -81,7 +165,69 @@ function summarizeCleanup(analyzedRecording) {
81
165
  if (parts.length === 0) {
82
166
  return;
83
167
  }
84
- console.log(pc.dim('[taro]') + ` Recording cleanup: ${parts.join(', ')}`);
168
+ console.log(pc.dim('[tayo]') + ` Recording cleanup: ${parts.join(', ')}`);
169
+ }
170
+ function countPlannedScenarioMarkers(scenarios) {
171
+ return scenarios.reduce((totals, scenario) => ({
172
+ emitted: totals.emitted + (scenario.markerAssertions?.length ?? 0),
173
+ unresolved: totals.unresolved + (scenario.unresolvedMarkerAssertions?.length ?? 0),
174
+ }), {
175
+ emitted: 0,
176
+ unresolved: 0,
177
+ });
178
+ }
179
+ function buildMarkerCoverageSummary(params) {
180
+ const { analyzedRecording, suitePlan } = params;
181
+ const preservedMarkers = analyzedRecording.diagnostics.preservedSemanticMarkers ?? 0;
182
+ const diagnosticUnresolvedMarkers = analyzedRecording.diagnostics.unresolvedSemanticMarkers ?? 0;
183
+ if (!suitePlan) {
184
+ return {
185
+ detected: preservedMarkers + diagnosticUnresolvedMarkers,
186
+ emitted: 0,
187
+ unresolved: diagnosticUnresolvedMarkers,
188
+ };
189
+ }
190
+ const plannedMarkerTotals = countPlannedScenarioMarkers(suitePlan.scenarios);
191
+ const unresolved = plannedMarkerTotals.unresolved;
192
+ const detected = Math.max(preservedMarkers + unresolved, plannedMarkerTotals.emitted + unresolved);
193
+ return {
194
+ detected,
195
+ emitted: plannedMarkerTotals.emitted,
196
+ unresolved,
197
+ };
198
+ }
199
+ function mergeAnalyzedStepState(recording, analyzedRecording) {
200
+ const analyzedStepsById = new Map(analyzedRecording.steps
201
+ .filter((step) => Boolean(step.id))
202
+ .map((step) => [step.id, step]));
203
+ return {
204
+ ...recording,
205
+ steps: recording.steps.map((step) => {
206
+ if (!step.id) {
207
+ return step;
208
+ }
209
+ const analyzedStep = analyzedStepsById.get(step.id);
210
+ if (!analyzedStep) {
211
+ return step;
212
+ }
213
+ return {
214
+ ...step,
215
+ ...(analyzedStep.semanticMarkerCandidate
216
+ ? { semanticMarkerCandidate: analyzedStep.semanticMarkerCandidate }
217
+ : {}),
218
+ ...(analyzedStep.semanticMarkerLink
219
+ ? { semanticMarkerLink: analyzedStep.semanticMarkerLink }
220
+ : {}),
221
+ ...(analyzedStep.unresolvedSemanticMarker
222
+ ? { unresolvedSemanticMarker: analyzedStep.unresolvedSemanticMarker }
223
+ : {}),
224
+ metadata: {
225
+ ...step.metadata,
226
+ ...analyzedStep.metadata,
227
+ },
228
+ };
229
+ }),
230
+ };
85
231
  }
86
232
  function toItGroups(analyzedRecording, fallbackTitle) {
87
233
  if (analyzedRecording.intentGroups.length > 0) {
@@ -164,6 +310,37 @@ function rehydrateSuitePlan(plan, steps) {
164
310
  })),
165
311
  };
166
312
  }
313
+ function isSemanticMarkerStep(step) {
314
+ return Boolean(step.semanticMarkerLink || step.unresolvedSemanticMarker);
315
+ }
316
+ function stripSemanticMarkerStepsFromItGroups(itGroups) {
317
+ return itGroups
318
+ .map((group) => ({
319
+ ...group,
320
+ steps: group.steps.filter((step) => !isSemanticMarkerStep(step)),
321
+ }))
322
+ .filter((group) => group.steps.length > 0);
323
+ }
324
+ function stripSemanticMarkerStepsFromHelpers(helpers) {
325
+ return helpers
326
+ .map((helper) => ({
327
+ ...helper,
328
+ steps: helper.steps.filter((step) => !isSemanticMarkerStep(step)),
329
+ }))
330
+ .filter((helper) => helper.steps.length > 0);
331
+ }
332
+ function stripSemanticMarkerStepsFromScenarios(scenarios, helpers) {
333
+ const helperNames = new Set(helpers.map((helper) => helper.name));
334
+ return scenarios
335
+ .map((scenario) => ({
336
+ ...scenario,
337
+ steps: scenario.steps.filter((step) => !isSemanticMarkerStep(step)),
338
+ helperRefs: scenario.helperRefs.filter((helperRef) => helperNames.has(helperRef)),
339
+ }))
340
+ .filter((scenario) => scenario.steps.length > 0 ||
341
+ scenario.helperRefs.length > 0 ||
342
+ (scenario.markerAssertions?.length ?? 0) > 0);
343
+ }
167
344
  function dedupeQueryResults(queryResults) {
168
345
  const seen = new Set();
169
346
  return queryResults.filter((queryResult) => {
@@ -189,7 +366,7 @@ function summarizeVisualState(visualState) {
189
366
  if (visualState.screenshotPath) {
190
367
  parts.push(`screenshot=${visualState.screenshotPath}`);
191
368
  }
192
- console.log(pc.dim('[taro]') + ` Visual state: ${parts.join(', ')}`);
369
+ console.log(pc.dim('[tayo]') + ` Visual state: ${parts.join(', ')}`);
193
370
  }
194
371
  function summarizeMockAnalysis(mockAnalysis) {
195
372
  if (!mockAnalysis) {
@@ -208,25 +385,25 @@ function summarizeMockAnalysis(mockAnalysis) {
208
385
  if (parts.length === 0) {
209
386
  return;
210
387
  }
211
- console.log(pc.dim('[taro]') + ` Mock analysis: ${parts.join(', ')}`);
388
+ console.log(pc.dim('[tayo]') + ` Mock analysis: ${parts.join(', ')}`);
212
389
  const topRecommendation = mockAnalysis.recommendations[0];
213
390
  if (topRecommendation) {
214
- console.log(pc.dim('[taro]') +
391
+ console.log(pc.dim('[tayo]') +
215
392
  ` Mock hint: ${topRecommendation.kind} ${topRecommendation.target} (${topRecommendation.count} file(s))`);
216
393
  }
217
394
  const topLifecycle = mockAnalysis.mutationLifecycles[0];
218
395
  if (topLifecycle) {
219
- console.log(pc.dim('[taro]') +
396
+ console.log(pc.dim('[tayo]') +
220
397
  ` Mutation lifecycle: ${topLifecycle.stages.join(' -> ')} in ${topLifecycle.file}`);
221
398
  }
222
399
  const topWarning = mockAnalysis.instabilityWarnings[0];
223
400
  if (topWarning) {
224
- console.warn(pc.yellow(`[taro] Mock stability: ${topWarning.reason} (${topWarning.file})`));
401
+ console.warn(pc.yellow(`[tayo] Mock stability: ${topWarning.reason} (${topWarning.file})`));
225
402
  }
226
403
  }
227
404
  function summarizeBoundaryWarnings(warnings) {
228
405
  for (const warning of warnings) {
229
- console.warn(pc.yellow(`[taro] Boundary: ${warning}`));
406
+ console.warn(pc.yellow(`[tayo] Boundary: ${warning}`));
230
407
  }
231
408
  }
232
409
  function tokenizeSuiteHint(value) {
@@ -288,7 +465,7 @@ function applyRepoRenderTarget(suitePlan, renderTarget) {
288
465
  resolvedTarget: renderTarget.symbol,
289
466
  confidence: suitePlan.renderBoundary.confidence === 'low' ? 'medium' : suitePlan.renderBoundary.confidence,
290
467
  },
291
- warnings: suitePlan.warnings.filter((warning) => !warning.includes('Taro could not resolve the exact render target from repo context') &&
468
+ warnings: suitePlan.warnings.filter((warning) => !warning.includes('Tayo could not resolve the exact render target from repo context') &&
292
469
  !warning.includes('Prefer a repo-local module/container render boundary')),
293
470
  };
294
471
  }
@@ -313,7 +490,7 @@ async function resolveJsGeneration(recording, itGroups) {
313
490
  .map((step) => [step.id, step]));
314
491
  const updatedSteps = new Map();
315
492
  if (selectorGroups.size > 0 && recording.url) {
316
- console.log(pc.dim('[taro]') +
493
+ console.log(pc.dim('[tayo]') +
317
494
  ` Resolving ${baseline.selectors.length} selector(s) via Playwright...`);
318
495
  }
319
496
  for (const [stepId, selectors] of selectorGroups) {
@@ -373,7 +550,7 @@ async function resolveJsGeneration(recording, itGroups) {
373
550
  }
374
551
  function summarizeSelectorWarnings(warnings) {
375
552
  for (const warning of warnings) {
376
- console.warn(pc.yellow(`[taro] ${warning}`));
553
+ console.warn(pc.yellow(`[tayo] ${warning}`));
377
554
  }
378
555
  }
379
556
  async function maybeCaptureVisualState(params) {
@@ -382,7 +559,7 @@ async function maybeCaptureVisualState(params) {
382
559
  return null;
383
560
  }
384
561
  const candidates = findVisualCaptureCandidates(analyzedRecording);
385
- const visualDir = join(projectRoot, '.taro', 'visual');
562
+ const visualDir = join(projectRoot, '.tayo', 'visual');
386
563
  if (candidates.length > 0) {
387
564
  await mkdir(visualDir, { recursive: true });
388
565
  return captureVisualState(url, {
@@ -410,7 +587,7 @@ async function maybeAnalyzeMocks(projectRoot) {
410
587
  }
411
588
  }
412
589
  async function appendHistoryEntry(projectRoot, historyEntry) {
413
- const taroDir = join(projectRoot, '.taro');
590
+ const taroDir = join(projectRoot, '.tayo');
414
591
  await mkdir(taroDir, { recursive: true });
415
592
  const historyPath = join(taroDir, 'history.json');
416
593
  let history = [];
@@ -429,12 +606,12 @@ async function finalizeGeneratedOutput(params) {
429
606
  const { code, outputPath, projectRoot, recordingFile, scoreResult } = params;
430
607
  const verification = verifySyntax(code, outputPath);
431
608
  if (!verification.valid) {
432
- console.error(pc.red('[taro] Error: Post-write verification failed'));
609
+ console.error(pc.red('[tayo] Error: Post-write verification failed'));
433
610
  console.error(pc.red(` ${verification.error}`));
434
- console.error(pc.red(' This is a Taro bug. Please report it.'));
611
+ console.error(pc.red(' This is a Tayo bug. Please report it.'));
435
612
  process.exit(1);
436
613
  }
437
- console.log(pc.green('[taro] ✓ post-write verified'));
614
+ console.log(pc.green('[tayo] ✓ post-write verified'));
438
615
  await appendHistoryEntry(projectRoot, {
439
616
  timestamp: new Date().toISOString(),
440
617
  recordingFile,
@@ -451,14 +628,11 @@ async function finalizeGeneratedOutput(params) {
451
628
  }
452
629
  }
453
630
  export function createGenerateCommand() {
454
- const generate = new Command('generate');
631
+ const generate = new Command('__generate');
455
632
  generate
456
- .description('Generate RTL test from Recorder JS or Chrome Recorder JSON export')
457
- .argument('<file>', 'Path to the recorder export file (.js or .json)')
458
- .option('-o, --output <path>', 'Output file path for the generated test')
459
- .option('-d, --dry-run', 'Preview the generated test without writing to disk', false)
460
- .option('-f, --force', 'Overwrite existing test file', false)
461
- .action(async (file, options) => {
633
+ .description('Internal runtime-only generator for Testing Library Recorder JS exports')
634
+ .argument('<file>', 'Path to the recorder export file (.js)')
635
+ .action(async (file) => {
462
636
  const filePath = resolve(file);
463
637
  const projectRoot = cwd();
464
638
  // 1. Verify file is accessible
@@ -477,23 +651,18 @@ export function createGenerateCommand() {
477
651
  console.error(pc.red('Error:') + ` Failed to parse recording: ${pc.bold(filePath)}\n${String(err)}`);
478
652
  process.exit(1);
479
653
  }
480
- const normalizedRecording = parsedInput.source === 'js' ? normalizeJsBaseline(parsedInput) : parsedInput.recording;
481
- let conventions = undefined;
482
- let repoRenderTargets = [];
483
- if (parsedInput.source === 'js') {
484
- conventions = await readConventions(projectRoot);
485
- if (!conventions) {
486
- console.log(pc.dim('[taro]') + ' Scanning project conventions...');
487
- conventions = await scanConventions(projectRoot);
488
- }
489
- repoRenderTargets = await discoverRepoRenderTargets(projectRoot);
654
+ const normalizedRecording = normalizeJsBaseline(parsedInput);
655
+ let conventions = await readConventions(projectRoot);
656
+ if (!conventions) {
657
+ console.log(pc.dim('[tayo]') + ' Scanning project conventions...');
658
+ conventions = await scanConventions(projectRoot);
490
659
  }
660
+ const repoRenderTargets = await discoverRepoRenderTargets(projectRoot);
491
661
  console.log(pc.green('Parsed:') +
492
662
  ` ${pc.bold(normalizedRecording.title)} — ${normalizedRecording.steps.length} steps` +
493
- (parsedInput.source === 'js'
494
- ? `, ${normalizedRecording.baseline?.itGroups.length ?? 0} test group(s)`
495
- : ''));
663
+ `, ${normalizedRecording.baseline?.itGroups.length ?? 0} test group(s)`);
496
664
  const analyzedRecording = analyzeRecording(normalizedRecording);
665
+ const markerAwareRecording = mergeAnalyzedStepState(normalizedRecording, analyzedRecording);
497
666
  summarizeCleanup(analyzedRecording);
498
667
  const visualState = await maybeCaptureVisualState({
499
668
  analyzedRecording,
@@ -504,79 +673,72 @@ export function createGenerateCommand() {
504
673
  summarizeVisualState(visualState);
505
674
  const mockAnalysis = await maybeAnalyzeMocks(projectRoot);
506
675
  summarizeMockAnalysis(mockAnalysis);
507
- const outputPath = options.output ?? deriveOutputPath(filePath);
508
- const rawJsSuitePlan = parsedInput.source === 'js'
509
- ? planJsSuite({
510
- recording: normalizedRecording,
511
- analyzedRecording,
512
- mockAnalysis,
513
- fallbackTitle: normalizedRecording.title,
514
- })
515
- : null;
516
- const repoRenderTarget = parsedInput.source === 'js' && rawJsSuitePlan
517
- ? resolveRepoRenderTarget({
518
- candidates: repoRenderTargets,
519
- recording: normalizedRecording,
520
- mockAnalysis,
521
- suitePlan: rawJsSuitePlan,
522
- })
523
- : null;
676
+ const outputPath = deriveOutputPath(filePath);
677
+ const rawJsSuitePlan = planJsSuite({
678
+ recording: markerAwareRecording,
679
+ analyzedRecording,
680
+ mockAnalysis,
681
+ fallbackTitle: normalizedRecording.title,
682
+ });
683
+ const repoRenderTarget = resolveRepoRenderTarget({
684
+ candidates: repoRenderTargets,
685
+ recording: normalizedRecording,
686
+ mockAnalysis,
687
+ suitePlan: rawJsSuitePlan,
688
+ });
524
689
  const jsSuitePlan = rawJsSuitePlan
525
690
  ? applyRepoRenderTarget(rawJsSuitePlan, repoRenderTarget)
526
691
  : null;
527
692
  if (jsSuitePlan) {
528
693
  summarizeBoundaryWarnings(jsSuitePlan.warnings);
529
694
  }
530
- const resolvedJsGeneration = parsedInput.source === 'js'
531
- ? await resolveJsGeneration(normalizedRecording, jsSuitePlan?.itGroups ?? toItGroups(analyzedRecording, normalizedRecording.title))
532
- : null;
695
+ const resolvedJsGeneration = await resolveJsGeneration(markerAwareRecording, jsSuitePlan?.itGroups ?? toItGroups(analyzedRecording, normalizedRecording.title));
533
696
  if (resolvedJsGeneration) {
534
697
  summarizeSelectorWarnings(resolvedJsGeneration.warnings);
535
698
  }
536
- const hydratedSuitePlan = parsedInput.source === 'js' && jsSuitePlan
537
- ? rehydrateSuitePlan(jsSuitePlan, resolvedJsGeneration?.recording.steps ?? normalizedRecording.steps)
699
+ const hydratedSuitePlan = jsSuitePlan
700
+ ? rehydrateSuitePlan(jsSuitePlan, resolvedJsGeneration?.recording.steps ?? markerAwareRecording.steps)
538
701
  : jsSuitePlan;
539
- const generated = parsedInput.source === 'js'
540
- ? generateTestFromGroups(normalizedRecording.title, resolvedJsGeneration?.itGroups ?? hydratedSuitePlan?.itGroups ?? toItGroups(analyzedRecording, normalizedRecording.title), {
541
- outputPath,
542
- dryRun: options.dryRun,
543
- conventions,
544
- queryResults: resolvedJsGeneration?.queryResults ?? [],
545
- helpers: hydratedSuitePlan?.helpers,
546
- scenarios: hydratedSuitePlan?.scenarios,
547
- renderTarget: repoRenderTarget,
548
- })
549
- : generateTest(analyzedRecording, { outputPath, dryRun: options.dryRun });
702
+ const generationHelpers = hydratedSuitePlan
703
+ ? stripSemanticMarkerStepsFromHelpers(hydratedSuitePlan.helpers)
704
+ : undefined;
705
+ const generationScenarios = hydratedSuitePlan && generationHelpers
706
+ ? stripSemanticMarkerStepsFromScenarios(hydratedSuitePlan.scenarios, generationHelpers)
707
+ : undefined;
708
+ const generationItGroups = stripSemanticMarkerStepsFromItGroups(resolvedJsGeneration?.itGroups ??
709
+ hydratedSuitePlan?.itGroups ??
710
+ toItGroups(analyzedRecording, normalizedRecording.title));
711
+ const generated = generateTestFromGroups(normalizedRecording.title, generationItGroups, {
712
+ outputPath,
713
+ conventions,
714
+ queryResults: resolvedJsGeneration?.queryResults ?? [],
715
+ helpers: generationHelpers,
716
+ scenarios: generationScenarios,
717
+ renderTarget: repoRenderTarget,
718
+ });
719
+ const markerCoverage = buildMarkerCoverageSummary({
720
+ analyzedRecording,
721
+ suitePlan: hydratedSuitePlan,
722
+ });
550
723
  if (hydratedSuitePlan?.warnings.length) {
551
724
  generated.code = [
552
- ...hydratedSuitePlan.warnings.map((warning) => `// taro-boundary-warning: ${warning}`),
725
+ ...hydratedSuitePlan.warnings.map((warning) => `// tayo-boundary-warning: ${warning}`),
553
726
  generated.code,
554
727
  ].join('\n');
555
728
  }
556
- if (parsedInput.source === 'js') {
557
- emitQuerySummary(resolvedJsGeneration?.queryResults ?? []);
558
- }
559
- const scoreResult = parsedInput.source === 'js'
560
- ? scoreGeneratedTest(generated.code, resolvedJsGeneration?.queryResults ?? [])
561
- : scoreGeneratedTest(generated.code);
729
+ emitQuerySummary(resolvedJsGeneration?.queryResults ?? []);
730
+ const scoreResult = scoreGeneratedTest(generated.code, {
731
+ queryResults: resolvedJsGeneration?.queryResults ?? [],
732
+ markerCoverage,
733
+ });
562
734
  const boundaryIssues = analyzeBoundaryIsolation(generated.code);
563
735
  logScore(scoreResult);
736
+ emitMarkerCoverageSection(scoreResult);
737
+ emitUnresolvedMarkerWarnings(hydratedSuitePlan);
564
738
  emitLowConfidenceBanner(scoreResult);
565
739
  emitScoreHints(scoreResult, resolvedJsGeneration?.queryResults ?? [], boundaryIssues);
566
- if (options.dryRun) {
567
- console.log(pc.yellow('\nDry run — test preview:\n'));
568
- console.log(pc.dim('─'.repeat(60)));
569
- console.log(generated.code);
570
- console.log(pc.dim('─'.repeat(60)));
571
- console.log(pc.dim(`\n[taro] Score: ${scoreResult.total}/100 (${scoreResult.grade})`));
572
- console.log(pc.yellow(`\nWould write to: ${pc.bold(outputPath)}`));
573
- return;
574
- }
575
740
  try {
576
- const result = await writeTestFile(generated.code, outputPath, {
577
- force: options.force,
578
- createDir: true,
579
- });
741
+ const result = await writeTestFile(generated.code, outputPath, { createDir: true });
580
742
  await finalizeGeneratedOutput({
581
743
  code: generated.code,
582
744
  outputPath: result.filePath,
@@ -586,6 +748,7 @@ export function createGenerateCommand() {
586
748
  });
587
749
  const action = result.overwritten ? pc.yellow('Updated') : pc.green('Created');
588
750
  console.log(`${action}: ${pc.bold(result.filePath)}`);
751
+ enforceMarkerGateExit(scoreResult);
589
752
  }
590
753
  catch (err) {
591
754
  console.error(pc.red('Error:') + ` ${String(err)}`);