@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.
- package/README.md +66 -33
- package/assets/claude/commands/@tayo-dev/rtl/generate.md +2 -2
- package/assets/claude/commands/@tayo-dev/rtl/help.md +1 -1
- package/assets/codex/@tayo-dev/rtl-conventions/SKILL.md +2 -2
- package/assets/codex/@tayo-dev/rtl-generate/SKILL.md +6 -5
- package/assets/codex/@tayo-dev/rtl-help/SKILL.md +1 -1
- package/assets/gemini/commands/@tayo-dev/rtl/generate.toml +2 -2
- package/assets/gemini/commands/@tayo-dev/rtl/help.toml +1 -1
- package/assets/opencode/commands/@tayo-dev/rtl-generate.md +2 -2
- package/assets/opencode/commands/@tayo-dev/rtl-help.md +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +485 -67
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/install.js +6 -6
- package/dist/core/baseline-normalizer.d.ts.map +1 -1
- package/dist/core/baseline-normalizer.js +42 -0
- package/dist/core/baseline-normalizer.js.map +1 -1
- package/dist/core/generator.d.ts +5 -1
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +235 -17
- package/dist/core/generator.js.map +1 -1
- package/dist/core/input-loader.d.ts.map +1 -1
- package/dist/core/input-loader.js +1 -0
- package/dist/core/input-loader.js.map +1 -1
- package/dist/core/js-parser.d.ts +2 -1
- package/dist/core/js-parser.d.ts.map +1 -1
- package/dist/core/js-parser.js +69 -0
- package/dist/core/js-parser.js.map +1 -1
- package/dist/core/orchestrator.d.ts +1 -1
- package/dist/core/orchestrator.js +4 -4
- package/dist/core/parser.js +2 -2
- package/dist/core/recording-intelligence.d.ts +1 -1
- package/dist/core/recording-intelligence.d.ts.map +1 -1
- package/dist/core/recording-intelligence.js +298 -4
- package/dist/core/recording-intelligence.js.map +1 -1
- package/dist/core/resolver.d.ts +28 -2
- package/dist/core/resolver.d.ts.map +1 -1
- package/dist/core/resolver.js +462 -23
- package/dist/core/resolver.js.map +1 -1
- package/dist/core/scanner.d.ts +11 -3
- package/dist/core/scanner.d.ts.map +1 -1
- package/dist/core/scanner.js +47 -9
- package/dist/core/scanner.js.map +1 -1
- package/dist/core/scorer.d.ts +6 -2
- package/dist/core/scorer.d.ts.map +1 -1
- package/dist/core/scorer.js +163 -9
- package/dist/core/scorer.js.map +1 -1
- package/dist/core/suite-planner.d.ts +4 -1
- package/dist/core/suite-planner.d.ts.map +1 -1
- package/dist/core/suite-planner.js +261 -11
- package/dist/core/suite-planner.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -4
- package/dist/install/planner.js +1 -1
- package/dist/install/types.d.ts +1 -1
- package/dist/learner/index.d.ts +2 -2
- package/dist/learner/index.js +3 -3
- package/dist/learner/storage.d.ts +1 -1
- package/dist/learner/storage.js +2 -2
- package/dist/templates/test-template.d.ts +26 -2
- package/dist/templates/test-template.d.ts.map +1 -1
- package/dist/templates/test-template.js +46 -6
- package/dist/templates/test-template.js.map +1 -1
- package/dist/types/recording.d.ts +162 -0
- package/dist/types/recording.d.ts.map +1 -1
- package/dist/types/recording.js.map +1 -1
- package/dist/types/score.d.ts +37 -0
- package/dist/types/score.d.ts.map +1 -1
- 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,
|
|
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
|
-
|
|
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(`[
|
|
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('[
|
|
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('[
|
|
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(`[
|
|
53
|
-
console.warn(pc.yellow(`[
|
|
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('[
|
|
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('[
|
|
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('[
|
|
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('[
|
|
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('[
|
|
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(`[
|
|
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(`[
|
|
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
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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, '.
|
|
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, '.
|
|
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('[
|
|
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
|
|
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('[
|
|
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('[
|
|
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
|
|
694
|
+
const rawJsSuitePlan = parsedInput.source === 'js'
|
|
324
695
|
? planJsSuite({
|
|
325
|
-
recording:
|
|
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,
|
|
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
|
-
|
|
747
|
+
const markerCoverage = buildMarkerCoverageSummary({
|
|
748
|
+
source: parsedInput.source,
|
|
749
|
+
analyzedRecording,
|
|
750
|
+
suitePlan: hydratedSuitePlan,
|
|
751
|
+
});
|
|
752
|
+
if (hydratedSuitePlan?.warnings.length) {
|
|
343
753
|
generated.code = [
|
|
344
|
-
...
|
|
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,
|
|
353
|
-
|
|
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
|
-
|
|
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[
|
|
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)}`);
|