@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/artifacts.d.ts +41 -0
- package/dist/artifacts.d.ts.map +1 -0
- package/dist/artifacts.js +116 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/cli.d.ts +12 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +40 -13
- package/dist/cli.js.map +1 -1
- package/dist/index.js +432 -136
- package/dist/index.js.map +1 -1
- package/dist/reporter.d.ts +25 -1
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +1 -1
- package/dist/reporter.js.map +1 -1
- package/dist/sam.d.ts +137 -0
- package/dist/sam.d.ts.map +1 -0
- package/dist/sam.js +449 -0
- package/dist/sam.js.map +1 -0
- package/dist/scanner/navigation.d.ts.map +1 -1
- package/dist/scanner/navigation.js +24 -8
- package/dist/scanner/navigation.js.map +1 -1
- package/dist/scanner/routes.d.ts.map +1 -1
- package/dist/scanner/routes.js +33 -2
- package/dist/scanner/routes.js.map +1 -1
- package/dist/uploader.d.ts +28 -0
- package/dist/uploader.d.ts.map +1 -1
- package/dist/uploader.js +84 -0
- package/dist/uploader.js.map +1 -1
- package/package.json +8 -2
- package/scripts/validate-artifact.js +66 -0
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 {
|
|
7
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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(
|
|
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(`
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
const
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
178
|
-
|
|
179
|
-
|
|
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(
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
194
|
-
console.log(chalk.
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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);
|