@titannio/webtoolkit-cli 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +639 -0
  3. package/dist/bin.d.ts +2 -0
  4. package/dist/bin.js +268 -0
  5. package/dist/bundle-audit.d.ts +7 -0
  6. package/dist/bundle-audit.js +111 -0
  7. package/dist/cleaner.d.ts +15 -0
  8. package/dist/cleaner.js +359 -0
  9. package/dist/config-reference.d.ts +8 -0
  10. package/dist/config-reference.js +805 -0
  11. package/dist/config.d.ts +306 -0
  12. package/dist/config.js +173 -0
  13. package/dist/dev-grid.d.ts +7 -0
  14. package/dist/dev-grid.js +181 -0
  15. package/dist/dev-watch.d.ts +13 -0
  16. package/dist/dev-watch.js +184 -0
  17. package/dist/environment.d.ts +10 -0
  18. package/dist/environment.js +172 -0
  19. package/dist/guard-registry.d.ts +1 -0
  20. package/dist/guard-registry.js +17 -0
  21. package/dist/guard-runner.d.ts +4 -0
  22. package/dist/guard-runner.js +36 -0
  23. package/dist/guards/any-guard.d.ts +16 -0
  24. package/dist/guards/any-guard.js +121 -0
  25. package/dist/guards/assert-no-tests-in-dist.d.ts +1 -0
  26. package/dist/guards/assert-no-tests-in-dist.js +56 -0
  27. package/dist/guards/check-mojibake.d.ts +52 -0
  28. package/dist/guards/check-mojibake.js +378 -0
  29. package/dist/guards/code-pattern-guard.d.ts +71 -0
  30. package/dist/guards/code-pattern-guard.js +654 -0
  31. package/dist/guards/dal-service-repository-check.d.ts +13 -0
  32. package/dist/guards/dal-service-repository-check.js +264 -0
  33. package/dist/guards/dependency-cruiser-guard.d.ts +14 -0
  34. package/dist/guards/dependency-cruiser-guard.js +69 -0
  35. package/dist/guards/documentation-guard.d.ts +3 -0
  36. package/dist/guards/documentation-guard.js +370 -0
  37. package/dist/guards/guard-config.d.ts +19 -0
  38. package/dist/guards/guard-config.js +87 -0
  39. package/dist/guards/internal-link-guard.d.ts +12 -0
  40. package/dist/guards/internal-link-guard.js +272 -0
  41. package/dist/guards/package-surface-guard.d.ts +37 -0
  42. package/dist/guards/package-surface-guard.js +234 -0
  43. package/dist/guards/pnpm-workspace-config.d.ts +2 -0
  44. package/dist/guards/pnpm-workspace-config.js +40 -0
  45. package/dist/guards/rebuild-preflight.d.ts +29 -0
  46. package/dist/guards/rebuild-preflight.js +137 -0
  47. package/dist/guards/repository-hygiene-guard.d.ts +12 -0
  48. package/dist/guards/repository-hygiene-guard.js +70 -0
  49. package/dist/guards/schema-guard.d.ts +10 -0
  50. package/dist/guards/schema-guard.js +160 -0
  51. package/dist/guards/singleton-deps-guard.d.ts +21 -0
  52. package/dist/guards/singleton-deps-guard.js +183 -0
  53. package/dist/guards/tsconfig-guard.d.ts +5 -0
  54. package/dist/guards/tsconfig-guard.js +105 -0
  55. package/dist/guards/workspace-manifest-guard.d.ts +21 -0
  56. package/dist/guards/workspace-manifest-guard.js +210 -0
  57. package/dist/jsdoc-report.d.ts +7 -0
  58. package/dist/jsdoc-report.js +456 -0
  59. package/dist/process.d.ts +20 -0
  60. package/dist/process.js +86 -0
  61. package/dist/ready-service.d.ts +7 -0
  62. package/dist/ready-service.js +135 -0
  63. package/dist/release-gate.d.ts +7 -0
  64. package/dist/release-gate.js +35 -0
  65. package/dist/repo-check.d.ts +7 -0
  66. package/dist/repo-check.js +121 -0
  67. package/dist/tasks.d.ts +17 -0
  68. package/dist/tasks.js +195 -0
  69. package/dist/upgrade.d.ts +10 -0
  70. package/dist/upgrade.js +674 -0
  71. package/dist/validate.d.ts +7 -0
  72. package/dist/validate.js +51 -0
  73. package/dist/workspace-tests.d.ts +33 -0
  74. package/dist/workspace-tests.js +529 -0
  75. package/package.json +57 -0
