@stackweld/core 0.2.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 (172) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-lint.log +498 -0
  3. package/.turbo/turbo-test.log +21 -0
  4. package/.turbo/turbo-typecheck.log +4 -0
  5. package/dist/__tests__/compatibility-scorer.test.d.ts +2 -0
  6. package/dist/__tests__/compatibility-scorer.test.d.ts.map +1 -0
  7. package/dist/__tests__/compatibility-scorer.test.js +226 -0
  8. package/dist/__tests__/compatibility-scorer.test.js.map +1 -0
  9. package/dist/__tests__/rules-engine.test.d.ts +2 -0
  10. package/dist/__tests__/rules-engine.test.d.ts.map +1 -0
  11. package/dist/__tests__/rules-engine.test.js +161 -0
  12. package/dist/__tests__/rules-engine.test.js.map +1 -0
  13. package/dist/__tests__/scaffold-orchestrator.test.d.ts +2 -0
  14. package/dist/__tests__/scaffold-orchestrator.test.d.ts.map +1 -0
  15. package/dist/__tests__/scaffold-orchestrator.test.js +149 -0
  16. package/dist/__tests__/scaffold-orchestrator.test.js.map +1 -0
  17. package/dist/__tests__/stack-engine.test.d.ts +2 -0
  18. package/dist/__tests__/stack-engine.test.d.ts.map +1 -0
  19. package/dist/__tests__/stack-engine.test.js +278 -0
  20. package/dist/__tests__/stack-engine.test.js.map +1 -0
  21. package/dist/db/database.d.ts +9 -0
  22. package/dist/db/database.d.ts.map +1 -0
  23. package/dist/db/database.js +106 -0
  24. package/dist/db/database.js.map +1 -0
  25. package/dist/db/index.d.ts +2 -0
  26. package/dist/db/index.d.ts.map +1 -0
  27. package/dist/db/index.js +2 -0
  28. package/dist/db/index.js.map +1 -0
  29. package/dist/engine/compatibility-scorer.d.ts +37 -0
  30. package/dist/engine/compatibility-scorer.d.ts.map +1 -0
  31. package/dist/engine/compatibility-scorer.js +178 -0
  32. package/dist/engine/compatibility-scorer.js.map +1 -0
  33. package/dist/engine/compose-generator.d.ts +35 -0
  34. package/dist/engine/compose-generator.d.ts.map +1 -0
  35. package/dist/engine/compose-generator.js +95 -0
  36. package/dist/engine/compose-generator.js.map +1 -0
  37. package/dist/engine/cost-estimator.d.ts +22 -0
  38. package/dist/engine/cost-estimator.d.ts.map +1 -0
  39. package/dist/engine/cost-estimator.js +451 -0
  40. package/dist/engine/cost-estimator.js.map +1 -0
  41. package/dist/engine/env-analyzer.d.ts +36 -0
  42. package/dist/engine/env-analyzer.d.ts.map +1 -0
  43. package/dist/engine/env-analyzer.js +111 -0
  44. package/dist/engine/env-analyzer.js.map +1 -0
  45. package/dist/engine/health-checker.d.ts +20 -0
  46. package/dist/engine/health-checker.d.ts.map +1 -0
  47. package/dist/engine/health-checker.js +377 -0
  48. package/dist/engine/health-checker.js.map +1 -0
  49. package/dist/engine/index.d.ts +11 -0
  50. package/dist/engine/index.d.ts.map +1 -0
  51. package/dist/engine/index.js +7 -0
  52. package/dist/engine/index.js.map +1 -0
  53. package/dist/engine/infra-generator.d.ts +26 -0
  54. package/dist/engine/infra-generator.d.ts.map +1 -0
  55. package/dist/engine/infra-generator.js +751 -0
  56. package/dist/engine/infra-generator.js.map +1 -0
  57. package/dist/engine/migration-planner.d.ts +34 -0
  58. package/dist/engine/migration-planner.d.ts.map +1 -0
  59. package/dist/engine/migration-planner.js +427 -0
  60. package/dist/engine/migration-planner.js.map +1 -0
  61. package/dist/engine/performance-profiler.d.ts +22 -0
  62. package/dist/engine/performance-profiler.d.ts.map +1 -0
  63. package/dist/engine/performance-profiler.js +292 -0
  64. package/dist/engine/performance-profiler.js.map +1 -0
  65. package/dist/engine/plugin-loader.d.ts +36 -0
  66. package/dist/engine/plugin-loader.d.ts.map +1 -0
  67. package/dist/engine/plugin-loader.js +157 -0
  68. package/dist/engine/plugin-loader.js.map +1 -0
  69. package/dist/engine/preferences.d.ts +24 -0
  70. package/dist/engine/preferences.d.ts.map +1 -0
  71. package/dist/engine/preferences.js +62 -0
  72. package/dist/engine/preferences.js.map +1 -0
  73. package/dist/engine/rules-engine.d.ts +31 -0
  74. package/dist/engine/rules-engine.d.ts.map +1 -0
  75. package/dist/engine/rules-engine.js +179 -0
  76. package/dist/engine/rules-engine.js.map +1 -0
  77. package/dist/engine/runtime-manager.d.ts +65 -0
  78. package/dist/engine/runtime-manager.d.ts.map +1 -0
  79. package/dist/engine/runtime-manager.js +181 -0
  80. package/dist/engine/runtime-manager.js.map +1 -0
  81. package/dist/engine/scaffold-orchestrator.d.ts +103 -0
  82. package/dist/engine/scaffold-orchestrator.d.ts.map +1 -0
  83. package/dist/engine/scaffold-orchestrator.js +934 -0
  84. package/dist/engine/scaffold-orchestrator.js.map +1 -0
  85. package/dist/engine/stack-detector.d.ts +21 -0
  86. package/dist/engine/stack-detector.d.ts.map +1 -0
  87. package/dist/engine/stack-detector.js +313 -0
  88. package/dist/engine/stack-detector.js.map +1 -0
  89. package/dist/engine/stack-differ.d.ts +26 -0
  90. package/dist/engine/stack-differ.d.ts.map +1 -0
  91. package/dist/engine/stack-differ.js +80 -0
  92. package/dist/engine/stack-differ.js.map +1 -0
  93. package/dist/engine/stack-engine.d.ts +54 -0
  94. package/dist/engine/stack-engine.d.ts.map +1 -0
  95. package/dist/engine/stack-engine.js +186 -0
  96. package/dist/engine/stack-engine.js.map +1 -0
  97. package/dist/engine/stack-serializer.d.ts +32 -0
  98. package/dist/engine/stack-serializer.d.ts.map +1 -0
  99. package/dist/engine/stack-serializer.js +75 -0
  100. package/dist/engine/stack-serializer.js.map +1 -0
  101. package/dist/engine/standards-linter.d.ts +34 -0
  102. package/dist/engine/standards-linter.d.ts.map +1 -0
  103. package/dist/engine/standards-linter.js +162 -0
  104. package/dist/engine/standards-linter.js.map +1 -0
  105. package/dist/engine/tech-installer.d.ts +37 -0
  106. package/dist/engine/tech-installer.d.ts.map +1 -0
  107. package/dist/engine/tech-installer.js +508 -0
  108. package/dist/engine/tech-installer.js.map +1 -0
  109. package/dist/index.d.ts +39 -0
  110. package/dist/index.d.ts.map +1 -0
  111. package/dist/index.js +25 -0
  112. package/dist/index.js.map +1 -0
  113. package/dist/types/index.d.ts +6 -0
  114. package/dist/types/index.d.ts.map +1 -0
  115. package/dist/types/index.js +2 -0
  116. package/dist/types/index.js.map +1 -0
  117. package/dist/types/project.d.ts +33 -0
  118. package/dist/types/project.d.ts.map +1 -0
  119. package/dist/types/project.js +6 -0
  120. package/dist/types/project.js.map +1 -0
  121. package/dist/types/stack.d.ts +29 -0
  122. package/dist/types/stack.d.ts.map +1 -0
  123. package/dist/types/stack.js +6 -0
  124. package/dist/types/stack.js.map +1 -0
  125. package/dist/types/technology.d.ts +47 -0
  126. package/dist/types/technology.d.ts.map +1 -0
  127. package/dist/types/technology.js +6 -0
  128. package/dist/types/technology.js.map +1 -0
  129. package/dist/types/template.d.ts +34 -0
  130. package/dist/types/template.d.ts.map +1 -0
  131. package/dist/types/template.js +6 -0
  132. package/dist/types/template.js.map +1 -0
  133. package/dist/types/validation.d.ts +20 -0
  134. package/dist/types/validation.d.ts.map +1 -0
  135. package/dist/types/validation.js +5 -0
  136. package/dist/types/validation.js.map +1 -0
  137. package/package.json +39 -0
  138. package/src/__tests__/compatibility-scorer.test.ts +264 -0
  139. package/src/__tests__/rules-engine.test.ts +170 -0
  140. package/src/__tests__/scaffold-orchestrator.test.ts +161 -0
  141. package/src/__tests__/stack-engine.test.ts +328 -0
  142. package/src/db/database.ts +112 -0
  143. package/src/db/index.ts +1 -0
  144. package/src/engine/compatibility-scorer.ts +222 -0
  145. package/src/engine/compose-generator.ts +134 -0
  146. package/src/engine/cost-estimator.ts +498 -0
  147. package/src/engine/env-analyzer.ts +156 -0
  148. package/src/engine/health-checker.ts +421 -0
  149. package/src/engine/index.ts +17 -0
  150. package/src/engine/infra-generator.ts +837 -0
  151. package/src/engine/migration-planner.ts +496 -0
  152. package/src/engine/performance-profiler.ts +354 -0
  153. package/src/engine/plugin-loader.ts +216 -0
  154. package/src/engine/preferences.ts +85 -0
  155. package/src/engine/rules-engine.ts +204 -0
  156. package/src/engine/runtime-manager.ts +207 -0
  157. package/src/engine/scaffold-orchestrator.ts +1052 -0
  158. package/src/engine/stack-detector.ts +345 -0
  159. package/src/engine/stack-differ.ts +118 -0
  160. package/src/engine/stack-engine.ts +258 -0
  161. package/src/engine/stack-serializer.ts +95 -0
  162. package/src/engine/standards-linter.ts +210 -0
  163. package/src/engine/tech-installer.ts +650 -0
  164. package/src/index.ts +78 -0
  165. package/src/types/index.ts +10 -0
  166. package/src/types/project.ts +36 -0
  167. package/src/types/stack.ts +32 -0
  168. package/src/types/technology.ts +58 -0
  169. package/src/types/template.ts +37 -0
  170. package/src/types/validation.ts +22 -0
  171. package/tsconfig.json +10 -0
  172. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Stack Engine — CRUD operations for stack definitions.
