@skip-ai/scanner 0.5.0 → 0.7.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/dist/index.js CHANGED
@@ -2,11 +2,9 @@
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
  import chalk from 'chalk';
5
- import { parseCli } from './cli.js';
6
- import { maskObject } from './scanner/secrets.js';
7
- import { uploadReport, pollStatus } from './uploader.js';
8
- import { banner, spinner, printSummary, fail } from './ui.js';
9
- // Modo legado de extração
5
+ import { parseCli, maskToken } from './cli.js';
6
+ import { uploadArtifactBundle } from './uploader.js';
7
+ import { banner, spinner, fail } from './ui.js';
10
8
  import { detectProject } from './detector.js';
11
9
  import { extractRoutes } from './scanner/routes.js';
12
10
  import { extractComponents } from './scanner/components.js';
@@ -16,7 +14,20 @@ import { buildBriefing } from './scanner/briefing.js';
16
14
  import { buildWidgetSnippet } from './scanner/widget-snippet.js';
17
15
  import { auditWcag, summarize, WCAG_RULES_VERSION } from './scanner/wcag.js';
18
16
  import { calculateScore, scoreColor } from './scanner/score.js';
19
- /** Aplica cor do chalk conforme o score (necessário porque chalk não é indexável por string). */
17
+ import { detectGuidedFlows } from './scanner/guided-flows.js';
18
+ import { appendHistory } from './scanner/history.js';
19
+ import { buildReport, maskReportSecrets, CLI_VERSION } from './reporter.js';
20
+ import { DEFAULT_IGNORE_PATTERNS } from './scanner/ignore.js';
21
+ import { buildScanArtifact, detectArtifactType, loadJsonFile, validateFileManifestArtifact, validateReportArtifact, validateUploadManifestArtifact, validateValidationArtifact, validateWcagAuditArtifact, } from './artifacts.js';
22
+ import { buildSemanticApplicationMap, createFileManifest, normalizeSemanticApplicationMap, validateSamArtifact, writeSkipSamFile, } from './sam.js';
23
+ const REQUIRED_ARTIFACTS = [
24
+ '.skip-report.json',
25
+ '.skip-sam.json',
26
+ '.skip-wcag-audit.json',
27
+ 'scan-file-manifest.json',
28
+ 'upload-manifest.json',
29
+ 'scan-validation.json',
30
+ ];
20
31
  function colorScore(score) {
21
32
  const c = scoreColor(score);
22
33
  if (c === 'green')
@@ -27,113 +38,267 @@ function colorScore(score) {
27
38
  return chalk.bold.magenta(`${score}/100`);
28
39
  return chalk.bold.red(`${score}/100`);
29
40
  }
30
- import { detectGuidedFlows } from './scanner/guided-flows.js';
31
- import { appendHistory } from './scanner/history.js';
32
- import { buildReport, maskReportSecrets, CLI_VERSION } from './reporter.js';
33
- import { DEFAULT_IGNORE_PATTERNS } from './scanner/ignore.js';
34
- import { maskToken } from './cli.js';
35
- // ============ MODO SEND (transportador puro) ============
41
+ function createScanId() {
42
+ return `scan_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
43
+ }
44
+ function writeJson(filePath, value) {
45
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
46
+ fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
47
+ }
48
+ function appendLog(outputDir, message) {
49
+ fs.mkdirSync(outputDir, { recursive: true });
50
+ fs.appendFileSync(path.join(outputDir, 'scanner.log'), `${new Date().toISOString()} ${message}\n`);
51
+ }
52
+ function safeRelative(from, to) {
53
+ return path.relative(from, to).replace(/\\/g, '/') || '.';
54
+ }
55
+ function discoverApplicationRoots(workspaceRoot) {
56
+ const candidates = ['.', 'apps/*', 'packages/*'];
57
+ const roots = new Set();
58
+ for (const pattern of candidates) {
59
+ const base = pattern.endsWith('/*') ? pattern.slice(0, -2) : pattern;
60
+ const absBase = path.join(workspaceRoot, base);
61
+ if (!fs.existsSync(absBase))
62
+ continue;
63
+ if (pattern === '.') {
64
+ if (fs.existsSync(path.join(workspaceRoot, 'package.json')))
65
+ roots.add(workspaceRoot);
66
+ continue;
67
+ }
68
+ for (const name of fs.readdirSync(absBase)) {
69
+ const dir = path.join(absBase, name);
70
+ if (fs.statSync(dir).isDirectory() && fs.existsSync(path.join(dir, 'package.json')))
71
+ roots.add(dir);
72
+ }
73
+ }
74
+ return [...roots];
75
+ }
76
+ function selectApplicationRoot(workspaceRoot, app) {
77
+ if (app)
78
+ return path.resolve(workspaceRoot, app);
79
+ const roots = discoverApplicationRoots(workspaceRoot);
80
+ const frontend = roots.find((root) => {
81
+ try {
82
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf-8'));
83
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
84
+ return Boolean(deps.next || deps.vite || deps.react || deps.vue || deps['react-router-dom']);
85
+ }
86
+ catch {
87
+ return false;
88
+ }
89
+ });
90
+ return frontend ?? workspaceRoot;
91
+ }
92
+ function validateArtifactByType(filePath, type, context) {
93
+ const payload = loadJsonFile(filePath);
94
+ if (type === 'semantic-map')
95
+ return validateSamArtifact(payload, context);
96
+ if (type === 'report')
97
+ return validateReportArtifact(payload);
98
+ if (type === 'wcag-audit')
99
+ return validateWcagAuditArtifact(payload);
100
+ if (type === 'file-manifest')
101
+ return validateFileManifestArtifact(payload);
102
+ if (type === 'upload-manifest')
103
+ return validateUploadManifestArtifact(payload);
104
+ if (type === 'validation')
105
+ return validateValidationArtifact(payload);
106
+ return { valid: true, errors: [], warnings: [], artifactType: type };
107
+ }
36
108
  async function sendMode(opts) {
109
+ if (opts.dir) {
110
+ const dir = path.resolve(opts.dir);
111
+ const missing = REQUIRED_ARTIFACTS.filter((file) => !fs.existsSync(path.join(dir, file)));
112
+ if (missing.length)
113
+ throw new Error(`Upload parcial bloqueado. Artefatos ausentes em ${dir}:\n${missing.join('\n')}`);
114
+ opts.file = REQUIRED_ARTIFACTS.map((file) => path.join(dir, file));
115
+ }
37
116
  console.log(banner(CLI_VERSION));
38
- console.log(chalk.dim(` Modo: send (transportador)`));
117
+ console.log(chalk.dim(' Modo: send (artefatos)'));
39
118
  console.log(chalk.dim(` Plataforma: ${opts.url}`));
40
119
  console.log(chalk.dim(` Token: ${maskToken(opts.token)}`));
41
- console.log(chalk.dim(` Arquivo: ${opts.file}\n`));
42
- // 1. Ler o payload do arquivo
43
- if (!fs.existsSync(opts.file)) {
44
- throw new Error(`Arquivo não encontrado: ${opts.file}`);
45
- }
46
- const readSpinner = spinner('Lendo payload...');
47
- let payload;
48
- try {
49
- payload = JSON.parse(fs.readFileSync(opts.file, 'utf-8'));
50
- }
51
- catch (e) {
52
- readSpinner.fail();
53
- throw new Error(`JSON inválido em ${opts.file}: ${e instanceof Error ? e.message : String(e)}`);
54
- }
55
- readSpinner.succeed('Payload carregado.');
56
- // 2. Checagem mínima de estrutura — bloqueia se não há dados de scan
57
- if (!payload.navigationMap && !payload.routes && !payload.wcag) {
58
- throw new Error('O payload não contém navigationMap, routes nem wcag. Use o validador da skill antes de enviar: node validate-report.js <arquivo>');
59
- }
60
- // Aviso (não bloqueia) se faltar auditoria WCAG — o scan estará incompleto
61
- if (!payload.wcag) {
62
- console.log(chalk.yellow(' ⚠ Payload sem bloco wcag — o scan estará incompleto no dashboard.'));
63
- }
64
- // 3. Preencher metadados se faltarem
65
- if (!payload.metadata) {
66
- payload.metadata = {
67
- framework: payload.framework ?? 'unknown',
68
- language: payload.language ?? 'TypeScript',
69
- packageCount: 0,
70
- typescript: payload.language === 'TypeScript',
71
- scannedAt: new Date().toISOString(),
72
- cliVersion: CLI_VERSION,
73
- };
74
- }
75
- // 4. Mascara secrets
76
- const { result: masked, count: secretsFound } = maskObject(payload);
77
- const report = { ...masked, secretsFound };
78
- if (secretsFound > 0) {
79
- console.log(chalk.yellow(`⚠ ${secretsFound} possível(eis) secret(s) mascarado(s) antes do envio.`));
80
- }
81
- if (opts.verbose) {
82
- console.log('\n' + chalk.dim('Payload a enviar:'));
83
- console.log(chalk.dim(JSON.stringify(report, null, 2).slice(0, 2000) + (JSON.stringify(report).length > 2000 ? '...' : '')));
84
- }
85
- // 5. Enviar
86
- const uploadSpinner = spinner('Enviando para o Skip Cloud...');
87
- const scanId = await uploadReport(report, opts.token, opts.url);
88
- uploadSpinner.succeed(`Scan enviado! ID: ${chalk.yellow(scanId)}`);
89
- // 6. Polling (opcional)
90
- if (opts.noPolling) {
91
- console.log(chalk.dim('\n--no-polling: não aguardando enriquecimento.\n'));
92
- return;
120
+ console.log(chalk.dim(` Arquivos: ${opts.file.join(', ')}\n`));
121
+ const artifacts = [];
122
+ let expectedScanId = opts.scanId;
123
+ for (const file of opts.file) {
124
+ if (!fs.existsSync(file))
125
+ throw new Error(`Arquivo não encontrado: ${file}`);
126
+ let payload;
127
+ try {
128
+ payload = loadJsonFile(file);
129
+ }
130
+ catch (e) {
131
+ throw new Error(`JSON inválido em ${file}: ${e instanceof Error ? e.message : String(e)}`);
132
+ }
133
+ const detected = opts.type ?? detectArtifactType(file, payload);
134
+ if (detected === 'semantic-map') {
135
+ const result = validateSamArtifact(payload, {
136
+ currentScanId: expectedScanId,
137
+ expectedFilename: '.skip-sam.json',
138
+ });
139
+ if (!result.valid)
140
+ throw new Error(result.errors.join('\n'));
141
+ expectedScanId = String(payload.scanId ?? expectedScanId ?? '');
142
+ }
143
+ else {
144
+ const result = validateArtifactByType(file, detected, {});
145
+ if (!result.valid)
146
+ throw new Error(result.errors.join('\n'));
147
+ }
148
+ const scanId = String(payload.scanId ?? expectedScanId ?? createScanId());
149
+ artifacts.push(buildScanArtifact(path.dirname(path.resolve(file)), path.basename(file), detected, scanId));
93
150
  }
94
- const status = await pollStatus(scanId, opts.token, opts.url);
95
- printSummary({ scanId, entitiesCount: status.entitiesCount, baseUrl: opts.url, status: status.status });
96
- if (status.status !== 'COMPLETED')
97
- process.exitCode = 1;
151
+ const manifest = {
152
+ schemaVersion: '1.0.0',
153
+ artifactType: 'upload-manifest',
154
+ scanId: expectedScanId ?? artifacts[0].scanId,
155
+ expectedFileCount: artifacts.length,
156
+ files: artifacts.map(({ absolutePath, ...artifact }) => artifact),
157
+ artifacts: artifacts.map(({ absolutePath, ...artifact }) => artifact),
158
+ };
159
+ console.log(`Artefatos preparados: ${artifacts.length}`);
160
+ for (const artifact of artifacts)
161
+ console.log(`✓ ${artifact.filename}`);
162
+ const uploadSpinner = spinner('Enviando artefatos para o Skip Cloud...');
163
+ const result = await uploadArtifactBundle(artifacts, opts.token, opts.url, manifest);
164
+ uploadSpinner.succeed(`Artefatos enviados e confirmados. Scan ID: ${chalk.yellow(result.scanId)}`);
98
165
  }
99
- // ============ MODO SCAN (legado, extração por regex) ============
100
166
  async function scanMode(opts) {
101
167
  console.log(banner(CLI_VERSION));
102
- console.log(chalk.dim(` Modo: scan (legado, regex)`));
103
- const rootDir = path.resolve(opts.dir);
104
- console.log(chalk.dim(` Diretório: ${rootDir}`));
105
- console.log(chalk.dim(` Plataforma: ${opts.url}\n`));
106
- console.log(chalk.yellow(' ⚠ Modo scan usa extração por regex (menos inteligente que o agente).\n'));
168
+ const workspaceRoot = path.resolve(opts.dir);
169
+ const applicationRoot = selectApplicationRoot(workspaceRoot, opts.app);
170
+ const scanId = createScanId();
171
+ const outputDir = path.resolve(opts.outputDir ?? path.join(workspaceRoot, '.skip-output', scanId));
172
+ const startedAt = new Date().toISOString();
107
173
  const ignorePatterns = [...DEFAULT_IGNORE_PATTERNS, ...opts.ignore];
108
- const detectSpinner = spinner('Detectando projeto...');
109
- const signature = detectProject(rootDir);
110
- detectSpinner.succeed(`Framework detectado: ${chalk.cyan(signature.framework)} (${signature.language})`);
111
- const routeSpinner = spinner('Extraindo rotas...');
112
- const routes = await extractRoutes(rootDir, signature.srcDir, ignorePatterns);
113
- routeSpinner.succeed(`${routes.length} rotas encontradas`);
114
- const compSpinner = spinner('Extraindo componentes e eventos...');
115
- const { components, events } = await extractComponents(rootDir, opts.ignore);
116
- compSpinner.succeed(`${components.length} componentes, ${events.length} eventos`);
117
- const apiSpinner = spinner('Extraindo chamadas de API...');
118
- const apiCalls = await extractApiCalls(rootDir, opts.ignore);
119
- apiSpinner.succeed(`${apiCalls.length} chamadas de API detectadas`);
120
- const navSpinner = spinner('Construindo mapa de navegação...');
121
- const navigationMap = await extractNavigation(rootDir, opts.ignore, {
174
+ console.log(chalk.dim(' Modo: scan'));
175
+ console.log(chalk.dim(` Diretorio: ${workspaceRoot}`));
176
+ console.log(chalk.dim(` Aplicacao: ${applicationRoot}`));
177
+ console.log(chalk.dim(` Output: ${outputDir}`));
178
+ console.log(chalk.dim(` Plataforma: ${opts.url}`));
179
+ console.log(chalk.dim(` Scan ID: ${scanId}\n`));
180
+ console.log('[1/10] Detectando projeto');
181
+ fs.mkdirSync(outputDir, { recursive: true });
182
+ appendLog(outputDir, `[SAM] Scanner version: ${CLI_VERSION}`);
183
+ appendLog(outputDir, `[SAM] Project root: ${workspaceRoot}`);
184
+ appendLog(outputDir, `[SAM] Workspace root: ${workspaceRoot}`);
185
+ appendLog(outputDir, `[SAM] Application roots: ${discoverApplicationRoots(workspaceRoot).map((root) => safeRelative(workspaceRoot, root)).join(', ')}`);
186
+ const signature = detectProject(applicationRoot);
187
+ const projectName = path.basename(applicationRoot);
188
+ console.log('[2/10] Descobrindo arquivos');
189
+ const fileManifest = await createFileManifest(applicationRoot, ignorePatterns);
190
+ appendLog(outputDir, `[SAM] Files discovered: ${fileManifest.files.length}`);
191
+ appendLog(outputDir, `[SAM] Source files included: ${fileManifest.files.filter((file) => /\.(tsx?|jsx?|html|vue|svelte|astro)$/.test(file.path)).length}`);
192
+ const fileManifestPayload = { schemaVersion: '1.0.0', artifactType: 'file-manifest', scanId, files: fileManifest.files, sha256: fileManifest.sha256 };
193
+ writeJson(path.join(outputDir, 'scan-file-manifest.json'), fileManifestPayload);
194
+ console.log('[3/10] Detectando rotas e telas');
195
+ const routes = await extractRoutes(applicationRoot, signature.srcDir, ignorePatterns);
196
+ appendLog(outputDir, `[SAM] Route files: ${routes.map((route) => route.physicalPath).join(', ')}`);
197
+ appendLog(outputDir, `[SAM] Routes detected: ${routes.length}`);
198
+ const navigationMap = await extractNavigation(applicationRoot, opts.ignore, {
122
199
  pagesRoutes: routes.map((r) => ({ physicalPath: r.physicalPath, logicalRoute: r.logicalRoute })),
123
200
  srcDir: signature.srcDir,
124
201
  });
125
- const totalActions = navigationMap.screens.reduce((acc, s) => acc + s.actions.length, 0);
126
- navSpinner.succeed(`${navigationMap.screens.length} telas mapeadas, ${totalActions} ações, ${navigationMap.navigationGraph.length} arestas de navegação`);
202
+ appendLog(outputDir, `[SAM] Screens detected: ${navigationMap.screens.length}`);
203
+ console.log('[4/10] Analisando componentes');
204
+ const { components, events } = await extractComponents(applicationRoot, opts.ignore);
205
+ const apiCalls = await extractApiCalls(applicationRoot, opts.ignore);
206
+ appendLog(outputDir, `[SAM] Components detected: ${components.length}`);
207
+ appendLog(outputDir, `[SAM] Interactive elements detected: ${navigationMap.screens.reduce((acc, screen) => acc + screen.actions.length, 0)}`);
208
+ appendLog(outputDir, `[SAM] Candidate entities: ${routes.length + navigationMap.screens.length + components.length + apiCalls.length}`);
127
209
  const briefing = buildBriefing(navigationMap);
128
210
  const widgetSnippet = buildWidgetSnippet();
129
- // ---- Auditoria WCAG ----
130
- // Lê os arquivos das telas LOCALMENTE (não envia o conteúdo ao servidor).
131
- const auditSpinner = spinner('Auditando WCAG...');
211
+ const guidedFlows = detectGuidedFlows(navigationMap);
212
+ console.log('[5/10] Gerando `.skip-sam.json`');
213
+ let sam = buildSemanticApplicationMap({
214
+ scanId,
215
+ startedAt,
216
+ rootDir: applicationRoot,
217
+ workspaceRoot,
218
+ applicationRoot,
219
+ signature,
220
+ routes,
221
+ components,
222
+ apiCalls,
223
+ navigationMap,
224
+ guidedFlows,
225
+ fileManifest: fileManifest.files,
226
+ sourceManifestSha256: fileManifest.sha256,
227
+ projectName,
228
+ });
229
+ sam = normalizeSemanticApplicationMap(sam);
230
+ appendLog(outputDir, `[SAM] Normalized entities: ${sam.entities.length}`);
231
+ appendLog(outputDir, `[SAM] Discarded entities: 0`);
232
+ appendLog(outputDir, `[SAM] Generated IDs: ${sam.entities.filter((entity) => entity.id).length}`);
233
+ appendLog(outputDir, `[SAM] Entities sent to validator: ${sam.entities.length}`);
234
+ appendLog(outputDir, `[SAM] Relationships sent to validator: ${sam.relationships.length}`);
235
+ writeJson(path.join(outputDir, '.skip-sam.invalid.json'), sam);
236
+ const validatorInputSummary = {
237
+ keys: Object.keys(sam),
238
+ artifactType: sam.artifactType,
239
+ scanId: sam.scanId,
240
+ entitiesType: Array.isArray(sam.entities) ? 'array' : typeof sam.entities,
241
+ entitiesCount: sam.entities.length,
242
+ relationshipsType: Array.isArray(sam.relationships) ? 'array' : typeof sam.relationships,
243
+ relationshipsCount: sam.relationships.length,
244
+ };
245
+ appendLog(outputDir, `[SAM] Validator input: ${JSON.stringify(validatorInputSummary)}`);
246
+ console.log('[6/10] Validando `.skip-sam.json`');
247
+ const samValidation = validateSamArtifact(sam, {
248
+ currentScanId: scanId,
249
+ scanStartedAt: startedAt,
250
+ fileManifest: fileManifest.files,
251
+ sourceManifestSha256: fileManifest.sha256,
252
+ expectedFilename: '.skip-sam.json',
253
+ });
254
+ sam.validation = { valid: samValidation.valid, errors: samValidation.errors, warnings: samValidation.warnings };
255
+ if (!samValidation.valid) {
256
+ sam.status = sam.status === 'complete' ? 'partial' : sam.status;
257
+ writeJson(path.join(outputDir, 'scanner-diagnostics.json'), {
258
+ schemaVersion: '1.0.0',
259
+ artifactType: 'diagnostics',
260
+ scanId,
261
+ scannerVersion: CLI_VERSION,
262
+ projectRoot: workspaceRoot,
263
+ workspaceRoot,
264
+ applicationRoots: discoverApplicationRoots(workspaceRoot).map((root) => safeRelative(workspaceRoot, root)),
265
+ applicationRoot,
266
+ filesDiscovered: fileManifest.files.length,
267
+ sourceFilesIncluded: fileManifest.files.filter((file) => /\.(tsx?|jsx?|html|vue|svelte|astro)$/.test(file.path)).length,
268
+ routeFilesDetected: routes.map((route) => route.physicalPath),
269
+ routesDetected: routes.length,
270
+ screensDetected: navigationMap.screens.length,
271
+ componentsDetected: components.length,
272
+ interactiveElementsDetected: navigationMap.screens.reduce((acc, screen) => acc + screen.actions.length, 0),
273
+ apisDetected: apiCalls.length,
274
+ flowsDetected: guidedFlows.length,
275
+ candidateEntities: routes.length + navigationMap.screens.length + components.length + apiCalls.length,
276
+ normalizedEntities: sam.entities.length,
277
+ discardedEntities: 0,
278
+ generatedEntityIds: sam.entities.filter((entity) => entity.id).length,
279
+ entitiesSentToValidator: sam.entities.length,
280
+ relationshipsSentToValidator: sam.relationships.length,
281
+ validatorInputSummary,
282
+ validationErrors: samValidation.errors,
283
+ });
284
+ writeJson(path.join(outputDir, 'scan-validation.json'), {
285
+ schemaVersion: '1.0.0',
286
+ artifactType: 'validation',
287
+ scanId,
288
+ valid: false,
289
+ generatedAt: new Date().toISOString(),
290
+ errors: samValidation.errors,
291
+ });
292
+ if (opts.validateSam)
293
+ throw new Error(`Falha ao validar .skip-sam.json:\n${samValidation.errors.join('\n')}`);
294
+ }
295
+ const samPath = writeSkipSamFile(outputDir, sam);
296
+ fs.rmSync(path.join(outputDir, '.skip-sam.invalid.json'), { force: true });
297
+ console.log('[7/10] Gerando auditoria WCAG');
132
298
  const contentsByFile = {};
133
299
  for (const s of navigationMap.screens) {
134
- const abs = path.join(rootDir, s.filePath);
135
300
  try {
136
- contentsByFile[s.filePath] = fs.readFileSync(abs, 'utf-8');
301
+ contentsByFile[s.filePath] = fs.readFileSync(path.join(applicationRoot, s.filePath), 'utf-8');
137
302
  }
138
303
  catch {
139
304
  contentsByFile[s.filePath] = '';
@@ -151,10 +316,6 @@ async function scanMode(opts) {
151
316
  auditedAt: new Date().toISOString(),
152
317
  rulesVersion: WCAG_RULES_VERSION,
153
318
  };
154
- auditSpinner.succeed(`WCAG: score ${colorScore(score)} (${level}), ${total} violação(ões) únicas, ${checks.total} checks [${summary.critical}c/${summary.serious}s/${summary.moderate}m/${summary.minor}i/${summary.unknown}u]`);
155
- // ---- Fluxos guiados ----
156
- const guidedFlows = detectGuidedFlows(navigationMap);
157
- // ---- Histórico (local, enviado junto pra o dashboard) ----
158
319
  const historyEntry = {
159
320
  scannedAt: wcag.auditedAt,
160
321
  score,
@@ -165,59 +326,194 @@ async function scanMode(opts) {
165
326
  minor: summary.minor,
166
327
  unknown: summary.unknown,
167
328
  };
168
- const history = appendHistory(rootDir, historyEntry);
329
+ const history = appendHistory(applicationRoot, historyEntry);
169
330
  const uniqueFiles = new Set([
170
331
  ...routes.map((r) => r.physicalPath),
171
332
  ...components.map((c) => c.filepath),
172
333
  ...apiCalls.map((a) => a.filepath),
173
334
  ...navigationMap.screens.map((s) => s.filePath),
174
335
  ]);
175
- const projectName = path.basename(rootDir);
176
336
  const rawReport = buildReport({
177
- signature, routes, components, events, apiCalls,
178
- navigationMap, widgetSnippet, briefing, wcag, guidedFlows, history,
179
- filesScanned: uniqueFiles.size, projectName,
337
+ signature,
338
+ routes,
339
+ components,
340
+ events,
341
+ apiCalls,
342
+ navigationMap,
343
+ widgetSnippet,
344
+ briefing,
345
+ wcag,
346
+ guidedFlows,
347
+ history,
348
+ filesScanned: uniqueFiles.size,
349
+ projectName,
180
350
  });
351
+ rawReport.scanId = scanId;
352
+ rawReport.semanticMapSummary = {
353
+ status: sam.status,
354
+ entities: sam.entities.length,
355
+ relationships: sam.relationships.length,
356
+ routes: sam.stats.routes,
357
+ components: sam.stats.components,
358
+ coverage: sam.coverage.percentage,
359
+ };
360
+ rawReport.artifactType = 'report';
361
+ rawReport.schemaVersion = '1.0.0';
362
+ rawReport.generatedAt = new Date().toISOString();
363
+ rawReport.framework = signature.framework;
364
+ rawReport.language = signature.language;
365
+ rawReport.wcagSummary = summary;
366
+ const wcagPayload = {
367
+ schemaVersion: '1.0.0',
368
+ artifactType: 'wcag-audit',
369
+ scanId,
370
+ generatedAt: wcag.auditedAt,
371
+ status: 'complete',
372
+ findings: violations,
373
+ checks: { total: checks.total, passed: checks.passed, failed: checks.failed, incomplete: checks.needsReview, inapplicable: 0, needsReview: checks.needsReview },
374
+ summary,
375
+ violations,
376
+ score,
377
+ level,
378
+ rulesVersion: WCAG_RULES_VERSION,
379
+ };
380
+ writeJson(path.join(outputDir, '.skip-wcag-audit.json'), wcagPayload);
381
+ const samArtifact = buildScanArtifact(outputDir, '.skip-sam.json', 'semantic-map', scanId);
382
+ rawReport.relatedArtifacts = [
383
+ {
384
+ artifactType: 'semantic-map',
385
+ filename: '.skip-sam.json',
386
+ path: path.relative(outputDir, samPath) || '.skip-sam.json',
387
+ required: true,
388
+ scanId,
389
+ sizeBytes: samArtifact.sizeBytes,
390
+ sha256: samArtifact.sha256,
391
+ },
392
+ ];
181
393
  const { report, secretsFound } = maskReportSecrets(rawReport);
182
- if (secretsFound > 0) {
183
- console.log(chalk.yellow(`⚠ ${secretsFound} possível(eis) secret(s) mascarado(s) antes do envio.`));
394
+ if (secretsFound > 0)
395
+ console.log(chalk.yellow(`${secretsFound} possível(eis) secret(s) mascarado(s) antes do envio.`));
396
+ writeJson(path.join(outputDir, '.skip-report.json'), report);
397
+ writeJson(path.join(outputDir, 'scanner-diagnostics.json'), {
398
+ schemaVersion: '1.0.0',
399
+ artifactType: 'diagnostics',
400
+ scanId,
401
+ scannerVersion: CLI_VERSION,
402
+ projectRoot: workspaceRoot,
403
+ workspaceRoot,
404
+ applicationRoots: discoverApplicationRoots(workspaceRoot).map((root) => safeRelative(workspaceRoot, root)),
405
+ applicationRoot,
406
+ filesDiscovered: fileManifest.files.length,
407
+ sourceFilesIncluded: fileManifest.files.filter((file) => /\.(tsx?|jsx?|html|vue|svelte|astro)$/.test(file.path)).length,
408
+ routeFilesDetected: routes.map((route) => route.physicalPath),
409
+ routesDetected: routes.length,
410
+ screensDetected: navigationMap.screens.length,
411
+ componentsDetected: components.length,
412
+ interactiveElementsDetected: navigationMap.screens.reduce((acc, screen) => acc + screen.actions.length, 0),
413
+ apisDetected: apiCalls.length,
414
+ flowsDetected: guidedFlows.length,
415
+ candidateEntities: routes.length + navigationMap.screens.length + components.length + apiCalls.length,
416
+ normalizedEntities: sam.entities.length,
417
+ discardedEntities: 0,
418
+ generatedEntityIds: sam.entities.filter((entity) => entity.id).length,
419
+ entitiesSentToValidator: sam.entities.length,
420
+ relationshipsSentToValidator: sam.relationships.length,
421
+ validatorInputSummary,
422
+ });
423
+ console.log('[8/10] Validando artefatos');
424
+ const firstArtifacts = [
425
+ buildScanArtifact(outputDir, '.skip-report.json', 'report', scanId),
426
+ samArtifact,
427
+ buildScanArtifact(outputDir, '.skip-wcag-audit.json', 'wcag-audit', scanId),
428
+ buildScanArtifact(outputDir, 'scan-file-manifest.json', 'file-manifest', scanId),
429
+ ];
430
+ const validationPayload = {
431
+ schemaVersion: '1.0.0',
432
+ artifactType: 'validation',
433
+ scanId,
434
+ valid: true,
435
+ generatedAt: new Date().toISOString(),
436
+ artifacts: firstArtifacts.map((artifact) => ({
437
+ filename: artifact.filename,
438
+ artifactType: artifact.artifactType,
439
+ valid: true,
440
+ sha256: artifact.sha256,
441
+ })),
442
+ };
443
+ writeJson(path.join(outputDir, 'scan-validation.json'), validationPayload);
444
+ const validationArtifact = buildScanArtifact(outputDir, 'scan-validation.json', 'validation', scanId);
445
+ const provisionalArtifacts = [...firstArtifacts, validationArtifact];
446
+ const uploadManifestPayload = {
447
+ schemaVersion: '1.0.0',
448
+ artifactType: 'upload-manifest',
449
+ scanId,
450
+ createdAt: new Date().toISOString(),
451
+ expectedFileCount: REQUIRED_ARTIFACTS.length,
452
+ files: provisionalArtifacts.map(({ absolutePath, ...artifact }) => artifact),
453
+ artifacts: provisionalArtifacts.map(({ absolutePath, ...artifact }) => artifact),
454
+ };
455
+ writeJson(path.join(outputDir, 'upload-manifest.json'), uploadManifestPayload);
456
+ let uploadArtifact = buildScanArtifact(outputDir, 'upload-manifest.json', 'upload-manifest', scanId);
457
+ let artifacts = [...firstArtifacts, uploadArtifact, validationArtifact];
458
+ const finalUploadManifestPayload = {
459
+ ...uploadManifestPayload,
460
+ files: artifacts.map(({ absolutePath, ...artifact }) => artifact),
461
+ artifacts: artifacts.map(({ absolutePath, ...artifact }) => artifact),
462
+ };
463
+ writeJson(path.join(outputDir, 'upload-manifest.json'), finalUploadManifestPayload);
464
+ uploadArtifact = buildScanArtifact(outputDir, 'upload-manifest.json', 'upload-manifest', scanId);
465
+ artifacts = [...firstArtifacts, uploadArtifact, validationArtifact];
466
+ for (const artifact of artifacts) {
467
+ const validation = validateArtifactByType(artifact.absolutePath, artifact.artifactType, {
468
+ currentScanId: scanId,
469
+ scanStartedAt: startedAt,
470
+ fileManifest: fileManifest.files,
471
+ sourceManifestSha256: fileManifest.sha256,
472
+ expectedFilename: '.skip-sam.json',
473
+ });
474
+ if (!validation.valid)
475
+ throw new Error(`Artefato inválido (${artifact.filename}):\n${validation.errors.join('\n')}`);
184
476
  }
185
- if (opts.verbose) {
186
- console.log('\n' + chalk.dim('Briefing:'));
187
- console.log(chalk.dim(briefing));
188
- console.log('\n' + chalk.dim('Top violações WCAG:'));
189
- for (const v of violations.slice(0, 8)) {
190
- console.log(chalk.dim(` [${v.severity}] ${v.title} — ${v.screen} (${v.selector})`));
191
- }
477
+ let uploaded = false;
478
+ let confirmed = false;
479
+ console.log('[9/10] Enviando artefatos');
480
+ if (opts.send) {
481
+ const bundleManifestPayload = {
482
+ ...finalUploadManifestPayload,
483
+ artifacts: artifacts.map(({ absolutePath, ...artifact }) => artifact),
484
+ files: artifacts.map(({ absolutePath, ...artifact }) => artifact),
485
+ };
486
+ const result = await uploadArtifactBundle(artifacts, opts.token, opts.url, bundleManifestPayload);
487
+ uploaded = true;
488
+ confirmed = result.confirmations.some((item) => item.filename === '.skip-sam.json' && item.persisted === true);
192
489
  }
193
- if (opts.dryRun) {
194
- console.log(chalk.bold.cyan('\n🏃 Modo dry-run: pulando envio.\n'));
195
- console.log(` Telas mapeadas: ${chalk.bold(navigationMap.screens.length)}`);
196
- console.log(` Ações: ${chalk.bold(totalActions)}`);
197
- console.log(` Arestas: ${chalk.bold(navigationMap.navigationGraph.length)}`);
198
- console.log(` Fluxos guiados: ${chalk.bold(guidedFlows.length)}`);
199
- console.log(` Score WCAG: ${colorScore(score)} (${level})`);
200
- console.log(` Violações WCAG: ${chalk.bold(String(total))} [${summary.critical} critical, ${summary.serious} serious, ${summary.moderate} moderate, ${summary.minor} minor]`);
201
- console.log(` Histórico: ${chalk.bold(String(history.length))} scan(s)\n`);
202
- return;
490
+ else {
491
+ console.log(chalk.dim(' --send não informado: upload pulado.'));
203
492
  }
204
- const uploadSpinner = spinner('Enviando para o Skip Cloud...');
205
- const scanId = await uploadReport(report, opts.token, opts.url);
206
- uploadSpinner.succeed(`Scan enviado! ID: ${chalk.yellow(scanId)}`);
207
- const status = await pollStatus(scanId, opts.token, opts.url);
208
- printSummary({ scanId, entitiesCount: status.entitiesCount, baseUrl: opts.url, status: status.status });
209
- if (status.status !== 'COMPLETED')
210
- process.exitCode = 1;
493
+ console.log('[10/10] Confirmando a API');
494
+ if (opts.send && !confirmed)
495
+ throw new Error('Upload incompleto:\n`.skip-sam.json` não foi confirmado pela API.');
496
+ console.log('\nSAM');
497
+ console.log(`- Arquivo: .skip-sam.json`);
498
+ console.log(`- Caminho: ${samPath}`);
499
+ console.log(`- Criado no scan atual: sim`);
500
+ console.log(`- Scan ID: ${scanId}`);
501
+ console.log(`- Gerado em: ${sam.generatedAt}`);
502
+ console.log(`- Entidades: ${sam.entities.length}`);
503
+ console.log(`- Relacionamentos: ${sam.relationships.length}`);
504
+ console.log(`- Status: ${sam.status}`);
505
+ console.log(`- Validação: ${sam.validation.valid ? 'aprovada' : 'reprovada'}`);
506
+ console.log(`- SHA-256: ${samArtifact.sha256}`);
507
+ console.log(`- Enviado: ${uploaded ? 'sim' : 'não'}`);
508
+ console.log(`- Confirmado pela API: ${confirmed ? 'sim' : 'não'}`);
509
+ console.log(`- Score WCAG: ${colorScore(score)} (${level})`);
211
510
  }
212
- // ============ Entry point ============
213
511
  async function main() {
214
512
  const { command, opts } = parseCli();
215
- if (command === 'send') {
513
+ if (command === 'send')
216
514
  await sendMode(opts);
217
- }
218
- else {
515
+ else
219
516
  await scanMode(opts);
220
- }
221
517
  }
222
518
  main().catch((err) => {
223
519
  const msg = err instanceof Error ? err.message : String(err);