@tayo-dev/rtl 1.3.0 → 1.4.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.
Files changed (69) hide show
  1. package/README.md +66 -33
  2. package/assets/claude/commands/@tayo-dev/rtl/generate.md +2 -2
  3. package/assets/claude/commands/@tayo-dev/rtl/help.md +1 -1
  4. package/assets/codex/@tayo-dev/rtl-conventions/SKILL.md +2 -2
  5. package/assets/codex/@tayo-dev/rtl-generate/SKILL.md +6 -5
  6. package/assets/codex/@tayo-dev/rtl-help/SKILL.md +1 -1
  7. package/assets/gemini/commands/@tayo-dev/rtl/generate.toml +2 -2
  8. package/assets/gemini/commands/@tayo-dev/rtl/help.toml +1 -1
  9. package/assets/opencode/commands/@tayo-dev/rtl-generate.md +2 -2
  10. package/assets/opencode/commands/@tayo-dev/rtl-help.md +1 -1
  11. package/dist/cli/commands/generate.d.ts.map +1 -1
  12. package/dist/cli/commands/generate.js +485 -67
  13. package/dist/cli/commands/generate.js.map +1 -1
  14. package/dist/cli/commands/install.js +6 -6
  15. package/dist/core/baseline-normalizer.d.ts.map +1 -1
  16. package/dist/core/baseline-normalizer.js +42 -0
  17. package/dist/core/baseline-normalizer.js.map +1 -1
  18. package/dist/core/generator.d.ts +5 -1
  19. package/dist/core/generator.d.ts.map +1 -1
  20. package/dist/core/generator.js +235 -17
  21. package/dist/core/generator.js.map +1 -1
  22. package/dist/core/input-loader.d.ts.map +1 -1
  23. package/dist/core/input-loader.js +1 -0
  24. package/dist/core/input-loader.js.map +1 -1
  25. package/dist/core/js-parser.d.ts +2 -1
  26. package/dist/core/js-parser.d.ts.map +1 -1
  27. package/dist/core/js-parser.js +69 -0
  28. package/dist/core/js-parser.js.map +1 -1
  29. package/dist/core/orchestrator.d.ts +1 -1
  30. package/dist/core/orchestrator.js +4 -4
  31. package/dist/core/parser.js +2 -2
  32. package/dist/core/recording-intelligence.d.ts +1 -1
  33. package/dist/core/recording-intelligence.d.ts.map +1 -1
  34. package/dist/core/recording-intelligence.js +298 -4
  35. package/dist/core/recording-intelligence.js.map +1 -1
  36. package/dist/core/resolver.d.ts +28 -2
  37. package/dist/core/resolver.d.ts.map +1 -1
  38. package/dist/core/resolver.js +462 -23
  39. package/dist/core/resolver.js.map +1 -1
  40. package/dist/core/scanner.d.ts +11 -3
  41. package/dist/core/scanner.d.ts.map +1 -1
  42. package/dist/core/scanner.js +47 -9
  43. package/dist/core/scanner.js.map +1 -1
  44. package/dist/core/scorer.d.ts +6 -2
  45. package/dist/core/scorer.d.ts.map +1 -1
  46. package/dist/core/scorer.js +163 -9
  47. package/dist/core/scorer.js.map +1 -1
  48. package/dist/core/suite-planner.d.ts +4 -1
  49. package/dist/core/suite-planner.d.ts.map +1 -1
  50. package/dist/core/suite-planner.js +261 -11
  51. package/dist/core/suite-planner.js.map +1 -1
  52. package/dist/index.d.ts +1 -1
  53. package/dist/index.js +4 -4
  54. package/dist/install/planner.js +1 -1
  55. package/dist/install/types.d.ts +1 -1
  56. package/dist/learner/index.d.ts +2 -2
  57. package/dist/learner/index.js +3 -3
  58. package/dist/learner/storage.d.ts +1 -1
  59. package/dist/learner/storage.js +2 -2
  60. package/dist/templates/test-template.d.ts +26 -2
  61. package/dist/templates/test-template.d.ts.map +1 -1
  62. package/dist/templates/test-template.js +46 -6
  63. package/dist/templates/test-template.js.map +1 -1
  64. package/dist/types/recording.d.ts +162 -0
  65. package/dist/types/recording.d.ts.map +1 -1
  66. package/dist/types/recording.js.map +1 -1
  67. package/dist/types/score.d.ts +37 -0
  68. package/dist/types/score.d.ts.map +1 -1
  69. package/package.json +5 -5
@@ -10,47 +10,137 @@ import { cwd } from 'node:process';
10
10
  import pc from 'picocolors';
11
11
  import { generateTest } from '../../core/generator.js';
12
12
  import { writeTestFile } from '../../core/writer.js';
13
- import { captureVisualState, inspectElements, buildQuery, selectMatcher, emitQry03Warning, } from '../../core/resolver.js';
13
+ import { captureVisualState, resolveSelector, } from '../../core/resolver.js';
14
14
  import { scoreGeneratedTest } from '../../core/scorer.js';
15
15
  import { analyzeBoundaryIsolation } from '../../core/boundary-intelligence.js';
16
16
  import { verifySyntax } from '../../core/verifier.js';
17
- import { analyzeSingleTestFile, mergeConventions, readConventions, scanConventions, } from '../../core/scanner.js';
17
+ import { analyzeSingleTestFile, discoverRepoRenderTargets, mergeConventions, readConventions, scanConventions, } from '../../core/scanner.js';
18
18
  import { analyzeRecording, findVisualCaptureCandidates, } from '../../core/recording-intelligence.js';
19
19
  import { analyzeMocks } from '../../core/mock-intelligence.js';
