@ronaldjdevfs/forge 1.0.2 → 1.1.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 +130 -8
- package/package.json +2 -2
- package/skills/forge/SKILL.md +86 -15
- package/skills/forge/profiles/express-drizzle.md +107 -0
- package/skills/forge/profiles/fastify-mongodb.md +103 -0
- package/skills/forge/profiles/fastify-prisma.md +81 -0
- package/skills/forge/profiles/nestjs-mongodb.md +92 -0
- package/skills/forge/profiles/nestjs-postgres.md +98 -0
- package/skills/forge/reference/api-design.md +62 -0
- package/skills/forge/reference/assay.md +82 -0
- package/skills/forge/reference/cast.md +81 -7
- package/skills/forge/reference/data-patterns.md +86 -0
- package/skills/forge/reference/di-strategies.md +50 -0
- package/skills/forge/reference/errors.md +65 -0
- package/skills/forge/reference/events.md +95 -0
- package/skills/forge/reference/help.md +40 -0
- package/skills/forge/reference/hooks.md +62 -0
- package/skills/forge/reference/observability.md +66 -0
- package/skills/forge/reference/patterns.md +52 -0
- package/skills/forge/reference/principles.md +6 -0
- package/skills/forge/reference/reforge.md +69 -5
- package/skills/forge/reference/relocate.md +15 -2
- package/skills/forge/reference/security-patterns.md +87 -0
- package/skills/forge/reference/testing-patterns.md +69 -0
- package/skills/forge/scripts/assay.mjs +481 -0
- package/skills/forge/scripts/context.mjs +147 -43
- package/skills/forge/scripts/detect.mjs +371 -22
- package/skills/forge/scripts/forge-api.mjs +373 -0
- package/skills/forge/scripts/forge-config.mjs +268 -0
- package/skills/forge/scripts/forge-signals.mjs +131 -0
- package/skills/forge/scripts/forge-state.mjs +97 -0
- package/skills/forge/scripts/formatter.mjs +133 -0
- package/skills/forge/scripts/graph.mjs +5 -21
- package/skills/forge/scripts/hook.mjs +250 -0
- package/skills/forge/scripts/inspect.mjs +171 -22
- package/skills/forge/scripts/parse-imports.mjs +249 -0
- package/skills/forge/scripts/pin.mjs +151 -0
- package/skills/forge/scripts/posttool.mjs +224 -0
- package/skills/forge/scripts/profile.mjs +124 -20
- package/skills/forge/scripts/registry/rules.mjs +344 -0
- package/skills/forge/scripts/rename.mjs +669 -0
- package/skills/forge/scripts/rollback.mjs +213 -0
- package/skills/forge/scripts/update.mjs +114 -0
- package/skills/forge/templates/feature/domain-error.ts.md +9 -0
- package/skills/forge/templates/feature/domain-event.ts.md +9 -0
- package/skills/forge/templates/feature/event-handler.ts.md +10 -0
- package/skills/forge/templates/feature/use-case.ts.md +10 -2
- package/skills/forge/tests/core.test.mjs +403 -0
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { join, relative, basename } from "path";
|
|
4
|
-
import { readFileSync, existsSync, readdirSync, statSync } from "fs";
|
|
4
|
+
import { readFileSync, existsSync, readdirSync, statSync, writeFileSync, renameSync, mkdirSync } from "fs";
|
|
5
5
|
import { buildGraph } from "./graph.mjs";
|
|
6
6
|
import { buildOwnershipReport } from "./armorer.mjs";
|
|
7
|
+
import { parseImportsWithLines } from "./parse-imports.mjs";
|
|
8
|
+
import { detectNamingViolations, computeExpectedName } from "./rename.mjs";
|
|
9
|
+
import { formatViolation, formatCheck, GREEN, RED, YELLOW, CYAN, BOLD, RESET, DIM, GRAY } from "./formatter.mjs";
|
|
7
10
|
|
|
8
11
|
const ROOT = process.cwd();
|
|
9
12
|
const SRC = join(ROOT, "src");
|
|
@@ -19,8 +22,6 @@ const LEGACY_DIRS = [
|
|
|
19
22
|
["src/adapters/out/database/mappers", "Mappers legacy"],
|
|
20
23
|
["src/setting/dependencies", "DI wiring legacy (.di.ts)"],
|
|
21
24
|
];
|
|
22
|
-
|
|
23
|
-
const IMPORT_RE = /import\s+(?:type\s+)?(?:\{[^}]*\}|[^;{]+)\s+from\s+['"]([^'"]+)['"]/g;
|
|
24
25
|
const INJECTABLE_RE = /@injectable\s*\(\)/g;
|
|
25
26
|
const INJECT_RE = /@inject\s*\([^)]+\)/g;
|
|
26
27
|
const BD_MODEL_RE = /\b\w+Model\s*\.\s*(find|findOne|findById|create|insertMany|updateOne|updateMany|deleteOne|deleteMany|aggregate|save|lean)\s*\(/g;
|
|
@@ -77,7 +78,7 @@ function findFiles(dir, ext, maxDepth = 5) {
|
|
|
77
78
|
return results;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
function detectFeaturesOnSrc() {
|
|
81
|
+
export function detectFeaturesOnSrc() {
|
|
81
82
|
if (!isDir(FEATURES)) return [];
|
|
82
83
|
return listDir(FEATURES).filter((f) => isDir(join(FEATURES, f)));
|
|
83
84
|
}
|
|
@@ -98,18 +99,7 @@ function detectLegacyFeatures() {
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
function parseImports(content, filePath) {
|
|
101
|
-
|
|
102
|
-
const lines = content.split("\n");
|
|
103
|
-
for (let i = 0; i < lines.length; i++) {
|
|
104
|
-
const m = lines[i].match(IMPORT_RE);
|
|
105
|
-
if (m) {
|
|
106
|
-
for (const full of m) {
|
|
107
|
-
const src = full.match(/from\s+['"]([^'"]+)['"]/);
|
|
108
|
-
if (src) imports.push({ file: filePath, source: src[1], line: i + 1 });
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return imports;
|
|
102
|
+
return parseImportsWithLines(content, filePath);
|
|
113
103
|
}
|
|
114
104
|
|
|
115
105
|
function hasDecorator(content, decoratorRe) {
|
|
@@ -129,6 +119,99 @@ function severity(label, sev) {
|
|
|
129
119
|
return { severity: sev, label };
|
|
130
120
|
}
|
|
131
121
|
|
|
122
|
+
// ── Inline Ignores ──
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Parse all forge-ignore comments in a file.
|
|
126
|
+
* Returns a Set of rule IDs that should be ignored for each line.
|
|
127
|
+
*/
|
|
128
|
+
export function parseInlineIgnores(content) {
|
|
129
|
+
const ignores = {}; // lineNumber → Set<ruleId>
|
|
130
|
+
const lines = content.split("\n");
|
|
131
|
+
|
|
132
|
+
for (let i = 0; i < lines.length; i++) {
|
|
133
|
+
const trimmed = lines[i].trim();
|
|
134
|
+
|
|
135
|
+
// // forge-ignore-next-line
|
|
136
|
+
if (trimmed.includes("// forge-ignore-next-line")) {
|
|
137
|
+
const targetLine = i + 2; // next line is 1-indexed
|
|
138
|
+
if (!ignores[targetLine]) ignores[targetLine] = new Set();
|
|
139
|
+
ignores[targetLine].add("*");
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// // forge-ignore: R1, R3 (applies to current line)
|
|
144
|
+
const match = trimmed.match(/\/\/\s*forge-ignore:\s*(.+)/);
|
|
145
|
+
if (match) {
|
|
146
|
+
if (!ignores[i + 1]) ignores[i + 1] = new Set();
|
|
147
|
+
const rules = match[1].split(",").map(r => r.trim().toUpperCase());
|
|
148
|
+
for (const r of rules) ignores[i + 1].add(r);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return ignores;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Load inline ignores for all .ts/.mjs files in a directory (recursive).
|
|
157
|
+
* Returns Map<filePath, Map<lineNumber, Set<ruleId>>>
|
|
158
|
+
*/
|
|
159
|
+
export function loadAllInlineIgnores(dir, maxDepth = 10) {
|
|
160
|
+
const allIgnores = new Map();
|
|
161
|
+
|
|
162
|
+
function walk(d, depth) {
|
|
163
|
+
if (depth > maxDepth) return;
|
|
164
|
+
try {
|
|
165
|
+
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
166
|
+
const full = join(d, entry.name);
|
|
167
|
+
if (entry.isDirectory()) {
|
|
168
|
+
walk(full, depth + 1);
|
|
169
|
+
} else if (entry.name.endsWith(".ts") || entry.name.endsWith(".mjs") || entry.name.endsWith(".js")) {
|
|
170
|
+
const content = read(full);
|
|
171
|
+
if (!content) continue;
|
|
172
|
+
const ignores = parseInlineIgnores(content);
|
|
173
|
+
if (Object.keys(ignores).length > 0) {
|
|
174
|
+
allIgnores.set(full, ignores);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch { /* skip */ }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (existsSync(dir)) walk(dir, 0);
|
|
182
|
+
return allIgnores;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Check if a violation should be ignored based on inline ignores.
|
|
187
|
+
*/
|
|
188
|
+
export function isIgnored(violation, allIgnores) {
|
|
189
|
+
if (!violation.detail) return false;
|
|
190
|
+
const filePath = violation.detail.split(":")[0];
|
|
191
|
+
const lineMatch = violation.detail.match(/:(\d+)/);
|
|
192
|
+
if (!lineMatch) return false;
|
|
193
|
+
|
|
194
|
+
const line = parseInt(lineMatch[1], 10);
|
|
195
|
+
const fileIgnores = allIgnores.get(filePath);
|
|
196
|
+
if (!fileIgnores) return false;
|
|
197
|
+
|
|
198
|
+
const lineIgnores = fileIgnores[line];
|
|
199
|
+
if (!lineIgnores) return false;
|
|
200
|
+
|
|
201
|
+
if (lineIgnores.has("*")) return true;
|
|
202
|
+
|
|
203
|
+
const ruleMatch = violation.label.match(/\[([^\]]+)\]/);
|
|
204
|
+
if (ruleMatch) {
|
|
205
|
+
const ruleId = ruleMatch[1].toUpperCase();
|
|
206
|
+
if (lineIgnores.has(ruleId)) return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Also check if the violation's rule id is in the ignores
|
|
210
|
+
if (violation.rule && lineIgnores.has(violation.rule.toUpperCase())) return true;
|
|
211
|
+
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
132
215
|
/* ── Checks ── */
|
|
133
216
|
|
|
134
217
|
export function checkStructure(features) {
|
|
@@ -793,25 +876,242 @@ export function checkDependencies(ctx) {
|
|
|
793
876
|
return { score: Math.min(score, 15), checks };
|
|
794
877
|
}
|
|
795
878
|
|
|
879
|
+
// ── Custom rules (B2) ──
|
|
880
|
+
|
|
881
|
+
const CUSTOM_RULES_PATH = join(ROOT, ".forge", "rules.json");
|
|
882
|
+
|
|
883
|
+
export function loadCustomRules() {
|
|
884
|
+
try {
|
|
885
|
+
const raw = readFileSync(CUSTOM_RULES_PATH, "utf-8");
|
|
886
|
+
return JSON.parse(raw);
|
|
887
|
+
} catch {
|
|
888
|
+
return [];
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
export function checkCustomRules(features) {
|
|
893
|
+
const rules = loadCustomRules();
|
|
894
|
+
const checks = [];
|
|
895
|
+
let score = 0;
|
|
896
|
+
|
|
897
|
+
if (rules.length === 0) {
|
|
898
|
+
checks.push({ severity: SEVERITY.INFO, label: "Reglas custom: vacío (usa .forge/rules.json)", pass: true });
|
|
899
|
+
score += 5;
|
|
900
|
+
return { score, checks };
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
let totalPatterns = 0;
|
|
904
|
+
let violations = 0;
|
|
905
|
+
|
|
906
|
+
for (const rule of rules) {
|
|
907
|
+
if (!rule.id || !rule.pattern) continue;
|
|
908
|
+
totalPatterns++;
|
|
909
|
+
|
|
910
|
+
let fileRe = null;
|
|
911
|
+
let contentRe = null;
|
|
912
|
+
try {
|
|
913
|
+
if (rule.files) fileRe = new RegExp(rule.files);
|
|
914
|
+
contentRe = new RegExp(rule.pattern);
|
|
915
|
+
} catch {
|
|
916
|
+
checks.push({ severity: SEVERITY.ERROR, label: `Regla custom ${rule.id}: patrón inválido`, pass: false });
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
for (const feat of features) {
|
|
921
|
+
if (!feat.files) continue;
|
|
922
|
+
for (const f of feat.files) {
|
|
923
|
+
if (fileRe && !fileRe.test(f)) continue;
|
|
924
|
+
const content = read(f);
|
|
925
|
+
if (!content) continue;
|
|
926
|
+
|
|
927
|
+
contentRe.lastIndex = 0;
|
|
928
|
+
let match;
|
|
929
|
+
while ((match = contentRe.exec(content)) !== null) {
|
|
930
|
+
violations++;
|
|
931
|
+
const line = content.slice(0, match.index).split("\n").length;
|
|
932
|
+
checks.push({
|
|
933
|
+
severity: rule.severity || SEVERITY.ERROR,
|
|
934
|
+
label: `[${rule.id}] ${rule.message || "Violación de regla custom"} en ${relative(ROOT, f)}:${line}`,
|
|
935
|
+
pass: false,
|
|
936
|
+
detail: `Match: "${match[0].slice(0, 80)}"`,
|
|
937
|
+
fix: rule.fix || null,
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if (totalPatterns > 0 && violations === 0) {
|
|
945
|
+
checks.push({ severity: SEVERITY.INFO, label: `Reglas custom: ${totalPatterns} patrón(es) activo(s), 0 violaciones`, pass: true });
|
|
946
|
+
score += 5;
|
|
947
|
+
} else if (totalPatterns > 0) {
|
|
948
|
+
checks.push({ severity: SEVERITY.WARNING, label: `Reglas custom: ${totalPatterns} patrón(es), ${violations} violación(es)`, pass: violations === 0 });
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return { score, checks };
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
export function checkNaming(projectRoot = ROOT) {
|
|
955
|
+
const checks = [];
|
|
956
|
+
let score = 10;
|
|
957
|
+
|
|
958
|
+
const violations = detectNamingViolations(projectRoot);
|
|
959
|
+
|
|
960
|
+
if (violations.length === 0) {
|
|
961
|
+
checks.push({ ...severity("Naming conventions: sin violaciones", SEVERITY.INFO), pass: true });
|
|
962
|
+
return { score, checks };
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
checks.push({
|
|
966
|
+
...severity(`${violations.length} violacion(es) de naming conventions`, SEVERITY.WARNING),
|
|
967
|
+
pass: false,
|
|
968
|
+
fix: "Ejecutar `node .opencode/skills/forge/scripts/rename.mjs --all` o `forge reforge <filename>` para corregir",
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
for (const v of violations) {
|
|
972
|
+
checks.push({
|
|
973
|
+
...severity(`Naming: ${relative(ROOT, v.current)}`, SEVERITY.SUGGESTION),
|
|
974
|
+
pass: false,
|
|
975
|
+
detail: `→ ${relative(ROOT, v.expected)}`,
|
|
976
|
+
fix: v.rule,
|
|
977
|
+
});
|
|
978
|
+
score -= 1;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
return { score: Math.max(score, 0), checks };
|
|
982
|
+
}
|
|
983
|
+
|
|
796
984
|
export function allChecks(features, graph, ctx) {
|
|
797
985
|
const g = graph || buildGraph(process.cwd());
|
|
798
986
|
const c = ctx || {};
|
|
799
987
|
return {
|
|
800
988
|
structure: checkStructure(features),
|
|
801
989
|
layers: checkLayers(features),
|
|
990
|
+
decorators: checkDecorators(features),
|
|
802
991
|
ownership: checkOwnership(c),
|
|
803
992
|
platform: checkPlatform(c),
|
|
804
993
|
dependencies: checkDependencies(c),
|
|
805
994
|
graph: checkGraph(g),
|
|
995
|
+
customRules: checkCustomRules(features),
|
|
996
|
+
naming: checkNaming(),
|
|
806
997
|
};
|
|
807
998
|
}
|
|
808
999
|
|
|
1000
|
+
// ── Auto-Fix ──
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Apply auto-fixes for non-CRITICAL violations.
|
|
1004
|
+
* Returns { fixed: number, skipped: number, details: string[] }
|
|
1005
|
+
*/
|
|
1006
|
+
export function applyFixes(checks, projectRoot = ROOT) {
|
|
1007
|
+
let fixed = 0;
|
|
1008
|
+
let skipped = 0;
|
|
1009
|
+
const details = [];
|
|
1010
|
+
const SRC = join(projectRoot, "src");
|
|
1011
|
+
|
|
1012
|
+
for (const check of checks) {
|
|
1013
|
+
if (check.pass) continue;
|
|
1014
|
+
if (check.severity === "CRITICAL") {
|
|
1015
|
+
skipped++;
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Fix missing @injectable()
|
|
1020
|
+
if (check.fix?.startsWith("Agregar @injectable()") && check.detail) {
|
|
1021
|
+
const filePath = check.detail.includes(":")
|
|
1022
|
+
? join(projectRoot, check.detail.split(":")[0])
|
|
1023
|
+
: null;
|
|
1024
|
+
if (filePath && existsSync(filePath)) {
|
|
1025
|
+
const content = readFileSync(filePath, "utf-8");
|
|
1026
|
+
const className = check.detail.replace(/.*\//, "").replace(/\.[jt]s/, "");
|
|
1027
|
+
const newContent = content.replace(
|
|
1028
|
+
/(export\s+class\s+\w+)/,
|
|
1029
|
+
"@injectable()\n$1"
|
|
1030
|
+
);
|
|
1031
|
+
if (newContent !== content) {
|
|
1032
|
+
writeFileSync(filePath, newContent);
|
|
1033
|
+
fixed++;
|
|
1034
|
+
details.push(` ${GREEN}✔${RESET} ${relative(projectRoot, filePath)}: agregado @injectable()`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Fix missing experimentalDecorators
|
|
1040
|
+
if (check.fix?.includes('"experimentalDecorators"')) {
|
|
1041
|
+
const tsconfigPath = join(projectRoot, "tsconfig.json");
|
|
1042
|
+
if (existsSync(tsconfigPath)) {
|
|
1043
|
+
try {
|
|
1044
|
+
const content = readFileSync(tsconfigPath, "utf-8");
|
|
1045
|
+
const json = JSON.parse(content);
|
|
1046
|
+
json.compilerOptions = json.compilerOptions || {};
|
|
1047
|
+
json.compilerOptions.experimentalDecorators = true;
|
|
1048
|
+
json.compilerOptions.emitDecoratorMetadata = true;
|
|
1049
|
+
writeFileSync(tsconfigPath, JSON.stringify(json, null, 2) + "\n");
|
|
1050
|
+
fixed++;
|
|
1051
|
+
details.push(` ${GREEN}✔${RESET} tsconfig.json: agregado experimentalDecorators / emitDecoratorMetadata`);
|
|
1052
|
+
} catch {}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Fix naming violations
|
|
1057
|
+
if (check.label.includes("Naming:") && check.detail?.includes("→")) {
|
|
1058
|
+
const current = join(projectRoot, check.detail.split(" →")[0].trim());
|
|
1059
|
+
const expected = join(projectRoot, check.detail.split("→ ")[1].trim());
|
|
1060
|
+
if (existsSync(current) && !existsSync(expected)) {
|
|
1061
|
+
try {
|
|
1062
|
+
const dir = join(expected, "..");
|
|
1063
|
+
mkdirSync(dir, { recursive: true });
|
|
1064
|
+
renameSync(current, expected);
|
|
1065
|
+
fixed++;
|
|
1066
|
+
details.push(` ${GREEN}✔${RESET} Renombrado: ${relative(projectRoot, current)} → ${relative(projectRoot, expected)}`);
|
|
1067
|
+
} catch (e) {
|
|
1068
|
+
skipped++;
|
|
1069
|
+
details.push(` ${YELLOW}⚠${RESET} No se pudo renombrar: ${e.message}`);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Fix container.resolve()
|
|
1075
|
+
if (check.fix?.includes("container.resolve") && check.detail) {
|
|
1076
|
+
const filePath = join(projectRoot, check.detail);
|
|
1077
|
+
if (existsSync(filePath)) {
|
|
1078
|
+
const content = readFileSync(filePath, "utf-8");
|
|
1079
|
+
const newContent = content
|
|
1080
|
+
.replace(/container\.resolve\([^)]+\)/g, "/* DI: injected via constructor */ null")
|
|
1081
|
+
.replace(/import.*container.*from.*tsyringe.*/g, "// container resolve removed");
|
|
1082
|
+
if (newContent !== content) {
|
|
1083
|
+
writeFileSync(filePath, newContent);
|
|
1084
|
+
fixed++;
|
|
1085
|
+
details.push(` ${GREEN}✔${RESET} ${relative(projectRoot, filePath)}: container.resolve() reemplazado`);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// Fix missing reflect-metadata import
|
|
1091
|
+
if (check.fix?.includes('import "reflect-metadata"') && check.detail) {
|
|
1092
|
+
const filePath = join(projectRoot, check.detail);
|
|
1093
|
+
if (existsSync(filePath)) {
|
|
1094
|
+
const content = readFileSync(filePath, "utf-8");
|
|
1095
|
+
if (!content.includes('import "reflect-metadata"')) {
|
|
1096
|
+
writeFileSync(filePath, 'import "reflect-metadata";\n' + content);
|
|
1097
|
+
fixed++;
|
|
1098
|
+
details.push(` ${GREEN}✔${RESET} ${relative(projectRoot, filePath)}: agregado import reflect-metadata`);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
return { fixed, skipped, details };
|
|
1105
|
+
}
|
|
1106
|
+
|
|
809
1107
|
/* ── CLI ── */
|
|
810
1108
|
async function main() {
|
|
811
1109
|
const args = process.argv.slice(2);
|
|
812
1110
|
const filterType = args.includes("--type") ? args[args.indexOf("--type") + 1] : null;
|
|
813
1111
|
const filterSeverity = args.includes("--severity") ? args[args.indexOf("--severity") + 1] : null;
|
|
814
1112
|
const format = args.includes("--json") ? "json" : "text";
|
|
1113
|
+
const doFix = args.includes("--fix");
|
|
1114
|
+
const showIgnores = args.includes("--show-ignores");
|
|
815
1115
|
|
|
816
1116
|
const { buildContext } = await import("./context.mjs");
|
|
817
1117
|
const ctx = await buildContext();
|
|
@@ -819,22 +1119,71 @@ async function main() {
|
|
|
819
1119
|
const graph = buildGraph(ROOT);
|
|
820
1120
|
const results = allChecks(features, graph, ctx);
|
|
821
1121
|
|
|
1122
|
+
// Load inline ignores
|
|
1123
|
+
const allIgnores = loadAllInlineIgnores(join(ROOT, "src"));
|
|
1124
|
+
|
|
822
1125
|
let all = [];
|
|
823
1126
|
for (const [, cat] of Object.entries(results)) {
|
|
824
1127
|
all = all.concat(cat.checks);
|
|
825
1128
|
}
|
|
826
1129
|
|
|
1130
|
+
// Filter out ignored violations
|
|
1131
|
+
const filtered = all.filter(c => {
|
|
1132
|
+
if (c.pass) return true;
|
|
1133
|
+
if (isIgnored(c, allIgnores)) return false;
|
|
1134
|
+
return true;
|
|
1135
|
+
});
|
|
1136
|
+
|
|
827
1137
|
if (filterSeverity) all = all.filter((c) => c.severity === filterSeverity);
|
|
828
1138
|
|
|
1139
|
+
if (doFix) {
|
|
1140
|
+
const result = applyFixes(filtered);
|
|
1141
|
+
console.log(`\n${BOLD}Auto-Fix${RESET}`);
|
|
1142
|
+
if (result.fixed > 0) {
|
|
1143
|
+
result.details.forEach(d => console.log(d));
|
|
1144
|
+
console.log(`\n${GREEN}✔${RESET} ${result.fixed} violación(es) corregidas automáticamente`);
|
|
1145
|
+
} else {
|
|
1146
|
+
console.log(` ${GRAY}No se encontraron violaciones auto-corregibles${RESET}`);
|
|
1147
|
+
}
|
|
1148
|
+
if (result.skipped > 0) {
|
|
1149
|
+
console.log(` ${YELLOW}⚠${RESET} ${result.skipped} violación(es) CRITICAL no se auto-corrigieron (requieren intervención manual)`);
|
|
1150
|
+
}
|
|
1151
|
+
console.log();
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// Show inline ignores summary
|
|
1156
|
+
let totalIgnores = 0;
|
|
1157
|
+
for (const [, fileIgnores] of allIgnores) {
|
|
1158
|
+
for (const [, rules] of Object.entries(fileIgnores)) {
|
|
1159
|
+
totalIgnores += rules.size;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
const ignoredCount = all.length - filtered.length;
|
|
1163
|
+
|
|
829
1164
|
if (format === "json") {
|
|
830
|
-
console.log(JSON.stringify({
|
|
1165
|
+
console.log(JSON.stringify({
|
|
1166
|
+
checks: filtered,
|
|
1167
|
+
total: filtered.length,
|
|
1168
|
+
ignoredCount,
|
|
1169
|
+
inlineIgnores: totalIgnores,
|
|
1170
|
+
}, null, 2));
|
|
831
1171
|
} else {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
console.log(`
|
|
835
|
-
|
|
1172
|
+
if (showIgnores && totalIgnores > 0) {
|
|
1173
|
+
console.log(`\n${BOLD}Inline Ignores${RESET}`);
|
|
1174
|
+
console.log(` ${totalIgnores} ignore(s) en ${allIgnores.size} archivo(s)`);
|
|
1175
|
+
for (const [file, lines] of allIgnores) {
|
|
1176
|
+
for (const [line, rules] of Object.entries(lines)) {
|
|
1177
|
+
console.log(` ${GRAY}${relative(ROOT, file)}:${line}${RESET} → ${[...rules].join(", ")}`);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
console.log();
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
for (const c of filtered) {
|
|
1184
|
+
console.log(formatCheck(c));
|
|
836
1185
|
}
|
|
837
|
-
console.log(`\nTotal: ${
|
|
1186
|
+
console.log(`\nTotal: ${filtered.length} checks${ignoredCount > 0 ? ` (${ignoredCount} ignorados inline)` : ""}`);
|
|
838
1187
|
}
|
|
839
1188
|
}
|
|
840
1189
|
|