3
+ * Handles creation, validation, versioning, and persistence.
4
+ */
5
+
6
+ import { randomUUID } from "node:crypto";
7
+ import { getDatabase } from "../db/database.js";
8
+ import type {
9
+ StackDefinition,
10
+ StackProfile,
11
+ StackTechnology,
12
+ StackVersion,
13
+ ValidationResult,
14
+ } from "../types/index.js";
15
+ import type { RulesEngine } from "./rules-engine.js";
16
+
17
+ export class StackEngine {
18
+ private rules: RulesEngine;
19
+
20
+ constructor(rules: RulesEngine) {
21
+ this.rules = rules;
22
+ }
23
+
24
+ /**
25
+ * Create a new stack definition.
26
+ * Validates technologies and auto-resolves dependencies.
27
+ */
28
+ create(opts: {
29
+ name: string;
30
+ description?: string;
31
+ profile?: StackProfile;
32
+ technologies: StackTechnology[];
33
+ tags?: string[];
34
+ }): { stack: StackDefinition; validation: ValidationResult } {
35
+ const validation = this.rules.validate(opts.technologies);
36
+
37
+ // Add auto-resolved dependencies
38
+ const allTechs = [...opts.technologies];
39
+ for (const depId of validation.resolvedDependencies) {
40
+ const tech = this.rules.getTechnology(depId);
41
+ if (tech) {
42
+ allTechs.push({
43
+ technologyId: depId,
44
+ version: tech.defaultVersion,
45
+ port: validation.portAssignments[depId],
46
+ });
47
+ }
48
+ }
49
+
50
+ // Apply port assignments to existing techs
51
+ for (const t of allTechs) {
52
+ if (validation.portAssignments[t.technologyId]) {
53
+ t.port = validation.portAssignments[t.technologyId];
54
+ }
55
+ }
56
+
57
+ const now = new Date().toISOString();
58
+ const stack: StackDefinition = {
59
+ id: randomUUID(),
60
+ name: opts.name,
61
+ description: opts.description || "",
62
+ profile: opts.profile || "standard",
63
+ technologies: allTechs,
64
+ createdAt: now,
65
+ updatedAt: now,
66
+ version: 1,
67
+ tags: opts.tags || [],
68
+ };
69
+
70
+ if (validation.valid) {
71
+ this.persist(stack);
72
+ }
73
+
74
+ return { stack, validation };
75
+ }
76
+
77
+ /**
78
+ * Get a stack by ID.
79
+ */
80
+ get(id: string): StackDefinition | null {
81
+ const db = getDatabase();
82
+ const row = db.prepare("SELECT * FROM stacks WHERE id = ?").get(id) as
83
+ | Record<string, unknown>
84
+ | undefined;
85
+ if (!row) return null;
86
+
87
+ const techRows = db
88
+ .prepare("SELECT * FROM stack_technologies WHERE stack_id = ?")
89
+ .all(id) as Record<string, unknown>[];
90
+
91
+ return {
92
+ id: row.id as string,
93
+ name: row.name as string,
94
+ description: row.description as string,
95
+ profile: row.profile as StackProfile,
96
+ version: row.version as number,
97
+ createdAt: row.created_at as string,
98
+ updatedAt: row.updated_at as string,
99
+ tags: JSON.parse(row.tags as string),
100
+ technologies: techRows.map((t) => ({
101
+ technologyId: t.technology_id as string,
102
+ version: t.version as string,
103
+ port: t.port as number | undefined,
104
+ config: JSON.parse((t.config as string) || "{}"),
105
+ })),
106
+ };
107
+ }
108
+
109
+ /**
110
+ * List all stacks.
111
+ */
112
+ list(): StackDefinition[] {
113
+ const db = getDatabase();
114
+ const rows = db.prepare("SELECT id FROM stacks ORDER BY updated_at DESC").all() as {
115
+ id: string;
116
+ }[];
117
+ return rows.map((r) => this.get(r.id)!).filter(Boolean);
118
+ }
119
+
120
+ /**
121
+ * Delete a stack.
122
+ */
123
+ delete(id: string): boolean {
124
+ const db = getDatabase();
125
+ const result = db.prepare("DELETE FROM stacks WHERE id = ?").run(id);
126
+ return result.changes > 0;
127
+ }
128
+
129
+ /**
130
+ * Update a stack. Auto-increments version and saves snapshot.
131
+ */
132
+ update(
133
+ id: string,
134
+ changes: Partial<
135
+ Pick<StackDefinition, "name" | "description" | "profile" | "technologies" | "tags">
136
+ >,
137
+ ): {
138
+ stack: StackDefinition;
139
+ validation: ValidationResult;
140
+ } | null {
141
+ const existing = this.get(id);
142
+ if (!existing) return null;
143
+
144
+ const techs = changes.technologies || existing.technologies;
145
+ const validation = this.rules.validate(techs);
146
+
147
+ const now = new Date().toISOString();
148
+ const newVersion = existing.version + 1;
149
+
150
+ const db = getDatabase();
151
+ db.prepare(`
152
+ UPDATE stacks SET
153
+ name = ?, description = ?, profile = ?, version = ?,
154
+ updated_at = ?, tags = ?
155
+ WHERE id = ?
156
+ `).run(
157
+ changes.name || existing.name,
158
+ changes.description ?? existing.description,
159
+ changes.profile || existing.profile,
160
+ newVersion,
161
+ now,
162
+ JSON.stringify(changes.tags || existing.tags),
163
+ id,
164
+ );
165
+
166
+ // Replace technologies
167
+ db.prepare("DELETE FROM stack_technologies WHERE stack_id = ?").run(id);
168
+ for (const t of techs) {
169
+ db.prepare(`
170
+ INSERT INTO stack_technologies (stack_id, technology_id, version, port, config)
171
+ VALUES (?, ?, ?, ?, ?)
172
+ `).run(id, t.technologyId, t.version, t.port || null, JSON.stringify(t.config || {}));
173
+ }
174
+
175
+ // Save version snapshot
176
+ const updatedStack = this.get(id)!;
177
+ this.saveVersionSnapshot(updatedStack, `Updated to v${newVersion}`);
178
+
179
+ return { stack: updatedStack, validation };
180
+ }
181
+
182
+ /**
183
+ * Get version history for a stack.
184
+ */
185
+ getVersionHistory(stackId: string): StackVersion[] {
186
+ const db = getDatabase();
187
+ const rows = db
188
+ .prepare("SELECT * FROM stack_versions WHERE stack_id = ? ORDER BY version DESC")
189
+ .all(stackId) as Record<string, unknown>[];
190
+
191
+ return rows.map((r) => ({
192
+ version: r.version as number,
193
+ timestamp: r.created_at as string,
194
+ changelog: r.changelog as string,
195
+ snapshot: JSON.parse(r.snapshot as string),
196
+ }));
197
+ }
198
+
199
+ /**
200
+ * Rollback to a specific version.
201
+ */
202
+ rollback(stackId: string, targetVersion: number): StackDefinition | null {
203
+ const db = getDatabase();
204
+ const versionRow = db
205
+ .prepare("SELECT snapshot FROM stack_versions WHERE stack_id = ? AND version = ?")
206
+ .get(stackId, targetVersion) as { snapshot: string } | undefined;
207
+
208
+ if (!versionRow) return null;
209
+
210
+ const snapshot = JSON.parse(versionRow.snapshot) as StackDefinition;
211
+ this.update(stackId, {
212
+ name: snapshot.name,
213
+ description: snapshot.description,
214
+ profile: snapshot.profile,
215
+ technologies: snapshot.technologies,
216
+ tags: snapshot.tags,
217
+ });
218
+
219
+ return this.get(stackId);
220
+ }
221
+
222
+ // ─── Private ────────────────────────────────────────────
223
+
224
+ private persist(stack: StackDefinition): void {
225
+ const db = getDatabase();
226
+
227
+ db.prepare(`
228
+ INSERT INTO stacks (id, name, description, profile, version, created_at, updated_at, tags)
229
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
230
+ `).run(
231
+ stack.id,
232
+ stack.name,
233
+ stack.description,
234
+ stack.profile,
235
+ stack.version,
236
+ stack.createdAt,
237
+ stack.updatedAt,
238
+ JSON.stringify(stack.tags),
239
+ );
240
+
241
+ for (const t of stack.technologies) {
242
+ db.prepare(`
243
+ INSERT INTO stack_technologies (stack_id, technology_id, version, port, config)
244
+ VALUES (?, ?, ?, ?, ?)
245
+ `).run(stack.id, t.technologyId, t.version, t.port || null, JSON.stringify(t.config || {}));
246
+ }
247
+
248
+ this.saveVersionSnapshot(stack, "Initial creation");
249
+ }
250
+
251
+ private saveVersionSnapshot(stack: StackDefinition, changelog: string): void {
252
+ const db = getDatabase();
253
+ db.prepare(`
254
+ INSERT OR REPLACE INTO stack_versions (stack_id, version, changelog, snapshot)
255
+ VALUES (?, ?, ?, ?)
256
+ `).run(stack.id, stack.version, changelog, JSON.stringify(stack));
257
+ }
258
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Stack Serializer — Compress/decompress stack definitions into URL-safe strings.
3
+ * Uses zlib deflate + base64url encoding. No server needed.
4
+ */
5
+
6
+ import { deflateSync, inflateSync } from "node:zlib";
7
+
8
+ export interface ShareableStack {
9
+ name: string;
10
+ profile: string;
11
+ technologies: Array<{ id: string; version?: string; port?: number }>;
12
+ }
13
+
14
+ const DEFAULT_BASE_URL = "https://stackweld.dev/s/#";
15
+
16
+ /**
17
+ * Compress a stack into a URL-safe base64 string.
18
+ * Flow: JSON → deflate → base64url
19
+ */
20
+ export function serializeStack(stack: ShareableStack): string {
21
+ const json = JSON.stringify({
22
+ n: stack.name,
23
+ p: stack.profile,
24
+ t: stack.technologies.map((t) => {
25
+ const entry: Record<string, unknown> = { i: t.id };
26
+ if (t.version) entry.v = t.version;
27
+ if (t.port) entry.p = t.port;
28
+ return entry;
29
+ }),
30
+ });
31
+
32
+ const compressed = deflateSync(Buffer.from(json, "utf-8"));
33
+ return toBase64Url(compressed);
34
+ }
35
+
36
+ /**
37
+ * Decompress from a base64url string back to a stack definition.
38
+ * Flow: base64url → inflate → JSON
39
+ */
40
+ export function deserializeStack(encoded: string): ShareableStack {
41
+ const buffer = fromBase64Url(encoded);
42
+ const json = inflateSync(buffer).toString("utf-8");
43
+
44
+ const data = JSON.parse(json) as {
45
+ n: string;
46
+ p: string;
47
+ t: Array<{ i: string; v?: string; p?: number }>;
48
+ };
49
+
50
+ return {
51
+ name: data.n,
52
+ profile: data.p,
53
+ technologies: data.t.map((t) => ({
54
+ id: t.i,
55
+ version: t.v,
56
+ port: t.p,
57
+ })),
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Generate a full shareable URL for a stack.
63
+ */
64
+ export function generateShareUrl(
65
+ stack: ShareableStack,
66
+ baseUrl: string = DEFAULT_BASE_URL,
67
+ ): string {
68
+ return `${baseUrl}${serializeStack(stack)}`;
69
+ }
70
+
71
+ /**
72
+ * Extract the encoded payload from a share URL.
73
+ */
74
+ export function extractFromShareUrl(url: string): string {
75
+ const hashIndex = url.indexOf("#");
76
+ if (hashIndex === -1) {
77
+ throw new Error("Invalid share URL: no hash fragment found.");
78
+ }
79
+ return url.substring(hashIndex + 1);
80
+ }
81
+
82
+ // ─── Base64url helpers ────────────────────────────────
83
+
84
+ function toBase64Url(buffer: Buffer): string {
85
+ return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
86
+ }
87
+
88
+ function fromBase64Url(str: string): Buffer {
89
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
90
+ // Add padding
91
+ const pad = base64.length % 4;
92
+ if (pad === 2) base64 += "==";
93
+ else if (pad === 3) base64 += "=";
94
+ return Buffer.from(base64, "base64");
95
+ }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Stack Standards Linter — Validates stacks against team-defined standards.
3
+ * Reads .stackweldrc config and checks compliance.
4
+ */
5
+
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+ import type { StackDefinition } from "../types/index.js";
9
+
10
+ // ─── Types ────────────────────────────────────────────
11
+
12
+ export interface StackStandards {
13
+ team?: string;
14
+ allowedTechnologies?: string[];
15
+ blockedTechnologies?: string[];
16
+ requiredTechnologies?: string[];
17
+ minProfile?: string;
18
+ requireDocker?: boolean;
19
+ requireTests?: boolean;
20
+ requireTypeScript?: boolean;
21
+ customRules?: Array<{ name: string; check: string; message: string }>;
22
+ }
23
+
24
+ export interface LintResult {
25
+ passed: boolean;
26
+ violations: LintViolation[];
27
+ warnings: LintWarning[];
28
+ }
29
+
30
+ export interface LintViolation {
31
+ rule: string;
32
+ message: string;
33
+ severity: "error" | "warning";
34
+ }
35
+
36
+ export type LintWarning = LintViolation;
37
+
38
+ // ─── Profile Ordering ─────────────────────────────────
39
+
40
+ const PROFILE_ORDER: Record<string, number> = {
41
+ lightweight: 0,
42
+ rapid: 1,
43
+ standard: 2,
44
+ production: 3,
45
+ enterprise: 4,
46
+ };
47
+
48
+ function profileLevel(profile: string): number {
49
+ return PROFILE_ORDER[profile] ?? -1;
50
+ }
51
+
52
+ // ─── Testing Frameworks ───────────────────────────────
53
+
54
+ const TESTING_FRAMEWORKS = new Set([
55
+ "vitest",
56
+ "jest",
57
+ "playwright",
58
+ "cypress",
59
+ "mocha",
60
+ "ava",
61
+ "tap",
62
+ "pytest",
63
+ "unittest",
64
+ "go-test",
65
+ "cargo-test",
66
+ ]);
67
+
68
+ // ─── Lint Function ────────────────────────────────────
69
+
70
+ export function lintStack(stack: StackDefinition, standards: StackStandards): LintResult {
71
+ const violations: LintViolation[] = [];
72
+ const warnings: LintWarning[] = [];
73
+
74
+ const techIds = new Set(stack.technologies.map((t) => t.technologyId));
75
+
76
+ // Allowed technologies check
77
+ if (standards.allowedTechnologies && standards.allowedTechnologies.length > 0) {
78
+ const allowed = new Set(standards.allowedTechnologies);
79
+ for (const id of techIds) {
80
+ if (!allowed.has(id)) {
81
+ violations.push({
82
+ rule: "allowedTechnologies",
83
+ message: `Technology "${id}" is not in the allowed list`,
84
+ severity: "error",
85
+ });
86
+ }
87
+ }
88
+ }
89
+
90
+ // Blocked technologies check
91
+ if (standards.blockedTechnologies && standards.blockedTechnologies.length > 0) {
92
+ for (const blocked of standards.blockedTechnologies) {
93
+ if (techIds.has(blocked)) {
94
+ violations.push({
95
+ rule: "blockedTechnologies",
96
+ message: `Blocked technology "${blocked}" found — remove it`,
97
+ severity: "error",
98
+ });
99
+ }
100
+ }
101
+ }
102
+
103
+ // Required technologies check
104
+ if (standards.requiredTechnologies && standards.requiredTechnologies.length > 0) {
105
+ for (const required of standards.requiredTechnologies) {
106
+ if (!techIds.has(required)) {
107
+ violations.push({
108
+ rule: "requiredTechnologies",
109
+ message: `Required technology "${required}" is missing`,
110
+ severity: "error",
111
+ });
112
+ }
113
+ }
114
+ }
115
+
116
+ // Minimum profile check
117
+ if (standards.minProfile) {
118
+ const minLevel = profileLevel(standards.minProfile);
119
+ const currentLevel = profileLevel(stack.profile);
120
+ if (minLevel >= 0 && currentLevel < minLevel) {
121
+ warnings.push({
122
+ rule: "minProfile",
123
+ message: `Profile "${stack.profile}" is below minimum "${standards.minProfile}"`,
124
+ severity: "warning",
125
+ });
126
+ }
127
+ }
128
+
129
+ // Require Docker
130
+ if (standards.requireDocker) {
131
+ if (!techIds.has("docker") && !techIds.has("docker-compose")) {
132
+ violations.push({
133
+ rule: "requireDocker",
134
+ message: "Docker is required but not present in the stack",
135
+ severity: "error",
136
+ });
137
+ }
138
+ }
139
+
140
+ // Require tests
141
+ if (standards.requireTests) {
142
+ const hasTestFramework = Array.from(techIds).some((id) => TESTING_FRAMEWORKS.has(id));
143
+ if (!hasTestFramework) {
144
+ warnings.push({
145
+ rule: "requireTests",
146
+ message: "No testing framework found (vitest, jest, playwright, cypress, etc.)",
147
+ severity: "warning",
148
+ });
149
+ }
150
+ }
151
+
152
+ // Require TypeScript
153
+ if (standards.requireTypeScript) {
154
+ if (!techIds.has("typescript")) {
155
+ violations.push({
156
+ rule: "requireTypeScript",
157
+ message: "TypeScript is required but not present in the stack",
158
+ severity: "error",
159
+ });
160
+ }
161
+ }
162
+
163
+ // Custom rules (simple presence/absence checks)
164
+ if (standards.customRules) {
165
+ for (const rule of standards.customRules) {
166
+ if (rule.check === "present") {
167
+ if (!techIds.has(rule.name)) {
168
+ violations.push({
169
+ rule: `custom:${rule.name}`,
170
+ message: rule.message,
171
+ severity: "error",
172
+ });
173
+ }
174
+ } else if (rule.check === "absent") {
175
+ if (techIds.has(rule.name)) {
176
+ violations.push({
177
+ rule: `custom:${rule.name}`,
178
+ message: rule.message,
179
+ severity: "error",
180
+ });
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ const hasErrors = violations.some((v) => v.severity === "error");
187
+
188
+ return {
189
+ passed: !hasErrors,
190
+ violations,
191
+ warnings,
192
+ };
193
+ }
194
+
195
+ // ─── Config Loader ────────────────────────────────────
196
+
197
+ export function loadStandards(projectPath: string): StackStandards | null {
198
+ const configPath = path.resolve(projectPath, ".stackweldrc");
199
+ if (!fs.existsSync(configPath)) {
200
+ return null;
201
+ }
202
+
203
+ try {
204
+ const content = fs.readFileSync(configPath, "utf-8");
205
+ const parsed = JSON.parse(content) as StackStandards;
206
+ return parsed;
207
+ } catch {
208
+ return null;
209
+ }
210
+ }