20
20
  import { generateTestFromGroups, emitQuerySummary } from '../../core/generator.js';
21
21
  import { loadInput } from '../../core/input-loader.js';
22
22
  import { normalizeJsBaseline } from '../../core/baseline-normalizer.js';
23
23
  import { planJsSuite } from '../../core/suite-planner.js';
24
+ const EMPTY_MARKER_COVERAGE = {
25
+ detected: 0,
26
+ emitted: 0,
27
+ unresolved: 0,
28
+ };
29
+ const UNRESOLVED_MARKER_REASON_GUIDANCE = {
30
+ 'missing-marker-candidate': 'Semantic marker candidate metadata is missing. Re-record or keep marker metadata intact.',
31
+ 'missing-anchor': 'Marker has no reliable anchor step. Re-record with marker near the intended assertion moment.',
32
+ 'missing-query': 'Recorder evidence is missing an accessible query. Capture a clearer role/name or visible text.',
33
+ 'unsupported-proof-subject': 'Marker proof subject is unsupported for safe RTL conversion. Use role/name or visible text proof.',
34
+ 'ambiguous-field-context': 'Field context is ambiguous. Capture a single, specific field label or value target.',
35
+ 'unsupported-field-context': 'Field context could not map to a trusted RTL field query. Record a clearer label/placeholder.',
36
+ 'generic-container': 'Marker points to a generic container. Capture the concrete user-facing element instead.',
37
+ 'css-only-evidence': 'Marker is backed only by CSS-like evidence. Capture semantic role/name or visible text evidence.',
38
+ 'icon-only-target': 'Marker target is icon-only and ambiguous. Capture surrounding accessible text context.',
39
+ 'hidden-evidence': 'Marker evidence depends on hidden/implementation selectors. Capture user-visible evidence instead.',
40
+ };
24
41
  function deriveOutputPath(inputPath) {
25
42
  const dir = dirname(inputPath);
26
43
  const name = basename(inputPath).replace(/\.(json|[cm]?[jt]sx?)$/, '');
27
44
  return join(dir, `${name}.test.tsx`);
28
45
  }
29
46
  function logScore(scoreResult) {
30
- console.log(pc.dim('[taro]') +
47
+ const markerCoverageSummary = `markers: detected=${scoreResult.markerCoverage.detected}, ` +
48
+ `emitted=${scoreResult.markerCoverage.emitted}, ` +
49
+ `unresolved=${scoreResult.markerCoverage.unresolved}`;
50
+ console.log(pc.dim('[tayo]') +
31
51
  ` Score: ${scoreResult.total}/100 (${scoreResult.grade}) — ` +
32
52
  `query: ${scoreResult.dimensions.queryQuality}, ` +
33
53
  `assertions: ${scoreResult.dimensions.assertionSpecificity}, ` +
34
54
  `structure: ${scoreResult.dimensions.testStructure}, ` +
35
- `boundary: ${scoreResult.dimensions.boundaryIsolation}`);
55
+ `boundary: ${scoreResult.dimensions.boundaryIsolation}, ` +
56
+ markerCoverageSummary);
57
+ }
58
+ function emitMarkerCoverageSection(scoreResult) {
59
+ const gateStatus = scoreResult.markerQualityGate.failing ? pc.red('FAIL') : pc.green('PASS');
60
+ console.log(pc.dim('[tayo]') + ' Marker coverage:');
61
+ console.log(pc.dim('[tayo]') + ` detected: ${scoreResult.markerCoverage.detected}`);
62
+ console.log(pc.dim('[tayo]') + ` emitted: ${scoreResult.markerCoverage.emitted}`);
63
+ console.log(pc.dim('[tayo]') + ` unresolved: ${scoreResult.markerCoverage.unresolved}`);
64
+ console.log(pc.dim('[tayo]') +
65
+ ` QUAL-02 gate: ${gateStatus} (${scoreResult.markerQualityGate.reason})`);
66
+ if (scoreResult.markerQualityGate.failing) {
67
+ console.error(pc.red(`[tayo] QUAL-02 FAIL: ${scoreResult.markerQualityGate.message}`));
68
+ }
69
+ }
70
+ function normalizeUnresolvedMarkerHint(marker) {
71
+ const hint = marker.proofText ?? marker.target ?? marker.query?.raw ?? marker.selector?.selector;
72
+ const normalized = hint?.replace(/\s+/g, ' ').trim();
73
+ return normalized && normalized.length > 0 ? normalized : 'none';
74
+ }
75
+ function formatUnresolvedMarkerLine(marker) {
76
+ const line = marker.line ?? marker.sourceContext.line;
77
+ return Number.isFinite(line) ? String(line) : 'unknown';
78
+ }
79
+ function formatUnresolvedMarkerWarning(marker) {
80
+ const line = formatUnresolvedMarkerLine(marker);
81
+ const hint = normalizeUnresolvedMarkerHint(marker);
82
+ const guidance = UNRESOLVED_MARKER_REASON_GUIDANCE[marker.reason];
83
+ return (`MKR-03 unresolved-marker marker=${marker.markerStepId} ` +
84
+ `line: ${line} reason=${marker.reason} ` +
85
+ `detail="${guidance}" hint="${hint}"`);
86
+ }
87
+ function collectUnresolvedMarkerAssertions(suitePlan) {
88
+ const seenMarkerStepIds = new Set();
89
+ const unresolvedMarkers = [];
90
+ for (const scenario of suitePlan.scenarios) {
91
+ for (const unresolvedMarker of scenario.unresolvedMarkerAssertions ?? []) {
92
+ if (seenMarkerStepIds.has(unresolvedMarker.markerStepId)) {
93
+ continue;
94
+ }
95
+ seenMarkerStepIds.add(unresolvedMarker.markerStepId);
96
+ unresolvedMarkers.push(unresolvedMarker);
97
+ }
98
+ }
99
+ return unresolvedMarkers;
100
+ }
101
+ function emitUnresolvedMarkerWarnings(suitePlan) {
102
+ if (!suitePlan) {
103
+ return;
104
+ }
105
+ const unresolvedMarkers = collectUnresolvedMarkerAssertions(suitePlan);
106
+ for (const unresolvedMarker of unresolvedMarkers) {
107
+ console.warn(pc.yellow(`[tayo] ${formatUnresolvedMarkerWarning(unresolvedMarker)}`));
108
+ }
109
+ }
110
+ function enforceMarkerGateExit(scoreResult, mode) {
111
+ if (!scoreResult.markerQualityGate.failing) {
112
+ return;
113
+ }
114
+ process.exitCode = 1;
115
+ const modeLabel = mode === 'dry-run' ? '--dry-run preview' : 'write mode output';
116
+ console.error(pc.red(`[tayo] Exiting with code 1: QUAL-02 gate failed after ${modeLabel}.`));
117
+ }
118
+ function emitLowConfidenceBanner(scoreResult) {
119
+ if (!scoreResult.requiresReview) {
120
+ return;
121
+ }
122
+ console.warn(pc.yellow(`[tayo] Manual review required — this generated test is still a draft (${scoreResult.total}/100, ${scoreResult.grade}).`));
123
+ if (scoreResult.blockers.length > 0) {
124
+ console.warn(pc.yellow(`[tayo] Top blockers: ${scoreResult.blockers.join(' | ')}`));
125
+ }
36
126
  }