@@ -0,0 +1,52 @@
1
+ type Finding = {
2
+ file: string;
3
+ absoluteFile: string;
4
+ lineNumber: number;
5
+ lineText: string;
6
+ reason: string;
7
+ snippets: string[];
8
+ affectedWord: string;
9
+ wordStart: number;
10
+ wordEnd: number;
11
+ absoluteWordStart: number;
12
+ absoluteWordEnd: number;
13
+ replacementWord: string | null;
14
+ autoFixStatus: 'fixable' | 'manual';
15
+ skipReason: string | null;
16
+ };
17
+ type FileFixPlan = {
18
+ absoluteFile: string;
19
+ relativeFile: string;
20
+ originalContent: string;
21
+ updatedContent: string;
22
+ appliedFixes: Finding[];
23
+ skippedFindings: Finding[];
24
+ };
25
+ type CliOptions = {
26
+ fix: boolean;
27
+ dryRun: boolean;
28
+ };
29
+ type BackupCleanupResult = {
30
+ deletedBackups: string[];
31
+ keptBackups: string[];
32
+ backupRoot: string;
33
+ };
34
+ export declare function parseCliOptions(argv: string[]): CliOptions;
35
+ export declare function shouldScanFile(filePath: string): boolean;
36
+ export declare function extractAffectedWordInfo(line: string, start: number, length: number): {
37
+ word: string;
38
+ wordStart: number;
39
+ wordEnd: number;
40
+ };
41
+ export declare function buildLinePreview(line: string): string;
42
+ export declare function resolveReplacementWord(word: string): {
43
+ replacementWord: string | null;
44
+ skipReason: string | null;
45
+ };
46
+ export declare function collectFindings(file: string, content: string, rootDir?: string): Finding[];
47
+ export declare function applyWordReplacements(content: string, fixes: Finding[]): string;
48
+ export declare function buildFileFixPlan(filePath: string, content: string, findings: Finding[], rootDir?: string): FileFixPlan | null;
49
+ export declare function applyFixPlans(plans: FileFixPlan[], backupRoot: string): void;
50
+ export declare function verifyAndCleanupBackups(plans: FileFixPlan[], backupRoot: string): BackupCleanupResult;
51
+ export declare function runMojibakeGuard(argv?: string[], rootDir?: string): number;
52
+ export {};
@@ -0,0 +1,378 @@
1
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { dirname, join, relative } from 'node:path';
4
+ import { isMainModule } from './guard-config.js';
5
+ const ALLOWED_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.json', '.md', '.css', '.scss', '.html', '.yml', '.yaml']);
6
+ const SKIP_DIRS = new Set(['node_modules', '.git', '.turbo', 'dist', 'build', 'coverage', '.next']);
7
+ const MAX_FINDINGS = 200;
8
+ const LINE_PREVIEW_LIMIT = 220;
9
+ const AUTO_FIXABLE_PREFIX_PATTERN = /[\u00C2\u00C3][\u0080-\u00BF]/;
10
+ const DOUBLE_ENCODED_PATTERN = /\u00C3\u0192\u00C2/;
11
+ const REPLACEMENT_CHAR_PATTERN = /\uFFFD/;
12
+ const BACKUP_ROOT_PREFIX = 'webtoolkit-mojibake-backup-';
13
+ const colors = {
14
+ reset: '\x1b[0m',
15
+ bright: '\x1b[1m',
16
+ green: '\x1b[32m',
17
+ yellow: '\x1b[33m',
18
+ blue: '\x1b[34m',
19
+ cyan: '\x1b[36m',
20
+ red: '\x1b[31m',
21
+ };
22
+ const suspiciousPatterns = [
23
+ { regex: /\u00C3\u0192\u00C2/g, reason: 'Dupla corrupção de encoding detectada' },
24
+ { regex: /\u00C3[\u0080-\u00BF]/g, reason: 'UTF-8 texto lido como Latin-1 (prefixo U+00C3)' },
25
+ { regex: /\u00C2[\u0080-\u00BF]/g, reason: 'UTF-8 texto lido como Latin-1 (prefixo U+00C2)' },
26
+ { regex: /\uFFFD/g, reason: 'Caractere de substituição encontrado' },
27
+ ];
28
+ export function parseCliOptions(argv) {
29
+ const args = new Set(argv);
30
+ return {
31
+ fix: args.has('--fix'),
32
+ dryRun: args.has('--dry-run'),
33
+ };
34
+ }
35
+ export function shouldScanFile(filePath) {
36
+ if (filePath.endsWith('check-mojibake.ts'))
37
+ return false;
38
+ const lower = filePath.toLowerCase();
39
+ for (const ext of ALLOWED_EXTENSIONS) {
40
+ if (lower.endsWith(ext))
41
+ return true;
42
+ }
43
+ return false;
44
+ }
45
+ function walk(dir, files) {
46
+ for (const entry of readdirSync(dir)) {
47
+ const fullPath = join(dir, entry);
48
+ const st = statSync(fullPath);
49
+ if (st.isDirectory()) {
50
+ if (!SKIP_DIRS.has(entry))
51
+ walk(fullPath, files);
52
+ continue;
53
+ }
54
+ if (st.isFile() && shouldScanFile(fullPath))
55
+ files.push(fullPath);
56
+ }
57
+ }
58
+ function isWordBoundary(char) {
59
+ /* v8 ignore next -- callers stay within line bounds */
60
+ if (!char)
61
+ return true;
62
+ return /[\s"'`.,;:!?()[\]{}<>/=+\\|]/.test(char);
63
+ }
64
+ export function extractAffectedWordInfo(line, start, length) {
65
+ let wordStart = start;
66
+ let wordEnd = start + length;
67
+ while (wordStart > 0 && !isWordBoundary(line[wordStart - 1])) {
68
+ wordStart -= 1;
69
+ }
70
+ while (wordEnd < line.length && !isWordBoundary(line[wordEnd])) {
71
+ wordEnd += 1;
72
+ }
73
+ const word = line.slice(wordStart, wordEnd).trim();
74
+ /* v8 ignore next -- suspicious encoding markers are non-whitespace */
75
+ if (word.length === 0) {
76
+ return {
77
+ word: line.slice(start, start + length),
78
+ wordStart: start,
79
+ wordEnd: start + length,
80
+ };
81
+ }
82
+ return { word, wordStart, wordEnd };
83
+ }
84
+ export function buildLinePreview(line) {
85
+ const normalized = line.replace(/\t/g, ' ').trimEnd();
86
+ if (normalized.length <= LINE_PREVIEW_LIMIT)
87
+ return normalized;
88
+ return `${normalized.slice(0, LINE_PREVIEW_LIMIT - 3)}...`;
89
+ }
90
+ function containsSuspiciousContent(value) {
91
+ return suspiciousPatterns.some(({ regex }) => {
92
+ regex.lastIndex = 0;
93
+ return regex.test(value);
94
+ });
95
+ }
96
+ function containsUnsafeControlChars(value) {
97
+ return /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/.test(value);
98
+ }
99
+ export function resolveReplacementWord(word) {
100
+ if (REPLACEMENT_CHAR_PATTERN.test(word)) {
101
+ return { replacementWord: null, skipReason: 'contem caractere de substituicao; auto-fix bloqueado' };
102
+ }
103
+ if (DOUBLE_ENCODED_PATTERN.test(word)) {
104
+ return { replacementWord: null, skipReason: 'contem dupla corrupcao; auto-fix de uma passada bloqueado' };
105
+ }
106
+ if (!AUTO_FIXABLE_PREFIX_PATTERN.test(word)) {
107
+ return { replacementWord: null, skipReason: 'fora do padrao conservador de palavra inteira' };
108
+ }
109
+ const replacementWord = Buffer.from(word, 'latin1').toString('utf8');
110
+ /* v8 ignore next -- a matched encoding prefix always converts to a different non-empty word */
111
+ if (replacementWord.length === 0 || replacementWord === word) {
112
+ return { replacementWord: null, skipReason: 'a conversao nao produziu uma palavra diferente' };
113
+ }
114
+ if (containsUnsafeControlChars(replacementWord)) {
115
+ return { replacementWord: null, skipReason: 'a correção gerou caracteres de controle' };
116
+ }
117
+ if (containsSuspiciousContent(replacementWord)) {
118
+ return { replacementWord: null, skipReason: 'a palavra corrigida ainda contem marcadores suspeitos' };
119
+ }
120
+ const roundTrip = Buffer.from(replacementWord, 'utf8').toString('latin1');
121
+ /* v8 ignore next -- valid latin1-to-UTF-8 candidates round-trip by construction */
122
+ if (roundTrip !== word) {
123
+ return { replacementWord: null, skipReason: 'a correção falhou na validação reversivel utf8->latin1' };
124
+ }
125
+ /* v8 ignore next -- affected words cannot cross the line boundaries used by the scanner */
126
+ if (replacementWord.includes('\r') || replacementWord.includes('\n')) {
127
+ return { replacementWord: null, skipReason: 'a correção alteraria a estrutura de linhas' };
128
+ }
129
+ return { replacementWord, skipReason: null };
130
+ }
131
+ export function collectFindings(file, content, rootDir = process.cwd()) {
132
+ const fileFindings = [];
133
+ const lines = content.split(/\r?\n/);
134
+ let lineStartOffset = 0;
135
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
136
+ const line = lines[lineIndex];
137
+ const lineFindings = new Map();
138
+ for (const { regex, reason } of suspiciousPatterns) {
139
+ regex.lastIndex = 0;
140
+ for (const match of line.matchAll(regex)) {
141
+ const snippet = match[0];
142
+ const start = match.index;
143
+ const affected = extractAffectedWordInfo(line, start, snippet.length);
144
+ const findingKey = `${lineIndex}:${affected.wordStart}:${affected.wordEnd}:${reason}`;
145
+ const existingFinding = lineFindings.get(findingKey);
146
+ if (existingFinding) {
147
+ if (!existingFinding.snippets.includes(snippet)) {
148
+ existingFinding.snippets.push(snippet);
149
+ }
150
+ continue;
151
+ }
152
+ const replacement = resolveReplacementWord(affected.word);
153
+ lineFindings.set(findingKey, {
154
+ file: relative(rootDir, file),
155
+ absoluteFile: file,
156
+ lineNumber: lineIndex + 1,
157
+ lineText: buildLinePreview(line),
158
+ reason,
159
+ snippets: [snippet],
160
+ affectedWord: affected.word,
161
+ wordStart: affected.wordStart,
162
+ wordEnd: affected.wordEnd,
163
+ absoluteWordStart: lineStartOffset + affected.wordStart,
164
+ absoluteWordEnd: lineStartOffset + affected.wordEnd,
165
+ replacementWord: replacement.replacementWord,
166
+ autoFixStatus: replacement.replacementWord ? 'fixable' : 'manual',
167
+ skipReason: replacement.skipReason,
168
+ });
169
+ if (fileFindings.length + lineFindings.size >= MAX_FINDINGS) {
170
+ fileFindings.push(...lineFindings.values());
171
+ return fileFindings.slice(0, MAX_FINDINGS);
172
+ }
173
+ }
174
+ }
175
+ fileFindings.push(...lineFindings.values());
176
+ const lineEndOffset = lineStartOffset + line.length;
177
+ const nextChars = content.slice(lineEndOffset, lineEndOffset + 2);
178
+ const lineBreakLength = nextChars.startsWith('\r\n') ? 2 : nextChars.startsWith('\n') ? 1 : 0;
179
+ lineStartOffset = lineEndOffset + lineBreakLength;
180
+ }
181
+ return fileFindings;
182
+ }
183
+ export function applyWordReplacements(content, fixes) {
184
+ const fixesDescending = [...fixes].sort((left, right) => right.absoluteWordStart - left.absoluteWordStart);
185
+ let updatedContent = content;
186
+ for (const finding of fixesDescending) {
187
+ if (!finding.replacementWord) {
188
+ throw new Error(`Fix sem replacementWord em ${finding.file}:${finding.lineNumber}`);
189
+ }
190
+ const currentWord = updatedContent.slice(finding.absoluteWordStart, finding.absoluteWordEnd);
191
+ if (currentWord !== finding.affectedWord) {
192
+ throw new Error(`O trecho esperado para correção mudou em ${finding.file}:${finding.lineNumber}`);
193
+ }
194
+ updatedContent = `${updatedContent.slice(0, finding.absoluteWordStart)}${finding.replacementWord}${updatedContent.slice(finding.absoluteWordEnd)}`;
195
+ }
196
+ return updatedContent;
197
+ }
198
+ export function buildFileFixPlan(filePath, content, findings, rootDir = process.cwd()) {
199
+ const fixableFindings = findings
200
+ .filter((finding) => finding.autoFixStatus === 'fixable' && finding.replacementWord !== null)
201
+ .sort((left, right) => left.absoluteWordStart - right.absoluteWordStart);
202
+ if (fixableFindings.length === 0)
203
+ return null;
204
+ const updatedContent = applyWordReplacements(content, fixableFindings);
205
+ const remainingFindings = collectFindings(filePath, updatedContent);
206
+ const unresolvedFixedWords = fixableFindings.filter((finding) => remainingFindings.some((remaining) => remaining.lineNumber === finding.lineNumber &&
207
+ remaining.affectedWord === finding.replacementWord));
208
+ /* v8 ignore next -- conservative replacement candidates are revalidated above */
209
+ if (unresolvedFixedWords.length > 0) {
210
+ return null;
211
+ }
212
+ return {
213
+ absoluteFile: filePath,
214
+ relativeFile: relative(rootDir, filePath),
215
+ originalContent: content,
216
+ updatedContent,
217
+ appliedFixes: fixableFindings,
218
+ skippedFindings: findings.filter((finding) => finding.autoFixStatus === 'manual'),
219
+ };
220
+ }
221
+ function buildFixPlans(files, findingsByFile, rootDir = process.cwd()) {
222
+ const plans = [];
223
+ for (const file of files) {
224
+ const findings = findingsByFile.get(file);
225
+ if (!findings)
226
+ continue;
227
+ const content = readFileSync(file, 'utf8');
228
+ const plan = buildFileFixPlan(file, content, findings, rootDir);
229
+ if (plan) {
230
+ plans.push(plan);
231
+ }
232
+ }
233
+ return plans;
234
+ }
235
+ function createBackupRoot() {
236
+ return mkdtempSync(join(tmpdir(), BACKUP_ROOT_PREFIX));
237
+ }
238
+ function getBackupPath(backupRoot, relativeFile) {
239
+ return join(backupRoot, relativeFile);
240
+ }
241
+ export function applyFixPlans(plans, backupRoot) {
242
+ for (const plan of plans) {
243
+ const backupPath = getBackupPath(backupRoot, plan.relativeFile);
244
+ mkdirSync(dirname(backupPath), { recursive: true });
245
+ if (existsSync(backupPath)) {
246
+ throw new Error(`Backup ja existe para ${plan.relativeFile}: ${backupPath}`);
247
+ }
248
+ writeFileSync(backupPath, plan.originalContent, 'utf8');
249
+ writeFileSync(plan.absoluteFile, plan.updatedContent, 'utf8');
250
+ const rewrittenContent = readFileSync(plan.absoluteFile, 'utf8');
251
+ /* v8 ignore next -- post-write corruption safety net */
252
+ if (rewrittenContent !== plan.updatedContent) {
253
+ writeFileSync(plan.absoluteFile, plan.originalContent, 'utf8');
254
+ throw new Error(`Falha na verificação pos-gravação para ${plan.relativeFile}; arquivo original restaurado`);
255
+ }
256
+ }
257
+ }
258
+ export function verifyAndCleanupBackups(plans, backupRoot) {
259
+ const deletedBackups = [];
260
+ const keptBackups = [];
261
+ for (const plan of plans) {
262
+ const backupPath = getBackupPath(backupRoot, plan.relativeFile);
263
+ if (!existsSync(backupPath)) {
264
+ keptBackups.push(backupPath);
265
+ continue;
266
+ }
267
+ const backupContent = readFileSync(backupPath, 'utf8');
268
+ const actualContent = readFileSync(plan.absoluteFile, 'utf8');
269
+ const expectedContent = applyWordReplacements(backupContent, plan.appliedFixes);
270
+ if (backupContent !== plan.originalContent || actualContent !== expectedContent) {
271
+ keptBackups.push(backupPath);
272
+ continue;
273
+ }
274
+ unlinkSync(backupPath);
275
+ deletedBackups.push(backupPath);
276
+ }
277
+ if (keptBackups.length === 0) {
278
+ rmSync(backupRoot, { recursive: true, force: true });
279
+ }
280
+ return { deletedBackups, keptBackups, backupRoot };
281
+ }
282
+ function formatFinding(item) {
283
+ const fixSuffix = item.autoFixStatus === 'fixable' && item.replacementWord
284
+ ? ` | auto-fix "${item.replacementWord}"`
285
+ : ` | sem auto-fix: ${item.skipReason}`;
286
+ return `- ${item.file}:${item.lineNumber}: palavra "${item.affectedWord}" | matches "${item.snippets.join(', ')}" | ${item.reason}${fixSuffix}`;
287
+ }
288
+ function scanFiles(files, rootDir = process.cwd()) {
289
+ const findings = [];
290
+ const findingsByFile = new Map();
291
+ for (const file of files) {
292
+ const content = readFileSync(file, 'utf8');
293
+ const fileFindings = collectFindings(file, content, rootDir);
294
+ if (fileFindings.length > 0) {
295
+ findingsByFile.set(file, fileFindings);
296
+ findings.push(...fileFindings);
297
+ }
298
+ if (findings.length >= MAX_FINDINGS)
299
+ break;
300
+ }
301
+ return { findings, findingsByFile };
302
+ }
303
+ function reportFindings(findings, mode) {
304
+ const fixableFindings = findings.filter((finding) => finding.autoFixStatus === 'fixable');
305
+ const manualFindings = findings.filter((finding) => finding.autoFixStatus === 'manual');
306
+ const headline = mode === 'fix'
307
+ ? `INFO ${colors.yellow}[Encontrados]${colors.reset}: ${findings.length} indicio(s) de mojibake localizados antes da correcao.`
308
+ : `FAIL ${colors.red}[Falha]${colors.reset}: ${findings.length} indicio(s) de mojibake encontrados.`;
309
+ console.info(headline);
310
+ console.info(`INFO ${colors.cyan}[Resumo]${colors.reset}: ${fixableFindings.length} ocorrencia(s) com auto-fix conservador; ${manualFindings.length} ocorrencia(s) exigem revisao manual.`);
311
+ for (const item of findings) {
312
+ console.info(formatFinding(item));
313
+ console.info(` linha: ${item.lineText}`);
314
+ }
315
+ }
316
+ export function runMojibakeGuard(argv = [], rootDir = process.cwd()) {
317
+ const options = parseCliOptions(argv);
318
+ const files = [];
319
+ walk(rootDir, files);
320
+ const initialScan = scanFiles(files, rootDir);
321
+ if (initialScan.findings.length === 0) {
322
+ console.info(`OK ${colors.green}${colors.bright}[OK]${colors.reset}: Nenhum indicio de mojibake encontrado.`);
323
+ return 0;
324
+ }
325
+ reportFindings(initialScan.findings, options.fix ? 'fix' : 'scan');
326
+ if (!options.fix) {
327
+ return 1;
328
+ }
329
+ const plans = buildFixPlans(files, initialScan.findingsByFile, rootDir);
330
+ const appliedFixCount = plans.reduce((count, plan) => count + plan.appliedFixes.length, 0);
331
+ if (plans.length === 0 || appliedFixCount === 0) {
332
+ console.info(`INFO ${colors.yellow}[Auto-fix]${colors.reset}: Nenhuma correção automatica elegivel foi aplicada.`);
333
+ return 1;
334
+ }
335
+ console.info(`INFO ${colors.yellow}[Auto-fix]${colors.reset}: ${appliedFixCount} palavra(s) elegivel(is) em ${plans.length} arquivo(s).`);
336
+ for (const plan of plans) {
337
+ for (const fix of plan.appliedFixes) {
338
+ console.info(` ${plan.relativeFile}:${fix.lineNumber}: "${fix.affectedWord}" -> "${fix.replacementWord}"`);
339
+ }
340
+ }
341
+ if (options.dryRun) {
342
+ console.info(`INFO ${colors.yellow}[Dry-run]${colors.reset}: Nenhum arquivo foi alterado.`);
343
+ return 1;
344
+ }
345
+ const backupRoot = createBackupRoot();
346
+ try {
347
+ applyFixPlans(plans, backupRoot);
348
+ const cleanupResult = verifyAndCleanupBackups(plans, backupRoot);
349
+ /* v8 ignore next -- cleanup failures are covered through verifyAndCleanupBackups */
350
+ if (cleanupResult.deletedBackups.length > 0) {
351
+ console.info(`INFO ${colors.cyan}[Backups]${colors.reset}: ${cleanupResult.deletedBackups.length} backup(s) temporario(s) removido(s) apos verificacao.`);
352
+ }
353
+ /* v8 ignore next -- cleanup failures are covered through verifyAndCleanupBackups */
354
+ if (cleanupResult.keptBackups.length > 0) {
355
+ console.info(`INFO ${colors.yellow}[Backups]${colors.reset}: ${cleanupResult.keptBackups.length} backup(s) mantido(s) em ${cleanupResult.backupRoot}.`);
356
+ }
357
+ const postFixScan = scanFiles(files, rootDir);
358
+ if (postFixScan.findings.length === 0) {
359
+ console.info(`OK ${colors.green}${colors.bright}[Auto-fix aplicado]${colors.reset}: correcoes gravadas e backups temporarios verificados.`);
360
+ return 0;
361
+ }
362
+ console.info(`INFO ${colors.yellow}[Pos-fix]${colors.reset}: ainda restam ocorrencias que exigem nova execução ou revisao manual.`);
363
+ reportFindings(postFixScan.findings, 'post-fix');
364
+ return 1;
365
+ /* v8 ignore start -- filesystem write failure adapter */
366
+ }
367
+ catch (error) {
368
+ console.error(`FAIL ${colors.red}[Erro]${colors.reset}: ${error.message}`);
369
+ console.error(`INFO ${colors.yellow}[Backups]${colors.reset}: backups desta execução foram mantidos em ${backupRoot}.`);
370
+ return 1;
371
+ }
372
+ /* v8 ignore stop */
373
+ }
374
+ /* v8 ignore start -- executable adapter */
375
+ if (isMainModule(import.meta.url)) {
376
+ process.exitCode = runMojibakeGuard(process.argv.slice(2));
377
+ }
378
+ /* v8 ignore stop */
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Code Pattern Guard
4
+ *
5
+ * Detects prohibited or undesirable code patterns that are too specific for the
6
+ * global lint configuration, but important enough to be enforced in the
7
+ * architecture maintenance pipeline.
8
+ *
9
+ * This guard is intentionally conservative:
10
+ * - it blocks patterns that are known to be dangerous for our runtime/build
11
+ * - it keeps explicit allowlists for legacy or operational exceptions that are
12
+ * currently considered valid in the healthy system
13
+ *
14
+ * If a rule starts failing on the current mainline without a recent code change,
15
+ * the rule itself should be reviewed before the code is considered broken.
16
+ */
17
+ import ts from 'typescript';
18
+ import type { CodePatternGuardConfig } from '../config.js';
19
+ type RuleSeverity = 'forbidden' | 'undesirable';
20
+ /**
21
+ * A single concrete hit produced by a rule.
22
+ */
23
+ type PatternViolation = {
24
+ filePath: string;
25
+ line: number;
26
+ ruleId: string;
27
+ severity: RuleSeverity;
28
+ message: string;
29
+ snippet: string;
30
+ };
31
+ /**
32
+ * Shared runtime context prepared once and reused by the rules.
33
+ */
34
+ type PatternContext = {
35
+ rootDir: string;
36
+ backendProgram: ts.Program;
37
+ backendTypeChecker: ts.TypeChecker;
38
+ backendCompilerOptions: ts.CompilerOptions;
39
+ modelsDirectory: string;
40
+ };
41
+ /**
42
+ * Rule contract.
43
+ *
44
+ * The guard is intentionally step-based: each rule is one explicit stage in the
45
+ * architecture check, with its own scope, rationale and allowlist.
46
+ */
47
+ type PatternRule = {
48
+ id: string;
49
+ severity: RuleSeverity;
50
+ summary: string;
51
+ rationale: string;
52
+ includeDirs: string[];
53
+ allowedPathPatterns?: RegExp[];
54
+ rootDir?: string;
55
+ check: (sourceFile: ts.SourceFile, absoluteFilePath: string, context: PatternContext, rule: PatternRule) => PatternViolation[];
56
+ };
57
+ export declare function collectImportBindingIdentifiers(importClause: ts.ImportClause): ts.Identifier[];
58
+ export declare function isTypeOnlyUsage(identifier: ts.Identifier): boolean;
59
+ export declare function shouldPreferTypeImport(importDeclaration: ts.ImportDeclaration, sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker): boolean;
60
+ /**
61
+ * Rule catalog.
62
+ *
63
+ * Read this list as a sequence of architecture-check stages. Each block below
64
+ * exists for a specific class of production/build safety issue.
65
+ */
66
+ export declare const RULES: PatternRule[];
67
+ export declare function runCodePatternGuard(options?: {
68
+ rootDir?: string;
69
+ config?: CodePatternGuardConfig;
70
+ }): Promise<number>;
71
+ export {};