@ronaldjdevfs/forge 1.0.1

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 (56) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +9 -0
  3. package/README.md +338 -0
  4. package/logo.png +0 -0
  5. package/package.json +47 -0
  6. package/skills/forge/SKILL.md +207 -0
  7. package/skills/forge/command/forge.md +110 -0
  8. package/skills/forge/profiles/express-mongodb.md +289 -0
  9. package/skills/forge/profiles/express-postgres.md +164 -0
  10. package/skills/forge/profiles/express-prisma.md +116 -0
  11. package/skills/forge/profiles/fastify-postgres.md +110 -0
  12. package/skills/forge/profiles/nestjs-prisma.md +160 -0
  13. package/skills/forge/reference/cast.md +77 -0
  14. package/skills/forge/reference/chain.md +73 -0
  15. package/skills/forge/reference/forge.md +44 -0
  16. package/skills/forge/reference/inscribe.md +129 -0
  17. package/skills/forge/reference/inspect.md +58 -0
  18. package/skills/forge/reference/patterns.md +108 -0
  19. package/skills/forge/reference/principles.md +29 -0
  20. package/skills/forge/reference/quench.md +74 -0
  21. package/skills/forge/reference/reforge.md +40 -0
  22. package/skills/forge/reference/relocate.md +38 -0
  23. package/skills/forge/reference/smelt.md +31 -0
  24. package/skills/forge/reference/temper.md +49 -0
  25. package/skills/forge/scripts/architecture.mjs +176 -0
  26. package/skills/forge/scripts/armorer.mjs +421 -0
  27. package/skills/forge/scripts/bootstrap.mjs +187 -0
  28. package/skills/forge/scripts/chain.mjs +258 -0
  29. package/skills/forge/scripts/context.mjs +237 -0
  30. package/skills/forge/scripts/detect.mjs +843 -0
  31. package/skills/forge/scripts/graph.mjs +594 -0
  32. package/skills/forge/scripts/inspect.mjs +193 -0
  33. package/skills/forge/scripts/profile.mjs +92 -0
  34. package/skills/forge/templates/feature/controller.ts.md +66 -0
  35. package/skills/forge/templates/feature/entity.ts.md +11 -0
  36. package/skills/forge/templates/feature/mapper.ts.md +18 -0
  37. package/skills/forge/templates/feature/repository-impl.ts.md +59 -0
  38. package/skills/forge/templates/feature/repository-interface.ts.md +12 -0
  39. package/skills/forge/templates/feature/routes.ts.md +17 -0
  40. package/skills/forge/templates/feature/schema.ts.md +16 -0
  41. package/skills/forge/templates/feature/use-case.ts.md +19 -0
  42. package/skills/forge/templates/infra/mail.ts.md +31 -0
  43. package/skills/forge/templates/infra/mongodb.ts.md +12 -0
  44. package/skills/forge/templates/infra/prisma.ts.md +21 -0
  45. package/skills/forge/templates/infra/redis.ts.md +30 -0
  46. package/skills/forge/templates/platform/config.ts.md +37 -0
  47. package/skills/forge/templates/platform/database.ts.md +40 -0
  48. package/skills/forge/templates/platform/di.ts.md +18 -0
  49. package/skills/forge/templates/platform/http.ts.md +29 -0
  50. package/skills/forge/templates/platform/logger.ts.md +38 -0
  51. package/skills/forge/templates/platform/server.ts.md +25 -0
  52. package/skills/forge/templates/shared/contract.ts.md +20 -0
  53. package/skills/forge/templates/shared/error.ts.md +27 -0
  54. package/skills/forge/templates/shared/type.ts.md +18 -0
  55. package/skills/forge/templates/shared/util.ts.md +23 -0
  56. package/src/cli.js +179 -0
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { join, basename } from "path";
4
+ import { writeFileSync, existsSync } from "fs";
5
+ import { buildContext } from "./context.mjs";
6
+ import { detectProfile, detectProfileExtended } from "./profile.mjs";
7
+ import { buildGraph, exportGraph } from "./graph.mjs";
8
+ import { buildOwnershipReport } from "./armorer.mjs";
9
+ import { allChecks } from "./detect.mjs";
10
+
11
+ const ROOT = process.cwd();
12
+
13
+ function formatDate() {
14
+ return new Date().toISOString().slice(0, 10);
15
+ }
16
+
17
+ function generateMd(ctx, profile, graph, ownership, auditScore) {
18
+ const sections = [];
19
+
20
+ sections.push("# Architecture State\n");
21
+ sections.push(`**Project Name:** ${ctx.projectName} `);
22
+ sections.push(`**Framework:** ${ctx.framework} `);
23
+ sections.push(`**Runtime:** ${ctx.runtime} `);
24
+ sections.push(`**Database:** ${ctx.database} `);
25
+ sections.push(`**ORM:** ${ctx.orm} `);
26
+ sections.push(`**DI Strategy:** ${ctx.diStrategy} `);
27
+ sections.push(`**Profile:** ${profile} `);
28
+ sections.push(`**Architecture:** hexagonal-feature (Platform + Features + Shared + Infra) `);
29
+ sections.push(`**Last Audit:** ${formatDate()} (score: ${auditScore}/100) `);
30
+ sections.push("");
31
+
32
+ /* Platform */
33
+ sections.push("## Platform\n");
34
+ if (ctx.platform && ctx.platform.exists && ctx.platform.components.length > 0) {
35
+ for (const comp of ctx.platform.components) {
36
+ sections.push(`- \`platform/${comp}/\``);
37
+ }
38
+ } else {
39
+ sections.push("*(No detectado)*");
40
+ }
41
+ sections.push("");
42
+
43
+ /* Features */
44
+ sections.push("## Features\n");
45
+ if (ctx.features.migrated.length > 0) {
46
+ for (const feat of ctx.features.migrated) {
47
+ sections.push(`- \`features/${feat}/\``);
48
+ }
49
+ }
50
+ if (ctx.features.legacy.length > 0) {
51
+ sections.push("\n### Legacy\n");
52
+ for (const feat of ctx.features.legacy) {
53
+ sections.push(`- \`${feat}\` (legacy)`);
54
+ }
55
+ }
56
+ if (ctx.features.migrated.length === 0 && ctx.features.legacy.length === 0) {
57
+ sections.push("*(No detectado)*");
58
+ }
59
+ sections.push("");
60
+
61
+ /* Shared */
62
+ sections.push("## Shared\n");
63
+ if (ctx.shared && ctx.shared.exists && ctx.shared.components.length > 0) {
64
+ for (const comp of ctx.shared.components) {
65
+ sections.push(`- \`shared/${comp}/\``);
66
+ }
67
+ } else {
68
+ sections.push("*(No detectado)*");
69
+ }
70
+ sections.push("");
71
+
72
+ /* Infrastructure */
73
+ sections.push("## Infrastructure\n");
74
+ if (ctx.infra && ctx.infra.exists && ctx.infra.components.length > 0) {
75
+ for (const comp of ctx.infra.components) {
76
+ sections.push(`- \`infra/${comp}/\``);
77
+ }
78
+ } else {
79
+ sections.push("*(No detectado)*");
80
+ }
81
+ sections.push("");
82
+
83
+ /* Ownership */
84
+ sections.push("## Ownership\n");
85
+ sections.push(`**Health:** ${ownership.health} `);
86
+ sections.push(`**Score:** ${ownership.score}/100 `);
87
+ sections.push(`**Orphans:** ${ownership.orphans.length} `);
88
+ sections.push(`**Duplicates:** ${ownership.duplicates.length} `);
89
+ sections.push(`**Misplaced:** ${ownership.misplaced.length} `);
90
+ if (ownership.orphans.length > 0) {
91
+ sections.push("\n### Orphans\n");
92
+ for (const o of ownership.orphans) {
93
+ sections.push(`- \`${o.path}\` — ${o.reason}`);
94
+ }
95
+ }
96
+ sections.push("");
97
+
98
+ /* Architecture Graph */
99
+ sections.push(exportGraph(graph));
100
+
101
+ /* Dependency Health */
102
+ sections.push("## Dependency Health\n");
103
+ sections.push(`**Valid Edges:** ${graph.edges.filter(e => e.type !== "violates").length}/${graph.edges.length} `);
104
+ sections.push(`**Dependency Health:** ${graph.stats.dependencyHealth}% `);
105
+ sections.push(`**Risk Score:** ${graph.stats.riskScore}/100 `);
106
+ sections.push(`**Health:** ${graph.stats.health} `);
107
+ sections.push("");
108
+
109
+ /* Violations */
110
+ if (graph.violations.length > 0) {
111
+ sections.push("## Violations\n");
112
+ sections.push("| Rule | From | To | Severity | Description |\n");
113
+ sections.push("|------|------|----|----------|-------------|\n");
114
+ for (const v of graph.violations) {
115
+ sections.push(`| ${v.rule} | \`${v.from}\` | \`${v.to}\` | ${v.severity} | ${v.description} |\n`);
116
+ }
117
+ sections.push("");
118
+ }
119
+
120
+ /* Context */
121
+ sections.push("## Context\n");
122
+ sections.push(`- **Has Src:** ${ctx.hasSrc} `);
123
+ sections.push(`- **Has Features Dir:** ${ctx.hasFeaturesDir} `);
124
+ sections.push(`- **Has Platform Dir:** ${ctx.hasPlatformDir} `);
125
+ sections.push(`- **Has Shared Dir:** ${ctx.hasSharedDir} `);
126
+ sections.push(`- **Has Infra Dir:** ${ctx.hasInfraDir} `);
127
+ sections.push(`- **Is Migrating:** ${ctx.isMigrating} `);
128
+ sections.push(`- **Is Fully Migrated:** ${ctx.isFullyMigrated} `);
129
+ sections.push(`- **Is Legacy:** ${ctx.isLegacy} `);
130
+ sections.push(`- **Is Greenfield:** ${ctx.isGreenfield} `);
131
+ sections.push(`- **Total Features:** ${ctx.features.total} `);
132
+ sections.push("");
133
+
134
+ /* Tech Stack */
135
+ sections.push("## Tech Stack\n");
136
+ sections.push("| Component | Technology |\n");
137
+ sections.push("|-----------|------------|\n");
138
+ sections.push(`| Framework | ${ctx.framework} |\n`);
139
+ sections.push(`| Runtime | ${ctx.runtime} |\n`);
140
+ sections.push(`| Database | ${ctx.database} |\n`);
141
+ sections.push(`| ORM | ${ctx.orm} |\n`);
142
+ sections.push(`| DI Strategy | ${ctx.diStrategy} |\n`);
143
+ sections.push(`| Profile | ${profile} |\n`);
144
+ sections.push("");
145
+
146
+ return sections.join("\n");
147
+ }
148
+
149
+ async function main() {
150
+ const args = process.argv.slice(2);
151
+ const outputPath = args.includes("--output")
152
+ ? args[args.indexOf("--output") + 1]
153
+ : join(ROOT, "ARCHITECTURE.md");
154
+
155
+ const ctx = await buildContext();
156
+ const profile = detectProfile(ctx);
157
+ const graph = buildGraph();
158
+ const ownership = buildOwnershipReport();
159
+ const features = ctx.features.migrated;
160
+ const result = allChecks(features, graph, ctx);
161
+
162
+ const totalScore = (result.structure?.score || 0) + (result.layers?.score || 0)
163
+ + (result.ownership?.score || 0) + (result.platform?.score || 0)
164
+ + (result.dependencies?.score || 0) + (result.graph?.score || 0);
165
+ const maxScore = 20 + 20 + 20 + 15 + 15 + 20;
166
+ const auditScore = maxScore > 0 ? Math.round((totalScore / maxScore) * 100) : 0;
167
+
168
+ const md = generateMd(ctx, profile, graph, ownership, auditScore);
169
+ writeFileSync(outputPath, md, "utf-8");
170
+
171
+ console.log(`✓ ARCHITECTURE.md generado en ${outputPath}`);
172
+ console.log(` Score: ${auditScore}/100 | Violaciones: ${graph.stats.violations} | Risk: ${graph.stats.riskScore}`);
173
+ console.log(` Platform: ${ctx.platform.components.length} | Features: ${ctx.features.migrated.length} | Shared: ${ctx.shared.components.length} | Infra: ${ctx.infra.components.length}`);
174
+ }
175
+
176
+ main().catch(console.error);
@@ -0,0 +1,421 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, existsSync, readdirSync, statSync } from "fs";
4
+ import { join, relative } from "path";
5
+
6
+ const ROOT = process.cwd();
7
+ const SRC = join(ROOT, "src");
8
+
9
+ const PLATFORM_DIRS = ["config", "database", "http", "server", "logger", "cache", "security", "events", "scheduler", "observability", "di"];
10
+
11
+ const INFRA_DIRS = ["prisma", "mongodb", "postgres", "redis", "mail", "s3", "cloudinary", "stripe", "sqs", "rabbitmq", "kafka", "smtp"];
12
+
13
+ const SHARED_DIRS = ["errors", "contracts", "types", "utils", "helpers", "constants", "enums"];
14
+
15
+ const OWNERSHIP = {
16
+ platform: ["server", "http", "config", "env", "database", "logger", "cache", "security", "scheduler", "event bus", "observability", "DI"],
17
+ features: ["auth", "users", "payments", "inventory", "orders"],
18
+ shared: ["errors", "contracts", "utils", "types"],
19
+ infra: ["prisma", "mongodb", "postgres", "redis", "smtp", "storage providers"],
20
+ };
21
+
22
+ function read(path) {
23
+ try {
24
+ return readFileSync(path, "utf-8");
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ function isDir(path) {
31
+ return existsSync(path) && statSync(path).isDirectory();
32
+ }
33
+
34
+ function listDir(path) {
35
+ try {
36
+ return readdirSync(path);
37
+ } catch {
38
+ return [];
39
+ }
40
+ }
41
+
42
+ function findFiles(dir, exts = [".ts", ".js", ".tsx", ".jsx"], maxDepth = 6) {
43
+ const results = [];
44
+ function walk(d, depth) {
45
+ if (depth > maxDepth) return;
46
+ try {
47
+ for (const entry of readdirSync(d)) {
48
+ const full = join(d, entry);
49
+ if (statSync(full).isDirectory()) {
50
+ walk(full, depth + 1);
51
+ } else if (exts.length === 0 || exts.some((e) => entry.endsWith(e))) {
52
+ results.push(full);
53
+ }
54
+ }
55
+ } catch {}
56
+ }
57
+ if (existsSync(dir)) walk(dir, 0);
58
+ return results;
59
+ }
60
+
61
+ function classifyPath(relPath) {
62
+ const parts = relPath.split("/");
63
+ if (parts.includes("platform")) return "platform";
64
+ if (parts.includes("features")) return "feature";
65
+ if (parts.includes("shared")) return "shared";
66
+ if (parts.includes("infra") || parts.includes("infrastructure")) return "infra";
67
+ if (parts.includes("core")) return "shared";
68
+ if (parts.includes("lib")) return "shared";
69
+ return null;
70
+ }
71
+
72
+ export function detectOrphans(projectRoot = ROOT) {
73
+ const src = join(projectRoot, "src");
74
+ if (!isDir(src)) return [];
75
+
76
+ const knownLayers = new Set(["platform", "features", "shared", "infra", "infrastructure", "core", "lib"]);
77
+ const orphans = [];
78
+
79
+ for (const entry of listDir(src)) {
80
+ const full = join(src, entry);
81
+ if (!statSync(full).isDirectory()) continue;
82
+ if (knownLayers.has(entry)) continue;
83
+ if (entry.startsWith(".")) continue;
84
+ orphans.push({
85
+ path: `src/${entry}/`,
86
+ name: entry,
87
+ reason: "Directorio no pertenece a ningún layer arquitectónico",
88
+ suggestion: `Mover a src/platform/, src/shared/, src/infra/ o src/features/<name>/`,
89
+ });
90
+ }
91
+
92
+ return orphans;
93
+ }
94
+
95
+ export function assignOwners(projectRoot = ROOT) {
96
+ const src = join(projectRoot, "src");
97
+ const result = {
98
+ platform: [],
99
+ features: [],
100
+ shared: [],
101
+ infra: [],
102
+ };
103
+
104
+ const platformDir = join(src, "platform");
105
+ if (isDir(platformDir)) {
106
+ for (const dir of listDir(platformDir)) {
107
+ if (isDir(join(platformDir, dir)) && !dir.startsWith(".")) {
108
+ result.platform.push(dir);
109
+ }
110
+ }
111
+ const platformFiles = findFiles(platformDir, [".ts", ".js"], 1);
112
+ for (const f of platformFiles) {
113
+ const name = f.replace(platformDir + "/", "").replace(/\.(ts|js)$/, "");
114
+ if (!result.platform.includes(name)) {
115
+ result.platform.push(name);
116
+ }
117
+ }
118
+ }
119
+
120
+ const featuresDir = join(src, "features");
121
+ if (isDir(featuresDir)) {
122
+ for (const dir of listDir(featuresDir)) {
123
+ if (isDir(join(featuresDir, dir)) && !dir.startsWith(".")) {
124
+ result.features.push(dir);
125
+ }
126
+ }
127
+ }
128
+
129
+ const sharedDir = join(src, "shared");
130
+ if (isDir(sharedDir)) {
131
+ for (const dir of listDir(sharedDir)) {
132
+ if (isDir(join(sharedDir, dir)) && !dir.startsWith(".")) {
133
+ result.shared.push(dir);
134
+ }
135
+ }
136
+ const sharedFiles = findFiles(sharedDir, [".ts", ".js"], 1);
137
+ for (const f of sharedFiles) {
138
+ const name = f.replace(sharedDir + "/", "").replace(/\.(ts|js)$/, "");
139
+ if (!result.shared.includes(name)) {
140
+ result.shared.push(name);
141
+ }
142
+ }
143
+ }
144
+
145
+ const infraDirs = ["infra", "infrastructure"];
146
+ for (const infraName of infraDirs) {
147
+ const infraDir = join(src, infraName);
148
+ if (isDir(infraDir)) {
149
+ for (const dir of listDir(infraDir)) {
150
+ if (isDir(join(infraDir, dir)) && !dir.startsWith(".")) {
151
+ result.infra.push(dir);
152
+ }
153
+ }
154
+ const infraFiles = findFiles(infraDir, [".ts", ".js"], 1);
155
+ for (const f of infraFiles) {
156
+ const name = f.replace(infraDir + "/", "").replace(/\.(ts|js)$/, "");
157
+ if (!result.infra.includes(name)) {
158
+ result.infra.push(name);
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ return result;
165
+ }
166
+
167
+ export function detectDuplicates(ownership) {
168
+ const duplicates = [];
169
+ const seen = new Map();
170
+
171
+ for (const [layer, components] of Object.entries(ownership)) {
172
+ for (const comp of components) {
173
+ if (seen.has(comp)) {
174
+ duplicates.push({
175
+ name: comp,
176
+ layers: [seen.get(comp), layer],
177
+ });
178
+ } else {
179
+ seen.set(comp, layer);
180
+ }
181
+ }
182
+ }
183
+
184
+ return duplicates;
185
+ }
186
+
187
+ export function detectMisplaced(projectRoot = ROOT) {
188
+ const src = join(projectRoot, "src");
189
+ if (!isDir(src)) return [];
190
+
191
+ const misplaced = [];
192
+ const allFiles = findFiles(src, [".ts", ".js", ".tsx", ".jsx"], 8);
193
+
194
+ for (const file of allFiles) {
195
+ const relPath = relative(projectRoot, file);
196
+ const content = read(file);
197
+ if (!content) continue;
198
+
199
+ const platformPattern = /from\s+['"](\.\.\/)*platform\//;
200
+ const featurePattern = /from\s+['"](\.\.\/)*features\//;
201
+ const sharedPattern = /from\s+['"](\.\.\/)*shared\//;
202
+ const infraPattern = /from\s+['"](\.\.\/)*infra\//;
203
+
204
+ const isFeatureFile = relPath.includes("features/");
205
+ const isPlatformFile = relPath.includes("platform/");
206
+ const isSharedFile = relPath.includes("shared/");
207
+ const isInfraFile = relPath.includes("infra/") || relPath.includes("infrastructure/");
208
+ const isDomainFile = relPath.includes("/domain/");
209
+
210
+ if (isDomainFile && infraPattern.test(content)) {
211
+ misplaced.push({
212
+ file: relPath,
213
+ reason: "Domain importa de infraestructura — violación de DIP",
214
+ suggestion: "Extraer interfaz en domain/ y mover implementación a infra/ o adapters/out/",
215
+ });
216
+ }
217
+
218
+ if (isDomainFile && platformPattern.test(content)) {
219
+ misplaced.push({
220
+ file: relPath,
221
+ reason: "Domain importa de platform — el dominio debe ser puro",
222
+ suggestion: "Inyectar dependencia vía interfaz en lugar de importar platform directamente",
223
+ });
224
+ }
225
+
226
+ if (isSharedFile && infraPattern.test(content)) {
227
+ misplaced.push({
228
+ file: relPath,
229
+ reason: "Shared importa de infraestructura — shared debe ser puro",
230
+ suggestion: "Mover código que depende de infra a un adapter o a infra/",
231
+ });
232
+ }
233
+
234
+ if (isSharedFile && featurePattern.test(content)) {
235
+ misplaced.push({
236
+ file: relPath,
237
+ reason: "Shared importa de features — shared no debe conocer lógica de negocio",
238
+ suggestion: "Mover la dependencia de feature a un adapter o refactorizar",
239
+ });
240
+ }
241
+
242
+ if (isFeatureFile && !relPath.includes("/adapters/") && infraPattern.test(content)) {
243
+ misplaced.push({
244
+ file: relPath,
245
+ reason: "Feature importa infraestructura fuera de adapters",
246
+ suggestion: "Envolver en un adapter dentro de features/<name>/adapters/out/",
247
+ });
248
+ }
249
+ }
250
+
251
+ return misplaced;
252
+ }
253
+
254
+ export function suggestRelocation(ownership, orphans) {
255
+ const suggestions = [];
256
+ const knownPlatform = new Set(PLATFORM_DIRS);
257
+ const knownInfra = new Set(INFRA_DIRS);
258
+ const knownShared = new Set(SHARED_DIRS);
259
+
260
+ for (const orphan of orphans) {
261
+ const name = orphan.name.toLowerCase();
262
+
263
+ if (knownPlatform.has(name)) {
264
+ suggestions.push({
265
+ from: orphan.path,
266
+ to: `src/platform/${name}/`,
267
+ reason: `${name} es un componente de plataforma`,
268
+ });
269
+ } else if (knownInfra.has(name)) {
270
+ suggestions.push({
271
+ from: orphan.path,
272
+ to: `src/infra/${name}/`,
273
+ reason: `${name} es infraestructura`,
274
+ });
275
+ } else if (knownShared.has(name)) {
276
+ suggestions.push({
277
+ from: orphan.path,
278
+ to: `src/shared/${name}/`,
279
+ reason: `${name} es un componente compartido`,
280
+ });
281
+ } else {
282
+ suggestions.push({
283
+ from: orphan.path,
284
+ to: false,
285
+ reason: `No se pudo determinar el destino para ${name}`,
286
+ });
287
+ }
288
+ }
289
+
290
+ return suggestions;
291
+ }
292
+
293
+ export function buildOwnershipReport(projectRoot = ROOT) {
294
+ const src = join(projectRoot, "src");
295
+
296
+ const ownership = assignOwners(projectRoot);
297
+ const orphans = detectOrphans(projectRoot);
298
+ const duplicates = detectDuplicates(ownership);
299
+ const misplaced = detectMisplaced(projectRoot);
300
+ const relocations = suggestRelocation(ownership, orphans);
301
+
302
+ let health = "healthy";
303
+ let issues = 0;
304
+
305
+ if (orphans.length > 0) issues += orphans.length * 3;
306
+ if (duplicates.length > 0) issues += duplicates.length * 2;
307
+ if (misplaced.length > 0) issues += misplaced.length * 2;
308
+
309
+ if (issues > 10) health = "critical";
310
+ else if (issues > 3) health = "degraded";
311
+
312
+ return {
313
+ ownership,
314
+ orphans,
315
+ duplicates,
316
+ misplaced,
317
+ relocations,
318
+ health,
319
+ score: Math.max(0, 100 - issues * 5),
320
+ hasPlatform: ownership.platform.length > 0,
321
+ hasFeatures: ownership.features.length > 0,
322
+ hasShared: ownership.shared.length > 0,
323
+ hasInfra: ownership.infra.length > 0,
324
+ };
325
+ }
326
+
327
+ function printReport(report) {
328
+ const GREEN = "\x1b[32m";
329
+ const RED = "\x1b[31m";
330
+ const YELLOW = "\x1b[33m";
331
+ const BOLD = "\x1b[1m";
332
+ const RESET = "\x1b[0m";
333
+ const DIM = "\x1b[2m";
334
+ const CYAN = "\x1b[36m";
335
+
336
+ console.log(`\n${BOLD}${CYAN} Ownership Report${RESET}`);
337
+ console.log(` Health: ${report.health === "healthy" ? GREEN : report.health === "degraded" ? YELLOW : RED}${report.health}${RESET} | Score: ${report.score}/100\n`);
338
+
339
+ console.log(` ${BOLD}Platform${RESET}`);
340
+ if (report.ownership.platform.length > 0) {
341
+ for (const c of report.ownership.platform) console.log(` ${GREEN}✔${RESET} ${c}`);
342
+ } else {
343
+ console.log(` ${DIM}(none detected)${RESET}`);
344
+ }
345
+
346
+ console.log(`\n ${BOLD}Features${RESET}`);
347
+ if (report.ownership.features.length > 0) {
348
+ for (const c of report.ownership.features) console.log(` ${GREEN}✔${RESET} ${c}`);
349
+ } else {
350
+ console.log(` ${DIM}(none detected)${RESET}`);
351
+ }
352
+
353
+ console.log(`\n ${BOLD}Shared${RESET}`);
354
+ if (report.ownership.shared.length > 0) {
355
+ for (const c of report.ownership.shared) console.log(` ${GREEN}✔${RESET} ${c}`);
356
+ } else {
357
+ console.log(` ${DIM}(none detected)${RESET}`);
358
+ }
359
+
360
+ console.log(`\n ${BOLD}Infrastructure${RESET}`);
361
+ if (report.ownership.infra.length > 0) {
362
+ for (const c of report.ownership.infra) console.log(` ${GREEN}✔${RESET} ${c}`);
363
+ } else {
364
+ console.log(` ${DIM}(none detected)${RESET}`);
365
+ }
366
+
367
+ if (report.orphans.length > 0) {
368
+ console.log(`\n ${BOLD}${YELLOW}Orphans${RESET}`);
369
+ for (const o of report.orphans) {
370
+ console.log(` ${YELLOW}⚠${RESET} ${o.path} — ${o.reason}`);
371
+ console.log(` ${DIM}→ ${o.suggestion}${RESET}`);
372
+ }
373
+ }
374
+
375
+ if (report.duplicates.length > 0) {
376
+ console.log(`\n ${BOLD}${RED}Duplicates${RESET}`);
377
+ for (const d of report.duplicates) {
378
+ console.log(` ${RED}✘${RESET} "${d.name}" appears in: [${d.layers.join(", ")}]`);
379
+ }
380
+ }
381
+
382
+ if (report.misplaced.length > 0) {
383
+ console.log(`\n ${BOLD}${RED}Misplaced${RESET}`);
384
+ for (const m of report.misplaced) {
385
+ console.log(` ${RED}✘${RESET} ${m.file}`);
386
+ console.log(` ${DIM}→ ${m.reason}${RESET}`);
387
+ console.log(` ${DIM}→ Fix: ${m.suggestion}${RESET}`);
388
+ }
389
+ }
390
+
391
+ if (report.relocations.length > 0) {
392
+ console.log(`\n ${BOLD}Suggested Relocations${RESET}`);
393
+ for (const r of report.relocations) {
394
+ if (r.to) {
395
+ console.log(` ${DIM}→ Move ${r.from} → ${r.to} (${r.reason})${RESET}`);
396
+ } else {
397
+ console.log(` ${DIM}→ ${r.from}: ${r.reason}${RESET}`);
398
+ }
399
+ }
400
+ }
401
+
402
+ console.log();
403
+ }
404
+
405
+ async function main() {
406
+ const args = process.argv.slice(2);
407
+ const format = args.includes("--json") ? "json" : "text";
408
+ const report = buildOwnershipReport();
409
+
410
+ if (format === "json") {
411
+ console.log(JSON.stringify(report, null, 2));
412
+ } else {
413
+ printReport(report);
414
+ }
415
+ }
416
+
417
+ if (process.argv[1] && (process.argv[1].endsWith("armorer.mjs") || process.argv[1].endsWith("armorer.js"))) {
418
+ main().catch(console.error);
419
+ }
420
+
421
+