37
127
  function emitScoreHints(scoreResult, queryResults = [], boundaryIssues = analyzeBoundaryIsolation('')) {
38
128
  if (scoreResult.dimensions.queryQuality < 60) {
39
129
  const testIdCount = queryResults.filter((queryResult) => {
40
130
  return queryResult.method === 'getByTestId';
41
131
  }).length;
42
- console.log(pc.yellow(`[taro] Tip: ${testIdCount} getByTestId queries — consider adding aria-label`));
132
+ console.log(pc.yellow(`[tayo] Tip: ${testIdCount} getByTestId queries — consider adding aria-label`));
43
133
  }
44
134
  if (scoreResult.dimensions.assertionSpecificity < 60) {
45
- console.log(pc.yellow('[taro] Tip: Add specific matchers like toHaveValue() for better assertions'));
135
+ console.log(pc.yellow('[tayo] Tip: Add specific matchers like toHaveValue() for better assertions'));
46
136
  }
47
137
  if (scoreResult.dimensions.testStructure < 60) {
48
- console.log(pc.yellow('[taro] Tip: Split into multiple it() blocks for better test organization'));
138
+ console.log(pc.yellow('[tayo] Tip: Split into multiple it() blocks for better test organization'));
49
139
  }
50
140
  if (scoreResult.dimensions.boundaryIsolation < 60) {
51
141
  for (const issue of boundaryIssues) {
52
- console.warn(pc.yellow(`[taro] Boundary: ${issue.message}`));
53
- console.warn(pc.yellow(`[taro] Tip: ${issue.suggestion}`));
142
+ console.warn(pc.yellow(`[tayo] Boundary: ${issue.message}`));
143
+ console.warn(pc.yellow(`[tayo] Tip: ${issue.suggestion}`));
54
144
  }
55
145
  }
56
146
  }
@@ -60,6 +150,12 @@ function summarizeCleanup(analyzedRecording) {
60
150
  if (diagnostics.removedRedundantClicks > 0) {
61
151
  parts.push(`${diagnostics.removedRedundantClicks} redundant click(s)`);
62
152
  }
153
+ if ((diagnostics.preservedSemanticMarkers ?? 0) > 0) {
154
+ parts.push(`${diagnostics.preservedSemanticMarkers} preserved semantic marker(s)`);
155
+ }
156
+ if ((diagnostics.unresolvedSemanticMarkers ?? 0) > 0) {
157
+ parts.push(`${diagnostics.unresolvedSemanticMarkers} unresolved semantic marker(s)`);
158
+ }
63
159
  if (diagnostics.removedDoubleClickNoise > 0) {
64
160
  parts.push(`${diagnostics.removedDoubleClickNoise} dblClick noise event(s)`);
65
161
  }
@@ -72,7 +168,72 @@ function summarizeCleanup(analyzedRecording) {
72
168
  if (parts.length === 0) {
73
169
  return;
74
170
  }
75
- console.log(pc.dim('[taro]') + ` Recording cleanup: ${parts.join(', ')}`);
171
+ console.log(pc.dim('[tayo]') + ` Recording cleanup: ${parts.join(', ')}`);
172
+ }
173
+ function countPlannedScenarioMarkers(scenarios) {
174
+ return scenarios.reduce((totals, scenario) => ({
175
+ emitted: totals.emitted + (scenario.markerAssertions?.length ?? 0),
176
+ unresolved: totals.unresolved + (scenario.unresolvedMarkerAssertions?.length ?? 0),
177
+ }), {
178
+ emitted: 0,
179
+ unresolved: 0,
180
+ });
181
+ }
182
+ function buildMarkerCoverageSummary(params) {
183
+ const { source, analyzedRecording, suitePlan } = params;
184
+ if (source !== 'js') {
185
+ return EMPTY_MARKER_COVERAGE;
186
+ }
187
+ const preservedMarkers = analyzedRecording.diagnostics.preservedSemanticMarkers ?? 0;
188
+ const diagnosticUnresolvedMarkers = analyzedRecording.diagnostics.unresolvedSemanticMarkers ?? 0;
189
+ if (!suitePlan) {
190
+ return {
191
+ detected: preservedMarkers + diagnosticUnresolvedMarkers,
192
+ emitted: 0,
193
+ unresolved: diagnosticUnresolvedMarkers,
194
+ };
195
+ }
196
+ const plannedMarkerTotals = countPlannedScenarioMarkers(suitePlan.scenarios);
197
+ const unresolved = plannedMarkerTotals.unresolved;
198
+ const detected = Math.max(preservedMarkers + unresolved, plannedMarkerTotals.emitted + unresolved);
199
+ return {
200
+ detected,
201
+ emitted: plannedMarkerTotals.emitted,
202
+ unresolved,
203
+ };
204
+ }
205
+ function mergeAnalyzedStepState(recording, analyzedRecording) {
206
+ const analyzedStepsById = new Map(analyzedRecording.steps
207
+ .filter((step) => Boolean(step.id))
208
+ .map((step) => [step.id, step]));
209
+ return {
210
+ ...recording,
211
+ steps: recording.steps.map((step) => {
212
+ if (!step.id) {
213
+ return step;
214
+ }
215
+ const analyzedStep = analyzedStepsById.get(step.id);
216
+ if (!analyzedStep) {
217
+ return step;
218
+ }
219
+ return {
220
+ ...step,
221
+ ...(analyzedStep.semanticMarkerCandidate
222
+ ? { semanticMarkerCandidate: analyzedStep.semanticMarkerCandidate }
223
+ : {}),
224
+ ...(analyzedStep.semanticMarkerLink
225
+ ? { semanticMarkerLink: analyzedStep.semanticMarkerLink }
226
+ : {}),
227
+ ...(analyzedStep.unresolvedSemanticMarker
228
+ ? { unresolvedSemanticMarker: analyzedStep.unresolvedSemanticMarker }
229
+ : {}),
230
+ metadata: {
231
+ ...step.metadata,
232
+ ...analyzedStep.metadata,
233
+ },
234
+ };
235
+ }),
236
+ };
76
237
  }
77
238
  function toItGroups(analyzedRecording, fallbackTitle) {
78
239
  if (analyzedRecording.intentGroups.length > 0) {
@@ -93,6 +254,110 @@ function queryDescriptorToResult(descriptor) {
93
254
  line: descriptor.line,
94
255
  };
95
256
  }
257
+ function isQueryDescriptor(value) {
258
+ return (typeof value === 'object' &&
259
+ value !== null &&
260
+ 'method' in value &&
261
+ typeof value.method === 'string');
262
+ }
263
+ function getStepQueryDescriptor(step) {
264
+ const query = step.metadata?.query;
265
+ return isQueryDescriptor(query) ? query : undefined;
266
+ }
267
+ function groupSelectorsByStepId(selectors) {
268
+ const grouped = new Map();
269
+ for (const selector of selectors) {
270
+ const current = grouped.get(selector.stepId) ?? [];
271
+ current.push(selector);
272
+ grouped.set(selector.stepId, current);
273
+ }
274
+ return grouped;
275
+ }
276
+ function mergeSelectorResolutionWarnings(resolution, warnings) {
277
+ const mergedWarnings = Array.from(new Set([...resolution.warnings, ...warnings]));
278
+ if (mergedWarnings.length === resolution.warnings.length) {
279
+ return resolution;
280
+ }
281
+ return {
282
+ ...resolution,
283
+ warnings: mergedWarnings,
284
+ };
285
+ }
286
+ function applySelectorResolution(step, resolution) {
287
+ return {
288
+ ...step,
289
+ metadata: {
290
+ ...step.metadata,
291
+ selectorResolution: resolution,
292
+ ...(resolution.status === 'resolved' ? { query: resolution.query } : {}),
293
+ },
294
+ };
295
+ }
296
+ function rehydrateItGroups(itGroups, steps) {
297
+ const stepMap = new Map(steps.map((step) => [step.id, step]));
298
+ return itGroups.map((group) => ({
299
+ ...group,
300
+ steps: group.steps.map((step) => (step.id ? stepMap.get(step.id) ?? step : step)),
301
+ }));
302
+ }
303
+ function rehydrateSuitePlan(plan, steps) {
304
+ const stepMap = new Map(steps.map((step) => [step.id, step]));
305
+ const mapStep = (step) => (step.id ? stepMap.get(step.id) ?? step : step);
306
+ return {
307
+ ...plan,
308
+ itGroups: rehydrateItGroups(plan.itGroups, steps),
309
+ helpers: plan.helpers.map((helper) => ({
310
+ ...helper,
311
+ steps: helper.steps.map(mapStep),
312
+ })),
313
+ scenarios: plan.scenarios.map((scenario) => ({
314
+ ...scenario,
315
+ steps: scenario.steps.map(mapStep),
316
+ })),
317
+ };
318
+ }
319
+ function isSemanticMarkerStep(step) {
320
+ return Boolean(step.semanticMarkerLink || step.unresolvedSemanticMarker);
321
+ }
322
+ function stripSemanticMarkerStepsFromItGroups(itGroups) {
323
+ return itGroups
324
+ .map((group) => ({
325
+ ...group,
326
+ steps: group.steps.filter((step) => !isSemanticMarkerStep(step)),
327
+ }))
328
+ .filter((group) => group.steps.length > 0);
329
+ }
330
+ function stripSemanticMarkerStepsFromHelpers(helpers) {
331
+ return helpers
332
+ .map((helper) => ({
333
+ ...helper,
334
+ steps: helper.steps.filter((step) => !isSemanticMarkerStep(step)),
335
+ }))
336
+ .filter((helper) => helper.steps.length > 0);
337
+ }
338
+ function stripSemanticMarkerStepsFromScenarios(scenarios, helpers) {
339
+ const helperNames = new Set(helpers.map((helper) => helper.name));
340
+ return scenarios
341
+ .map((scenario) => ({
342
+ ...scenario,
343
+ steps: scenario.steps.filter((step) => !isSemanticMarkerStep(step)),
344
+ helperRefs: scenario.helperRefs.filter((helperRef) => helperNames.has(helperRef)),
345
+ }))
346
+ .filter((scenario) => scenario.steps.length > 0 ||
347
+ scenario.helperRefs.length > 0 ||
348
+ (scenario.markerAssertions?.length ?? 0) > 0);
349
+ }
350
+ function dedupeQueryResults(queryResults) {
351
+ const seen = new Set();
352
+ return queryResults.filter((queryResult) => {
353
+ const key = `${queryResult.method}:${queryResult.query}:${queryResult.line ?? 'na'}`;
354
+ if (seen.has(key)) {
355
+ return false;
356
+ }
357
+ seen.add(key);
358
+ return true;
359
+ });
360
+ }
96
361
  function getPrimarySelector(recording) {
97
362
  return recording.baseline?.selectors[0]?.selector;
98
363
  }
@@ -107,7 +372,7 @@ function summarizeVisualState(visualState) {
107
372
  if (visualState.screenshotPath) {
108
373
  parts.push(`screenshot=${visualState.screenshotPath}`);
109
374
  }
110
- console.log(pc.dim('[taro]') + ` Visual state: ${parts.join(', ')}`);
375
+ console.log(pc.dim('[tayo]') + ` Visual state: ${parts.join(', ')}`);
111
376
  }
112
377
  function summarizeMockAnalysis(mockAnalysis) {
113
378
  if (!mockAnalysis) {
@@ -126,71 +391,173 @@ function summarizeMockAnalysis(mockAnalysis) {
126
391
  if (parts.length === 0) {
127
392
  return;
128
393
  }
129
- console.log(pc.dim('[taro]') + ` Mock analysis: ${parts.join(', ')}`);
394
+ console.log(pc.dim('[tayo]') + ` Mock analysis: ${parts.join(', ')}`);
130
395
  const topRecommendation = mockAnalysis.recommendations[0];
131
396
  if (topRecommendation) {
132
- console.log(pc.dim('[taro]') +
397
+ console.log(pc.dim('[tayo]') +
133
398
  ` Mock hint: ${topRecommendation.kind} ${topRecommendation.target} (${topRecommendation.count} file(s))`);
134
399
  }
135
400
  const topLifecycle = mockAnalysis.mutationLifecycles[0];
136
401
  if (topLifecycle) {
137
- console.log(pc.dim('[taro]') +
402
+ console.log(pc.dim('[tayo]') +
138
403
  ` Mutation lifecycle: ${topLifecycle.stages.join(' -> ')} in ${topLifecycle.file}`);
139
404
  }
140
405
  const topWarning = mockAnalysis.instabilityWarnings[0];
141
406
  if (topWarning) {
142
- console.warn(pc.yellow(`[taro] Mock stability: ${topWarning.reason} (${topWarning.file})`));
407
+ console.warn(pc.yellow(`[tayo] Mock stability: ${topWarning.reason} (${topWarning.file})`));
143
408
  }
144
409
  }
145
410
  function summarizeBoundaryWarnings(warnings) {
146
411
  for (const warning of warnings) {
147
- console.warn(pc.yellow(`[taro] Boundary: ${warning}`));
412
+ console.warn(pc.yellow(`[tayo] Boundary: ${warning}`));
413
+ }
414
+ }
415
+ function tokenizeSuiteHint(value) {
416
+ return value
417
+ .toLowerCase()
418
+ .split(/[^a-z0-9]+/)
419
+ .filter((token) => token.length >= 3);
420
+ }
421
+ function scoreRenderTargetCandidate(candidate, recording, mockAnalysis, suitePlan) {
422
+ const recordingTokens = new Set([
423
+ ...tokenizeSuiteHint(recording.title),
424
+ ...recording.steps.flatMap((step) => tokenizeSuiteHint(step.target ?? '')),
425
+ ]);
426
+ const candidateTokens = new Set([
427
+ ...tokenizeSuiteHint(candidate.symbol),
428
+ ...tokenizeSuiteHint(candidate.importPath),
429
+ ...tokenizeSuiteHint(candidate.sourceTestFile),
430
+ ...candidate.helperNames.flatMap((name) => tokenizeSuiteHint(name)),
431
+ ]);
432
+ let score = 0;
433
+ for (const token of candidateTokens) {
434
+ if (recordingTokens.has(token)) {
435
+ score += 3;
436
+ }
148
437
  }
438
+ if (/Module$/u.test(candidate.symbol) && suitePlan.renderBoundary.kind === 'module') {
439
+ score += 4;
440
+ }
441
+ if (candidate.usesWithin) {
442
+ score += 1;
443
+ }
444
+ if (mockAnalysis?.repeatedTargets.length) {
445
+ score += 1;
446
+ }
447
+ return score;
448
+ }
449
+ function resolveRepoRenderTarget(params) {
450
+ const { candidates, recording, mockAnalysis, suitePlan } = params;
451
+ if (candidates.length === 0) {
452
+ return null;
453
+ }
454
+ const ranked = candidates
455
+ .map((candidate) => ({
456
+ candidate,
457
+ score: scoreRenderTargetCandidate(candidate, recording, mockAnalysis, suitePlan),
458
+ }))
459
+ .filter((entry) => entry.score > 0)
460
+ .sort((left, right) => right.score - left.score || left.candidate.symbol.localeCompare(right.candidate.symbol));
461
+ return ranked[0]?.candidate ?? null;
462
+ }
463
+ function applyRepoRenderTarget(suitePlan, renderTarget) {
464
+ if (!renderTarget) {
465
+ return suitePlan;
466
+ }
467
+ return {
468
+ ...suitePlan,
469
+ renderBoundary: {
470
+ ...suitePlan.renderBoundary,
471
+ resolvedTarget: renderTarget.symbol,
472
+ confidence: suitePlan.renderBoundary.confidence === 'low' ? 'medium' : suitePlan.renderBoundary.confidence,
473
+ },
474
+ warnings: suitePlan.warnings.filter((warning) => !warning.includes('Tayo could not resolve the exact render target from repo context') &&
475
+ !warning.includes('Prefer a repo-local module/container render boundary')),
476
+ };
149
477
  }
150
478
  function findRecordingUrl(analyzedRecording) {
151
479
  return analyzedRecording.url ?? analyzedRecording.steps.find((step) => step.action === 'navigate')?.target;
152
480
  }
153
- async function resolveJsQueryResults(recording) {
481
+ async function resolveJsGeneration(recording, itGroups) {
154
482
  const baseline = recording.baseline;
155
483
  if (!baseline) {
156
- return [];
484
+ return {
485
+ itGroups,
486
+ queryResults: [],
487
+ recording,
488
+ warnings: [],
489
+ };
157
490
  }
158
491
  const queryResults = baseline.queries.map(queryDescriptorToResult);
159
- if (baseline.selectors.length > 0 && recording.url) {
160
- console.log(pc.dim('[taro]') +
492
+ const warnings = [];
493
+ const selectorGroups = groupSelectorsByStepId(baseline.selectors);
494
+ const stepMap = new Map(recording.steps
495
+ .filter((step) => Boolean(step.id))
496
+ .map((step) => [step.id, step]));
497
+ const updatedSteps = new Map();
498
+ if (selectorGroups.size > 0 && recording.url) {
499
+ console.log(pc.dim('[tayo]') +
161
500
  ` Resolving ${baseline.selectors.length} selector(s) via Playwright...`);
162
- const selectors = baseline.selectors.map((descriptor) => descriptor.selector);
163
- const elementMap = await inspectElements(recording.url, selectors);
164
- for (const descriptor of baseline.selectors) {
165
- const info = elementMap.get(descriptor.selector) ?? null;
166
- if (!info) {
167
- const fallbackResult = buildQuery({
168
- tagName: 'div',
169
- role: null,
170
- ariaLabel: null,
171
- ariaLabelledBy: null,
172
- innerText: '',
173
- value: undefined,
174
- type: undefined,
175
- placeholder: null,
176
- isPresent: false,
177
- }, descriptor.selector);
178
- queryResults.push({ ...fallbackResult, line: descriptor.line });
179
- continue;
501
+ }
502
+ for (const [stepId, selectors] of selectorGroups) {
503
+ const step = updatedSteps.get(stepId) ?? stepMap.get(stepId);
504
+ if (!step) {
505
+ continue;
506
+ }
507
+ const preservedQuery = getStepQueryDescriptor(step);
508
+ const stepWarnings = [];
509
+ let chosenResolution;
510
+ if (preservedQuery) {
511
+ chosenResolution = await resolveSelector(selectors[0], {
512
+ url: recording.url,
513
+ preservedQuery,
514
+ });
515
+ }
516
+ else {
517
+ for (const selector of selectors) {
518
+ const resolution = await resolveSelector(selector, {
519
+ url: recording.url,
520
+ });
521
+ if (resolution.status === 'resolved') {
522
+ chosenResolution = resolution;
523
+ break;
524
+ }
525
+ stepWarnings.push(...resolution.warnings);
526
+ chosenResolution ??= resolution;
180
527
  }
181
- const result = buildQuery(info, descriptor.selector);
182
- if (result.quality === 'fragile') {
183
- emitQry03Warning(descriptor.selector);
528
+ }
529
+ if (!chosenResolution) {
530
+ continue;
531
+ }
532
+ const resolution = mergeSelectorResolutionWarnings(chosenResolution, stepWarnings);
533
+ updatedSteps.set(stepId, applySelectorResolution(step, resolution));
534
+ if (resolution.status === 'resolved') {
535
+ if (resolution.outcome !== 'preserved-query') {
536
+ queryResults.push(queryDescriptorToResult(resolution.query));
184
537
  }
185
- const matcher = selectMatcher(info, 'assert');
186
- queryResults.push({ ...result, matcher, line: descriptor.line });
538
+ continue;
187
539
  }
540
+ warnings.push(`QRY-03 [${stepId}] unresolved selector ${resolution.selector.selector}: ${resolution.reason}`);
188
541
  }
189
- else if (baseline.selectors.length > 0 && !recording.url) {
190
- console.warn(pc.yellow('[taro]') +
191
- ' QRY-02: No @jest-environment-options URL found — cannot resolve document.querySelector selectors. Falling back to getByTestId.');
542
+ const resolvedSteps = recording.steps.map((step) => step.id ? updatedSteps.get(step.id) ?? step : step);
543
+ return {
544
+ itGroups: rehydrateItGroups(itGroups, resolvedSteps),
545
+ queryResults: dedupeQueryResults(queryResults),
546
+ recording: {
547
+ ...recording,
548
+ baseline: {
549
+ ...baseline,
550
+ itGroups: rehydrateItGroups(baseline.itGroups, resolvedSteps),
551
+ },
552
+ steps: resolvedSteps,
553
+ },
554
+ warnings,
555
+ };
556
+ }
557
+ function summarizeSelectorWarnings(warnings) {
558
+ for (const warning of warnings) {
559
+ console.warn(pc.yellow(`[tayo] ${warning}`));
192
560
  }
193
- return queryResults;
194
561
  }
195
562
  async function maybeCaptureVisualState(params) {
196
563
  const { analyzedRecording, projectRoot, selector, url } = params;
@@ -198,7 +565,7 @@ async function maybeCaptureVisualState(params) {
198
565
  return null;
199
566
  }
200
567
  const candidates = findVisualCaptureCandidates(analyzedRecording);
201
- const visualDir = join(projectRoot, '.taro', 'visual');
568
+ const visualDir = join(projectRoot, '.tayo', 'visual');
202
569
  if (candidates.length > 0) {
203
570
  await mkdir(visualDir, { recursive: true });
204
571
  return captureVisualState(url, {
@@ -226,7 +593,7 @@ async function maybeAnalyzeMocks(projectRoot) {
226
593
  }
227
594
  }
228
595
  async function appendHistoryEntry(projectRoot, historyEntry) {
229
- const taroDir = join(projectRoot, '.taro');
596
+ const taroDir = join(projectRoot, '.tayo');
230
597
  await mkdir(taroDir, { recursive: true });
231
598
  const historyPath = join(taroDir, 'history.json');
232
599
  let history = [];
@@ -245,12 +612,12 @@ async function finalizeGeneratedOutput(params) {
245
612
  const { code, outputPath, projectRoot, recordingFile, scoreResult } = params;
246
613
  const verification = verifySyntax(code, outputPath);
247
614
  if (!verification.valid) {
248
- console.error(pc.red('[taro] Error: Post-write verification failed'));
615
+ console.error(pc.red('[tayo] Error: Post-write verification failed'));
249
616
  console.error(pc.red(` ${verification.error}`));
250
- console.error(pc.red(' This is a Taro bug. Please report it.'));
617
+ console.error(pc.red(' This is a Tayo bug. Please report it.'));
251
618
  process.exit(1);
252
619
  }
253
- console.log(pc.green('[taro] ✓ post-write verified'));
620
+ console.log(pc.green('[tayo] ✓ post-write verified'));
254
621
  await appendHistoryEntry(projectRoot, {
255
622
  timestamp: new Date().toISOString(),
256
623
  recordingFile,
@@ -269,8 +636,8 @@ async function finalizeGeneratedOutput(params) {
269
636
  export function createGenerateCommand() {
270
637
  const generate = new Command('generate');
271
638
  generate
272
- .description('Generate RTL test from Recorder export')
273
- .argument('<file>', 'Path to the recorder export file')
639
+ .description('Generate RTL test from Recorder JS or Chrome Recorder JSON export')
640
+ .argument('<file>', 'Path to the recorder export file (.js or .json)')
274
641
  .option('-o, --output <path>', 'Output file path for the generated test')
275
642
  .option('-d, --dry-run', 'Preview the generated test without writing to disk', false)
276
643
  .option('-f, --force', 'Overwrite existing test file', false)
@@ -295,12 +662,14 @@ export function createGenerateCommand() {
295
662
  }
296
663
  const normalizedRecording = parsedInput.source === 'js' ? normalizeJsBaseline(parsedInput) : parsedInput.recording;
297
664
  let conventions = undefined;
665
+ let repoRenderTargets = [];
298
666
  if (parsedInput.source === 'js') {
299
667
  conventions = await readConventions(projectRoot);
300
668
  if (!conventions) {
301
- console.log(pc.dim('[taro]') + ' Scanning project conventions...');
669
+ console.log(pc.dim('[tayo]') + ' Scanning project conventions...');
302
670
  conventions = await scanConventions(projectRoot);
303
671
  }
672
+ repoRenderTargets = await discoverRepoRenderTargets(projectRoot);
304
673
  }
305
674
  console.log(pc.green('Parsed:') +
306
675
  ` ${pc.bold(normalizedRecording.title)} — ${normalizedRecording.steps.length} steps` +
@@ -308,6 +677,9 @@ export function createGenerateCommand() {
308
677
  ? `, ${normalizedRecording.baseline?.itGroups.length ?? 0} test group(s)`
309
678
  : ''));
310
679
  const analyzedRecording = analyzeRecording(normalizedRecording);
680
+ const markerAwareRecording = parsedInput.source === 'js'
681
+ ? mergeAnalyzedStepState(normalizedRecording, analyzedRecording)
682
+ : normalizedRecording;
311
683
  summarizeCleanup(analyzedRecording);
312
684
  const visualState = await maybeCaptureVisualState({
313
685
  analyzedRecording,
@@ -318,49 +690,94 @@ export function createGenerateCommand() {
318
690
  summarizeVisualState(visualState);
319
691
  const mockAnalysis = await maybeAnalyzeMocks(projectRoot);
320
692
  summarizeMockAnalysis(mockAnalysis);
321
- const queryResults = parsedInput.source === 'js' ? await resolveJsQueryResults(normalizedRecording) : [];
322
693
  const outputPath = options.output ?? deriveOutputPath(filePath);
323
- const jsSuitePlan = parsedInput.source === 'js'
694
+ const rawJsSuitePlan = parsedInput.source === 'js'
324
695
  ? planJsSuite({
325
- recording: normalizedRecording,
696
+ recording: markerAwareRecording,
326
697
  analyzedRecording,
327
698
  mockAnalysis,
328
699
  fallbackTitle: normalizedRecording.title,
329
700
  })
330
701
  : null;
702
+ const repoRenderTarget = parsedInput.source === 'js' && rawJsSuitePlan
703
+ ? resolveRepoRenderTarget({
704
+ candidates: repoRenderTargets,
705
+ recording: normalizedRecording,
706
+ mockAnalysis,
707
+ suitePlan: rawJsSuitePlan,
708
+ })
709
+ : null;
710
+ const jsSuitePlan = rawJsSuitePlan
711
+ ? applyRepoRenderTarget(rawJsSuitePlan, repoRenderTarget)
712
+ : null;
331
713
  if (jsSuitePlan) {
332
714
  summarizeBoundaryWarnings(jsSuitePlan.warnings);
333
715
  }
716
+ const resolvedJsGeneration = parsedInput.source === 'js'
717
+ ? await resolveJsGeneration(markerAwareRecording, jsSuitePlan?.itGroups ?? toItGroups(analyzedRecording, normalizedRecording.title))
718
+ : null;
719
+ if (resolvedJsGeneration) {
720
+ summarizeSelectorWarnings(resolvedJsGeneration.warnings);
721
+ }
722
+ const hydratedSuitePlan = parsedInput.source === 'js' && jsSuitePlan
723
+ ? rehydrateSuitePlan(jsSuitePlan, resolvedJsGeneration?.recording.steps ?? markerAwareRecording.steps)
724
+ : jsSuitePlan;
725
+ const generationHelpers = hydratedSuitePlan
726
+ ? stripSemanticMarkerStepsFromHelpers(hydratedSuitePlan.helpers)
727
+ : undefined;
728
+ const generationScenarios = hydratedSuitePlan && generationHelpers
729
+ ? stripSemanticMarkerStepsFromScenarios(hydratedSuitePlan.scenarios, generationHelpers)
730
+ : undefined;
731
+ const generationItGroups = parsedInput.source === 'js'
732
+ ? stripSemanticMarkerStepsFromItGroups(resolvedJsGeneration?.itGroups ??
733
+ hydratedSuitePlan?.itGroups ??
734
+ toItGroups(analyzedRecording, normalizedRecording.title))
735
+ : toItGroups(analyzedRecording, normalizedRecording.title);
334
736
  const generated = parsedInput.source === 'js'
335
- ? generateTestFromGroups(normalizedRecording.title, jsSuitePlan?.itGroups ?? toItGroups(analyzedRecording, normalizedRecording.title), {
737
+ ? generateTestFromGroups(normalizedRecording.title, generationItGroups, {
336
738
  outputPath,
337
739
  dryRun: options.dryRun,
338
740
  conventions,
339
- queryResults,
741
+ queryResults: resolvedJsGeneration?.queryResults ?? [],
742
+ helpers: generationHelpers,
743
+ scenarios: generationScenarios,
744
+ renderTarget: repoRenderTarget,
340
745
  })
341
746
  : generateTest(analyzedRecording, { outputPath, dryRun: options.dryRun });
342
- if (jsSuitePlan?.warnings.length) {
747
+ const markerCoverage = buildMarkerCoverageSummary({
748
+ source: parsedInput.source,
749
+ analyzedRecording,
750
+ suitePlan: hydratedSuitePlan,
751
+ });
752
+ if (hydratedSuitePlan?.warnings.length) {
343
753
  generated.code = [
344
- ...jsSuitePlan.warnings.map((warning) => `// taro-boundary-warning: ${warning}`),
754
+ ...hydratedSuitePlan.warnings.map((warning) => `// tayo-boundary-warning: ${warning}`),
345
755
  generated.code,
346
756
  ].join('\n');
347
757
  }
348
758
  if (parsedInput.source === 'js') {
349
- emitQuerySummary(queryResults);
759
+ emitQuerySummary(resolvedJsGeneration?.queryResults ?? []);
350
760
  }
351
761
  const scoreResult = parsedInput.source === 'js'
352
- ? scoreGeneratedTest(generated.code, queryResults)
353
- : scoreGeneratedTest(generated.code);
762
+ ? scoreGeneratedTest(generated.code, {
763
+ queryResults: resolvedJsGeneration?.queryResults ?? [],
764
+ markerCoverage,
765
+ })
766
+ : scoreGeneratedTest(generated.code, { markerCoverage });
354
767
  const boundaryIssues = analyzeBoundaryIsolation(generated.code);
355
768
  logScore(scoreResult);
356
- emitScoreHints(scoreResult, queryResults, boundaryIssues);
769
+ emitMarkerCoverageSection(scoreResult);
770
+ emitUnresolvedMarkerWarnings(hydratedSuitePlan);
771
+ emitLowConfidenceBanner(scoreResult);
772
+ emitScoreHints(scoreResult, resolvedJsGeneration?.queryResults ?? [], boundaryIssues);
357
773
  if (options.dryRun) {
358
774
  console.log(pc.yellow('\nDry run — test preview:\n'));
359
775
  console.log(pc.dim('─'.repeat(60)));
360
776
  console.log(generated.code);
361
777
  console.log(pc.dim('─'.repeat(60)));
362
- console.log(pc.dim(`\n[taro] Score: ${scoreResult.total}/100 (${scoreResult.grade})`));
778
+ console.log(pc.dim(`\n[tayo] Score: ${scoreResult.total}/100 (${scoreResult.grade})`));
363
779
  console.log(pc.yellow(`\nWould write to: ${pc.bold(outputPath)}`));
780
+ enforceMarkerGateExit(scoreResult, 'dry-run');
364
781
  return;
365
782
  }
366
783
  try {
@@ -377,6 +794,7 @@ export function createGenerateCommand() {
377
794
  });
378
795
  const action = result.overwritten ? pc.yellow('Updated') : pc.green('Created');
379
796
  console.log(`${action}: ${pc.bold(result.filePath)}`);
797
+ enforceMarkerGateExit(scoreResult, 'write');
380
798
  }
381
799
  catch (err) {
382
800
  console.error(pc.red('Error:') + ` ${String(err)}`);