@ultrst-armada/krnl 1.0.1 → 1.0.2
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/package.json +2 -2
- package/packages/cli/dist/bin/krnl.cjs +1026 -9462
- package/packages/cli/dist/bin/krnl.js +14 -1859
- package/packages/cli/dist/chunk-ESTKOQ7T.js +1146 -0
- package/packages/cli/dist/chunk-FZ6X7PHR.js +1146 -0
- package/packages/cli/dist/index.cjs +735 -7348
- package/packages/cli/dist/index.js +1 -20
- package/packages/core/dist/index.cjs +137 -5
- package/packages/core/dist/index.d.cts +23 -1
- package/packages/core/dist/index.d.ts +23 -1
- package/packages/core/dist/index.js +135 -4
- package/packages/core/package.json +1 -1
- package/packages/create-krnl/dist/index.cjs +277 -308
- package/packages/create-krnl/dist/index.js +277 -308
- package/packages/validate/dist/index.cjs +407 -369
- package/packages/validate/dist/index.d.cts +1 -13
- package/packages/validate/dist/index.d.ts +1 -13
- package/packages/validate/dist/index.js +408 -370
|
@@ -1,453 +1,491 @@
|
|
|
1
1
|
// src/KrnlValidatorEngine.ts
|
|
2
|
-
import * as
|
|
3
|
-
import * as
|
|
4
|
-
import { Project
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} else {
|
|
21
|
-
this.project = new Project({ compilerOptions: {
|
|
22
|
-
target: 99
|
|
23
|
-
/* ESNext */
|
|
24
|
-
} });
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Scans and validates all source files in the project.
|
|
29
|
-
*/
|
|
30
|
-
validateAll() {
|
|
31
|
-
const files = this.collectSourceFiles(this.projectRoot);
|
|
32
|
-
const errors = [];
|
|
33
|
-
const warnings = [];
|
|
34
|
-
this.validateWorkspaceLayout(errors);
|
|
35
|
-
for (const f of files) {
|
|
36
|
-
const sf = this.project.addSourceFileAtPath(f);
|
|
37
|
-
const fileReport = this.validateSourceFile(sf);
|
|
38
|
-
errors.push(...fileReport.errors);
|
|
39
|
-
warnings.push(...fileReport.warnings);
|
|
40
|
-
}
|
|
41
|
-
return {
|
|
42
|
-
valid: errors.length === 0,
|
|
43
|
-
totalFiles: files.length,
|
|
44
|
-
errors,
|
|
45
|
-
warnings
|
|
46
|
-
};
|
|
2
|
+
import * as fs2 from "fs";
|
|
3
|
+
import * as path4 from "path";
|
|
4
|
+
import { Project } from "ts-morph";
|
|
5
|
+
|
|
6
|
+
// src/rules/fileLimits.ts
|
|
7
|
+
function checkFileLimits(sf, options) {
|
|
8
|
+
const errors = [];
|
|
9
|
+
const warnings = [];
|
|
10
|
+
const filePath = sf.getFilePath();
|
|
11
|
+
const sourceText = sf.getText();
|
|
12
|
+
const lines = sourceText.split("\n");
|
|
13
|
+
if (lines.length > options.maxFileLineLimit) {
|
|
14
|
+
errors.push({
|
|
15
|
+
ruleId: "krnl/file-line-limit",
|
|
16
|
+
filePath,
|
|
17
|
+
message: `File exceeds maximum length of ${options.maxFileLineLimit} lines (currently ${lines.length} lines). Decompose into sub-components, custom hooks, quad-units, or concept libraries.`,
|
|
18
|
+
severity: "error"
|
|
19
|
+
});
|
|
47
20
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
validateSourceFile(sf) {
|
|
52
|
-
const errors = [];
|
|
53
|
-
const warnings = [];
|
|
54
|
-
const filePath = sf.getFilePath();
|
|
55
|
-
const sourceText = sf.getText();
|
|
56
|
-
const baseName = path.basename(filePath);
|
|
57
|
-
if (baseName.includes("_pb.") || baseName.endsWith(".d.ts")) {
|
|
58
|
-
return { errors, warnings };
|
|
59
|
-
}
|
|
60
|
-
const lines = sourceText.split("\n");
|
|
61
|
-
if (lines.length > this.maxFileLineLimit) {
|
|
21
|
+
for (const decl of sf.getImportDeclarations()) {
|
|
22
|
+
const moduleSpec = decl.getModuleSpecifierValue();
|
|
23
|
+
if (moduleSpec.startsWith(".") && !moduleSpec.endsWith(".js") && !moduleSpec.endsWith(".json") && !moduleSpec.endsWith(".css") && !moduleSpec.endsWith(".svg") && !moduleSpec.endsWith(".png")) {
|
|
62
24
|
errors.push({
|
|
63
|
-
ruleId: "krnl/
|
|
25
|
+
ruleId: "krnl/import-suffix",
|
|
64
26
|
filePath,
|
|
65
|
-
|
|
27
|
+
line: decl.getStartLineNumber(),
|
|
28
|
+
message: `Relative import '${moduleSpec}' must end with '.js' for NodeNext module resolution.`,
|
|
66
29
|
severity: "error"
|
|
67
30
|
});
|
|
68
31
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
32
|
+
}
|
|
33
|
+
const isHelper = filePath.includes("helper_functions") || filePath.includes("/helpers/") || filePath.includes("\\helpers\\");
|
|
34
|
+
if (isHelper) {
|
|
35
|
+
for (const fn of sf.getFunctions()) {
|
|
36
|
+
const fnLines = fn.getText().split("\n").length;
|
|
37
|
+
if (fnLines > options.maxHelperLineLimit && !sourceText.includes("Concepts.")) {
|
|
73
38
|
errors.push({
|
|
74
|
-
ruleId: "krnl/
|
|
39
|
+
ruleId: "krnl/helper-line-limit",
|
|
75
40
|
filePath,
|
|
76
|
-
line,
|
|
77
|
-
message: `
|
|
41
|
+
line: fn.getStartLineNumber(),
|
|
42
|
+
message: `Helper function '${fn.getName() || "anonymous"}' exceeds ${options.maxHelperLineLimit} lines (${fnLines} lines). Must delegate to a root libraries/{Domain}Concepts.ts library.`,
|
|
78
43
|
severity: "error"
|
|
79
44
|
});
|
|
80
45
|
}
|
|
81
46
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
severity: "error"
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
const calls2 = sf.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
101
|
-
for (const call of calls2) {
|
|
102
|
-
const exprText = call.getExpression().getText();
|
|
103
|
-
if (["useState", "useEffect", "useRef", "useMemo", "useCallback", "useReducer", "useContext", "useLayoutEffect"].includes(exprText) || /^use[A-Z]/.test(exprText) && exprText !== "use") {
|
|
104
|
-
errors.push({
|
|
105
|
-
ruleId: "krnl/top-level-purity",
|
|
106
|
-
filePath,
|
|
107
|
-
line: call.getStartLineNumber(),
|
|
108
|
-
message: `Top-level file '${baseName}' violates Top-Level Purity Law. React hook '${exprText}' is forbidden in top-level files. Encapsulate state and effects inside dedicated component files.`,
|
|
109
|
-
severity: "error"
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
}
|
|
47
|
+
}
|
|
48
|
+
const isMainLogic = filePath.endsWith("logic.ts") || filePath.endsWith("logic.js");
|
|
49
|
+
if (isMainLogic) {
|
|
50
|
+
const substantiveLines = lines.filter((l) => {
|
|
51
|
+
const trimmed = l.trim();
|
|
52
|
+
return trimmed.length > 0 && !trimmed.startsWith("//") && !trimmed.startsWith("/*") && !trimmed.startsWith("*");
|
|
53
|
+
});
|
|
54
|
+
if (substantiveLines.length > options.maxMainLogicLimit && !sourceText.includes("helpers") && !sourceText.includes("Concepts") && !sourceText.includes("state")) {
|
|
55
|
+
errors.push({
|
|
56
|
+
ruleId: "krnl/main-delegation",
|
|
57
|
+
filePath,
|
|
58
|
+
message: `Main logic function has ${substantiveLines.length} substantive lines without delegating to Layer 1 helper functions or Layer 0 libraries.`,
|
|
59
|
+
severity: "error"
|
|
60
|
+
});
|
|
113
61
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
62
|
+
}
|
|
63
|
+
return { errors, warnings };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/rules/componentRules.ts
|
|
67
|
+
import * as path from "path";
|
|
68
|
+
import { SyntaxKind } from "ts-morph";
|
|
69
|
+
function checkComponentRules(sf) {
|
|
70
|
+
const errors = [];
|
|
71
|
+
const warnings = [];
|
|
72
|
+
const filePath = sf.getFilePath();
|
|
73
|
+
const baseName = path.basename(filePath);
|
|
74
|
+
if (filePath.endsWith(".tsx") || filePath.endsWith(".jsx")) {
|
|
75
|
+
const componentNames = [];
|
|
76
|
+
for (const fn of sf.getFunctions()) {
|
|
77
|
+
const name = fn.getName();
|
|
78
|
+
if (name && /^[A-Z]/.test(name)) {
|
|
79
|
+
const hasJsx = fn.getDescendantsOfKind(SyntaxKind.JsxElement).length > 0 || fn.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).length > 0 || fn.getDescendantsOfKind(SyntaxKind.JsxFragment).length > 0;
|
|
80
|
+
if (hasJsx) componentNames.push(name);
|
|
127
81
|
}
|
|
128
82
|
}
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
for (const stmt of sf.getVariableStatements()) {
|
|
141
|
-
for (const decl of stmt.getDeclarations()) {
|
|
142
|
-
const name = decl.getName();
|
|
143
|
-
if (/^[A-Z]/.test(name)) {
|
|
144
|
-
const typeText = decl.getTypeNode()?.getText() || "";
|
|
145
|
-
const isFcType = typeText.includes("FC") || typeText.includes("FunctionComponent") || typeText.includes("ComponentType");
|
|
146
|
-
const init = decl.getInitializer();
|
|
147
|
-
if (init) {
|
|
148
|
-
const hasJsx = init.getDescendantsOfKind(SyntaxKind.JsxElement).length > 0 || init.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).length > 0 || init.getDescendantsOfKind(SyntaxKind.JsxFragment).length > 0;
|
|
149
|
-
if (isFcType || hasJsx) {
|
|
150
|
-
if (!componentNames.includes(name)) {
|
|
151
|
-
componentNames.push(name);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
83
|
+
for (const stmt of sf.getVariableStatements()) {
|
|
84
|
+
for (const decl of stmt.getDeclarations()) {
|
|
85
|
+
const name = decl.getName();
|
|
86
|
+
if (/^[A-Z]/.test(name)) {
|
|
87
|
+
const typeText = decl.getTypeNode()?.getText() || "";
|
|
88
|
+
const isFcType = typeText.includes("FC") || typeText.includes("FunctionComponent") || typeText.includes("ComponentType");
|
|
89
|
+
const init = decl.getInitializer();
|
|
90
|
+
if (init) {
|
|
91
|
+
const hasJsx = init.getDescendantsOfKind(SyntaxKind.JsxElement).length > 0 || init.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).length > 0 || init.getDescendantsOfKind(SyntaxKind.JsxFragment).length > 0;
|
|
92
|
+
if (isFcType || hasJsx) {
|
|
93
|
+
if (!componentNames.includes(name)) componentNames.push(name);
|
|
154
94
|
}
|
|
155
95
|
}
|
|
156
96
|
}
|
|
157
97
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
}
|
|
98
|
+
}
|
|
99
|
+
for (const cls of sf.getClasses()) {
|
|
100
|
+
const name = cls.getName();
|
|
101
|
+
const ext = cls.getExtends()?.getText() || "";
|
|
102
|
+
if (name && (ext.includes("Component") || ext.includes("PureComponent"))) {
|
|
103
|
+
if (!componentNames.includes(name)) componentNames.push(name);
|
|
166
104
|
}
|
|
167
|
-
|
|
105
|
+
}
|
|
106
|
+
if (componentNames.length > 1) {
|
|
107
|
+
errors.push({
|
|
108
|
+
ruleId: "krnl/one-component-per-file",
|
|
109
|
+
filePath,
|
|
110
|
+
message: `File '${baseName}' defines multiple React components (${componentNames.join(
|
|
111
|
+
", "
|
|
112
|
+
)}). Krnl strictly enforces ONE component per file. Decompose into sub-component files.`,
|
|
113
|
+
severity: "error"
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const isHelper = filePath.includes("helper_functions") || filePath.includes("/helpers/") || filePath.includes("\\helpers\\");
|
|
118
|
+
if (isHelper) {
|
|
119
|
+
for (const decl of sf.getImportDeclarations()) {
|
|
120
|
+
const spec = decl.getModuleSpecifierValue();
|
|
121
|
+
if (["fs", "node:fs", "http", "node:http", "ws"].includes(spec)) {
|
|
168
122
|
errors.push({
|
|
169
|
-
ruleId: "krnl/
|
|
123
|
+
ruleId: "krnl/helper-purity",
|
|
170
124
|
filePath,
|
|
171
|
-
|
|
125
|
+
line: decl.getStartLineNumber(),
|
|
126
|
+
message: `Helper functions (Layer 1) cannot directly import I/O modules ('${spec}'). Delegate to Layer 0 libraries or Entity context.`,
|
|
172
127
|
severity: "error"
|
|
173
128
|
});
|
|
174
129
|
}
|
|
175
130
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
message: `Helper function '${fn.getName() || "anonymous"}' exceeds ${this.maxHelperLineLimit} lines (${fnLines} lines). Must delegate to a root libraries/{Domain}Concepts.ts library.`,
|
|
200
|
-
severity: "error"
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
if (isMainLogic) {
|
|
206
|
-
const substantiveLines = lines.filter((l) => {
|
|
207
|
-
const trimmed = l.trim();
|
|
208
|
-
return trimmed.length > 0 && !trimmed.startsWith("//") && !trimmed.startsWith("/*") && !trimmed.startsWith("*");
|
|
131
|
+
}
|
|
132
|
+
return { errors, warnings };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/rules/entityRules.ts
|
|
136
|
+
import * as path2 from "path";
|
|
137
|
+
import { SyntaxKind as SyntaxKind2, Node } from "ts-morph";
|
|
138
|
+
function checkEntityRules(sf) {
|
|
139
|
+
const errors = [];
|
|
140
|
+
const warnings = [];
|
|
141
|
+
const filePath = sf.getFilePath();
|
|
142
|
+
const sourceText = sf.getText();
|
|
143
|
+
const baseName = path2.basename(filePath);
|
|
144
|
+
const calls = sf.getDescendantsOfKind(SyntaxKind2.CallExpression);
|
|
145
|
+
for (const call of calls) {
|
|
146
|
+
const exprText = call.getExpression().getText();
|
|
147
|
+
if (exprText === "setInterval" || exprText === "window.setInterval") {
|
|
148
|
+
errors.push({
|
|
149
|
+
ruleId: "krnl/no-polling",
|
|
150
|
+
filePath,
|
|
151
|
+
line: call.getStartLineNumber(),
|
|
152
|
+
message: `Prohibited use of 'setInterval'. Krnl applications must use reactive event streams (entity.emitEvent / entity.on).`,
|
|
153
|
+
severity: "error"
|
|
209
154
|
});
|
|
210
|
-
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (sourceText.includes("main = async") || sourceText.includes("export const main = async")) {
|
|
158
|
+
const match = sourceText.match(/main\s*=\s*async\s*\([^,]+,\s*\{([^}]*)\}\s*\)/);
|
|
159
|
+
if (match) {
|
|
160
|
+
const destructured = match[1];
|
|
161
|
+
if (/\bsystem\b/.test(destructured) && !/\bentity\b/.test(destructured)) {
|
|
211
162
|
errors.push({
|
|
212
|
-
ruleId: "krnl/
|
|
163
|
+
ruleId: "krnl/context-destructuring",
|
|
213
164
|
filePath,
|
|
214
|
-
message: `
|
|
165
|
+
message: `Functionality context parameter destructures 'system'. Functionality passes { state, entity }. Always destructure 'entity'.`,
|
|
215
166
|
severity: "error"
|
|
216
167
|
});
|
|
217
168
|
}
|
|
218
169
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
170
|
+
}
|
|
171
|
+
const isEntityClass = sf.getClasses().some((c) => {
|
|
172
|
+
const ext = c.getExtends();
|
|
173
|
+
return ext && (ext.getText().includes("Entity") || ext.getText().includes("krnlEntityBase"));
|
|
174
|
+
});
|
|
175
|
+
if (isEntityClass) {
|
|
176
|
+
for (const decl of sf.getImportDeclarations()) {
|
|
177
|
+
const spec = decl.getModuleSpecifierValue();
|
|
178
|
+
if (spec.includes("libraries/") || spec.includes("Concepts")) {
|
|
179
|
+
errors.push({
|
|
180
|
+
ruleId: "krnl/entity-no-libraries",
|
|
181
|
+
filePath,
|
|
182
|
+
line: decl.getStartLineNumber(),
|
|
183
|
+
message: `Entity class violates Library Calling Invariant. Entities must not call or import libraries directly ('${spec}'). Only functionalities in 'src/functionalities/' are permitted to call shared libraries.`,
|
|
184
|
+
severity: "error"
|
|
185
|
+
});
|
|
231
186
|
}
|
|
232
187
|
}
|
|
233
|
-
|
|
188
|
+
let loadAllLine = -1;
|
|
189
|
+
const addLocalLines = [];
|
|
234
190
|
for (const call of calls) {
|
|
235
|
-
const
|
|
236
|
-
if (
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
191
|
+
const txt = call.getText();
|
|
192
|
+
if (txt.includes("MagneticLoader.loadAll") || txt.includes("KrnlMagneticLoader.loadAll")) {
|
|
193
|
+
loadAllLine = call.getStartLineNumber();
|
|
194
|
+
} else if (txt.startsWith("this.addLocal(")) {
|
|
195
|
+
const hasUiSpec = txt.includes("uiSpec");
|
|
196
|
+
const idMatch = txt.match(/id\s*:\s*["']([^"']+)["']/);
|
|
197
|
+
addLocalLines.push({
|
|
240
198
|
line: call.getStartLineNumber(),
|
|
241
|
-
|
|
242
|
-
|
|
199
|
+
hasUiSpec,
|
|
200
|
+
id: idMatch ? idMatch[1] : "unknown"
|
|
243
201
|
});
|
|
244
202
|
}
|
|
245
203
|
}
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
});
|
|
250
|
-
if (isEntityClass) {
|
|
251
|
-
for (const decl of sf.getImportDeclarations()) {
|
|
252
|
-
const spec = decl.getModuleSpecifierValue();
|
|
253
|
-
if (spec.includes("libraries/") || spec.includes("Concepts")) {
|
|
204
|
+
if (loadAllLine !== -1) {
|
|
205
|
+
for (const al of addLocalLines) {
|
|
206
|
+
if (al.hasUiSpec && al.line > loadAllLine) {
|
|
254
207
|
errors.push({
|
|
255
|
-
ruleId: "krnl/
|
|
208
|
+
ruleId: "krnl/boot-order",
|
|
256
209
|
filePath,
|
|
257
|
-
line:
|
|
258
|
-
message: `
|
|
210
|
+
line: al.line,
|
|
211
|
+
message: `addLocal('${al.id}') with uiSpec at line ${al.line} appears AFTER loadAll() at line ${loadAllLine}. All addLocal() with UI must precede loadAll().`,
|
|
259
212
|
severity: "error"
|
|
260
213
|
});
|
|
261
214
|
}
|
|
262
215
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
216
|
+
}
|
|
217
|
+
for (const cls of sf.getClasses()) {
|
|
218
|
+
const ext = cls.getExtends();
|
|
219
|
+
if (ext && (ext.getText().includes("Entity") || ext.getText().includes("krnlEntityBase"))) {
|
|
220
|
+
const loops = [
|
|
221
|
+
...cls.getDescendantsOfKind(SyntaxKind2.ForStatement),
|
|
222
|
+
...cls.getDescendantsOfKind(SyntaxKind2.ForOfStatement),
|
|
223
|
+
...cls.getDescendantsOfKind(SyntaxKind2.ForInStatement),
|
|
224
|
+
...cls.getDescendantsOfKind(SyntaxKind2.WhileStatement),
|
|
225
|
+
...cls.getDescendantsOfKind(SyntaxKind2.DoStatement)
|
|
226
|
+
];
|
|
227
|
+
for (const loop of loops) {
|
|
228
|
+
errors.push({
|
|
229
|
+
ruleId: "krnl/entity-purity",
|
|
230
|
+
filePath,
|
|
231
|
+
line: loop.getStartLineNumber(),
|
|
232
|
+
message: `Entity '${cls.getName() || "Anonymous"}' violates Entity Purity Law. Loops and procedural algorithms are prohibited inside entities. Move logic to a functionality or a global library function.`,
|
|
233
|
+
severity: "error"
|
|
276
234
|
});
|
|
277
235
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (al.hasUiSpec && al.line > loadAllLine) {
|
|
236
|
+
for (const method of cls.getMethods()) {
|
|
237
|
+
const methodName = method.getName();
|
|
238
|
+
if (!["boot", "init", "dispose"].includes(methodName)) {
|
|
282
239
|
errors.push({
|
|
283
|
-
ruleId: "krnl/
|
|
240
|
+
ruleId: "krnl/entity-purity",
|
|
284
241
|
filePath,
|
|
285
|
-
line:
|
|
286
|
-
message: `
|
|
242
|
+
line: method.getStartLineNumber(),
|
|
243
|
+
message: `Entity '${cls.getName() || "Anonymous"}' defines custom method '${methodName}'. Entities are purely declarative compositors/orchestrators. Define capabilities as functionalities or global library functions.`,
|
|
287
244
|
severity: "error"
|
|
288
245
|
});
|
|
289
246
|
}
|
|
290
247
|
}
|
|
291
248
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
249
|
+
}
|
|
250
|
+
for (const call of calls) {
|
|
251
|
+
const callText = call.getText();
|
|
252
|
+
if (callText.startsWith("this.orchestration(") || callText.startsWith("this.flow(")) {
|
|
253
|
+
let currentExpr = call;
|
|
254
|
+
const chainCalls = [];
|
|
255
|
+
while (currentExpr && Node.isCallExpression(currentExpr)) {
|
|
256
|
+
const propAccess = currentExpr.getExpression();
|
|
257
|
+
if (Node.isPropertyAccessExpression(propAccess)) {
|
|
258
|
+
const methodName = propAccess.getName();
|
|
259
|
+
const args = currentExpr.getArguments();
|
|
260
|
+
const argText = args.length > 0 ? args[0].getText() : void 0;
|
|
261
|
+
chainCalls.unshift({ name: methodName, node: currentExpr, argText });
|
|
262
|
+
currentExpr = propAccess.getExpression();
|
|
263
|
+
} else {
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
let chainId = "orchestration";
|
|
268
|
+
for (const step of chainCalls) {
|
|
269
|
+
if (step.name === "orchestration" || step.name === "flow") {
|
|
270
|
+
chainId = step.argText ? step.argText.replace(/['"]/g, "") : "orchestration";
|
|
271
|
+
} else if (step.name === "thenFunction") {
|
|
303
272
|
errors.push({
|
|
304
|
-
ruleId: "krnl/entity-
|
|
273
|
+
ruleId: "krnl/entity-no-direct-library-calls",
|
|
305
274
|
filePath,
|
|
306
|
-
line:
|
|
307
|
-
message: `
|
|
275
|
+
line: step.node.getStartLineNumber(),
|
|
276
|
+
message: `Orchestration chain '${chainId}' violates Entity Purity Law. Entities cannot chain direct library functions via thenFunction(). Entities must only orchestrate functionalities (thenFunctionality) and orchestrations from other entities (thenEntityOrchestration).`,
|
|
308
277
|
severity: "error"
|
|
309
278
|
});
|
|
310
279
|
}
|
|
311
|
-
for (const method of cls.getMethods()) {
|
|
312
|
-
const methodName = method.getName();
|
|
313
|
-
if (!["boot", "init", "dispose"].includes(methodName)) {
|
|
314
|
-
errors.push({
|
|
315
|
-
ruleId: "krnl/entity-purity",
|
|
316
|
-
filePath,
|
|
317
|
-
line: method.getStartLineNumber(),
|
|
318
|
-
message: `Entity '${cls.getName() || "Anonymous"}' defines custom method '${methodName}'. Entities are purely declarative compositors/orchestrators. Define capabilities as functionalities or global library functions.`,
|
|
319
|
-
severity: "error"
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
280
|
}
|
|
324
281
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return { errors, warnings };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/rules/workspaceRules.ts
|
|
288
|
+
import * as fs from "fs";
|
|
289
|
+
import * as path3 from "path";
|
|
290
|
+
import { SyntaxKind as SyntaxKind3 } from "ts-morph";
|
|
291
|
+
function checkTopLevelPurity(sf) {
|
|
292
|
+
const errors = [];
|
|
293
|
+
const warnings = [];
|
|
294
|
+
const filePath = sf.getFilePath();
|
|
295
|
+
const baseName = path3.basename(filePath);
|
|
296
|
+
const isUiTopLevel = /^(App|main|index|_app|root)\.(tsx|jsx)$/i.test(baseName);
|
|
297
|
+
const isBackendTopLevel = /^(server|app)\.(ts|js)$/i.test(baseName) && !filePath.includes("/components/") && !filePath.includes("/entities/") && !filePath.includes("/libraries/");
|
|
298
|
+
if (isUiTopLevel) {
|
|
299
|
+
const jsxElements = sf.getDescendantsOfKind(SyntaxKind3.JsxOpeningElement);
|
|
300
|
+
const jsxSelfClosing = sf.getDescendantsOfKind(SyntaxKind3.JsxSelfClosingElement);
|
|
301
|
+
const allJsxTags = [...jsxElements, ...jsxSelfClosing];
|
|
302
|
+
for (const jsx of allJsxTags) {
|
|
303
|
+
const tagName = jsx.getTagNameNode().getText();
|
|
304
|
+
if (/^[a-z]/.test(tagName)) {
|
|
305
|
+
errors.push({
|
|
306
|
+
ruleId: "krnl/top-level-purity",
|
|
307
|
+
filePath,
|
|
308
|
+
line: jsx.getStartLineNumber(),
|
|
309
|
+
message: `Top-level file '${baseName}' violates Top-Level Purity Law. Raw JSX/HTML element '<${tagName}>' is strictly forbidden. Top-level files must ONLY import and compose React components.`,
|
|
310
|
+
severity: "error"
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const calls = sf.getDescendantsOfKind(SyntaxKind3.CallExpression);
|
|
315
|
+
for (const call of calls) {
|
|
316
|
+
const exprText = call.getExpression().getText();
|
|
317
|
+
if ([
|
|
318
|
+
"useState",
|
|
319
|
+
"useEffect",
|
|
320
|
+
"useRef",
|
|
321
|
+
"useMemo",
|
|
322
|
+
"useCallback",
|
|
323
|
+
"useReducer",
|
|
324
|
+
"useContext",
|
|
325
|
+
"useLayoutEffect"
|
|
326
|
+
].includes(exprText) || /^use[A-Z]/.test(exprText) && exprText !== "use") {
|
|
327
|
+
errors.push({
|
|
328
|
+
ruleId: "krnl/top-level-purity",
|
|
329
|
+
filePath,
|
|
330
|
+
line: call.getStartLineNumber(),
|
|
331
|
+
message: `Top-level file '${baseName}' violates Top-Level Purity Law. React hook '${exprText}' is forbidden in top-level files. Encapsulate state and effects inside dedicated component files.`,
|
|
332
|
+
severity: "error"
|
|
333
|
+
});
|
|
362
334
|
}
|
|
363
335
|
}
|
|
364
|
-
return { errors, warnings };
|
|
365
336
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
337
|
+
if (isBackendTopLevel) {
|
|
338
|
+
const calls = sf.getDescendantsOfKind(SyntaxKind3.CallExpression);
|
|
339
|
+
for (const call of calls) {
|
|
340
|
+
const exprText = call.getExpression().getText();
|
|
341
|
+
if (/^(app|router|server)\.(get|post|put|delete|patch|options|head)$/.test(exprText)) {
|
|
342
|
+
errors.push({
|
|
343
|
+
ruleId: "krnl/top-level-purity",
|
|
344
|
+
filePath,
|
|
345
|
+
line: call.getStartLineNumber(),
|
|
346
|
+
message: `Backend entry file '${baseName}' violates Entry Purity Law. Inline route definition '${exprText}' is forbidden. Delegate to dedicated route/controller modules.`,
|
|
347
|
+
severity: "error"
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return { errors, warnings };
|
|
353
|
+
}
|
|
354
|
+
function checkWorkspaceLayout(projectRoot, errors) {
|
|
355
|
+
const krnlDir = path3.join(projectRoot, "krnl");
|
|
356
|
+
const srcDir = path3.join(projectRoot, "src");
|
|
357
|
+
const targetDir = fs.existsSync(krnlDir) ? krnlDir : fs.existsSync(srcDir) ? srcDir : null;
|
|
358
|
+
if (!targetDir) return;
|
|
359
|
+
const backendDir = path3.join(targetDir, "backend");
|
|
360
|
+
const frontendDir = path3.join(targetDir, "frontend");
|
|
361
|
+
if (fs.existsSync(backendDir)) {
|
|
362
|
+
const bLibs = path3.join(backendDir, "libraries");
|
|
363
|
+
if (fs.existsSync(bLibs)) {
|
|
364
|
+
for (const entry of fs.readdirSync(bLibs, { withFileTypes: true })) {
|
|
365
|
+
if (!entry.isDirectory()) continue;
|
|
366
|
+
const libPath = path3.join(bLibs, entry.name);
|
|
367
|
+
if (!fs.existsSync(path3.join(libPath, "manifest.json"))) {
|
|
368
|
+
errors.push({
|
|
369
|
+
ruleId: "krnl/concept-manifest-required",
|
|
370
|
+
filePath: libPath,
|
|
371
|
+
message: `Backend concept library "${entry.name}" is missing a manifest.json file.`,
|
|
372
|
+
severity: "error"
|
|
373
|
+
});
|
|
391
374
|
}
|
|
392
375
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
}
|
|
376
|
+
}
|
|
377
|
+
const bFuncs = path3.join(backendDir, "functionalities");
|
|
378
|
+
if (fs.existsSync(bFuncs)) {
|
|
379
|
+
for (const entry of fs.readdirSync(bFuncs, { withFileTypes: true })) {
|
|
380
|
+
if (!entry.isDirectory()) continue;
|
|
381
|
+
const funcPath = path3.join(bFuncs, entry.name);
|
|
382
|
+
if (!fs.existsSync(path3.join(funcPath, "manifest.json"))) {
|
|
383
|
+
errors.push({
|
|
384
|
+
ruleId: "krnl/functionality-manifest-required",
|
|
385
|
+
filePath: funcPath,
|
|
386
|
+
message: `Backend functionality "${entry.name}" is missing a manifest.json file.`,
|
|
387
|
+
severity: "error"
|
|
388
|
+
});
|
|
407
389
|
}
|
|
408
390
|
}
|
|
409
391
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
392
|
+
}
|
|
393
|
+
if (fs.existsSync(frontendDir)) {
|
|
394
|
+
const fLibs = path3.join(frontendDir, "libraries");
|
|
395
|
+
if (fs.existsSync(fLibs)) {
|
|
396
|
+
for (const entry of fs.readdirSync(fLibs, { withFileTypes: true })) {
|
|
397
|
+
if (!entry.isDirectory()) continue;
|
|
398
|
+
const libPath = path3.join(fLibs, entry.name);
|
|
399
|
+
if (!fs.existsSync(path3.join(libPath, "manifest.json"))) {
|
|
400
|
+
errors.push({
|
|
401
|
+
ruleId: "krnl/frontend-concept-manifest-required",
|
|
402
|
+
filePath: libPath,
|
|
403
|
+
message: `Frontend concept library "${entry.name}" is missing a manifest.json file.`,
|
|
404
|
+
severity: "error"
|
|
405
|
+
});
|
|
425
406
|
}
|
|
426
407
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
408
|
+
}
|
|
409
|
+
const fFuncs = path3.join(frontendDir, "functionalities");
|
|
410
|
+
if (fs.existsSync(fFuncs)) {
|
|
411
|
+
for (const entry of fs.readdirSync(fFuncs, { withFileTypes: true })) {
|
|
412
|
+
if (!entry.isDirectory()) continue;
|
|
413
|
+
const funcPath = path3.join(fFuncs, entry.name);
|
|
414
|
+
if (!fs.existsSync(path3.join(funcPath, "manifest.json"))) {
|
|
415
|
+
errors.push({
|
|
416
|
+
ruleId: "krnl/frontend-functionality-manifest-required",
|
|
417
|
+
filePath: funcPath,
|
|
418
|
+
message: `Frontend functionality "${entry.name}" is missing a manifest.json file.`,
|
|
419
|
+
severity: "error"
|
|
420
|
+
});
|
|
441
421
|
}
|
|
442
422
|
}
|
|
443
423
|
}
|
|
444
424
|
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/KrnlValidatorEngine.ts
|
|
428
|
+
var KrnlValidatorEngine = class {
|
|
429
|
+
project;
|
|
430
|
+
projectRoot;
|
|
431
|
+
limitOptions;
|
|
432
|
+
constructor(optionsOrRoot) {
|
|
433
|
+
const options = typeof optionsOrRoot === "string" ? { projectRoot: optionsOrRoot } : optionsOrRoot;
|
|
434
|
+
this.projectRoot = path4.resolve(options.projectRoot || process.cwd());
|
|
435
|
+
this.limitOptions = {
|
|
436
|
+
maxFileLineLimit: options.maxFileLineLimit ?? 200,
|
|
437
|
+
maxMainLogicLimit: options.maxMainLogicLimit ?? 20,
|
|
438
|
+
maxHelperLineLimit: options.maxHelperLineLimit ?? 30
|
|
439
|
+
};
|
|
440
|
+
const tsConfigPath = path4.join(this.projectRoot, "tsconfig.json");
|
|
441
|
+
if (fs2.existsSync(tsConfigPath)) {
|
|
442
|
+
this.project = new Project({ tsConfigFilePath: tsConfigPath, skipAddingFilesFromTsConfig: true });
|
|
443
|
+
} else {
|
|
444
|
+
this.project = new Project({ compilerOptions: {
|
|
445
|
+
target: 99
|
|
446
|
+
/* ESNext */
|
|
447
|
+
} });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
validateAll() {
|
|
451
|
+
const files = this.collectSourceFiles(this.projectRoot);
|
|
452
|
+
const errors = [];
|
|
453
|
+
const warnings = [];
|
|
454
|
+
checkWorkspaceLayout(this.projectRoot, errors);
|
|
455
|
+
for (const f of files) {
|
|
456
|
+
const sf = this.project.addSourceFileAtPath(f);
|
|
457
|
+
const fileReport = this.validateSourceFile(sf);
|
|
458
|
+
errors.push(...fileReport.errors);
|
|
459
|
+
warnings.push(...fileReport.warnings);
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
valid: errors.length === 0,
|
|
463
|
+
totalFiles: files.length,
|
|
464
|
+
errors,
|
|
465
|
+
warnings
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
validateSourceFile(sf) {
|
|
469
|
+
const filePath = sf.getFilePath();
|
|
470
|
+
const baseName = path4.basename(filePath);
|
|
471
|
+
if (baseName.includes("_pb.") || baseName.endsWith(".d.ts")) {
|
|
472
|
+
return { errors: [], warnings: [] };
|
|
473
|
+
}
|
|
474
|
+
const limits = checkFileLimits(sf, this.limitOptions);
|
|
475
|
+
const purity = checkTopLevelPurity(sf);
|
|
476
|
+
const components = checkComponentRules(sf);
|
|
477
|
+
const entity = checkEntityRules(sf);
|
|
478
|
+
return {
|
|
479
|
+
errors: [...limits.errors, ...purity.errors, ...components.errors, ...entity.errors],
|
|
480
|
+
warnings: [...limits.warnings, ...purity.warnings, ...components.warnings, ...entity.warnings]
|
|
481
|
+
};
|
|
482
|
+
}
|
|
445
483
|
collectSourceFiles(dir, collected = []) {
|
|
446
|
-
if (!
|
|
447
|
-
const entries =
|
|
484
|
+
if (!fs2.existsSync(dir)) return collected;
|
|
485
|
+
const entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
448
486
|
for (const entry of entries) {
|
|
449
487
|
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git") continue;
|
|
450
|
-
const full =
|
|
488
|
+
const full = path4.join(dir, entry.name);
|
|
451
489
|
if (entry.isDirectory()) {
|
|
452
490
|
this.collectSourceFiles(full, collected);
|
|
453
491
|
} else if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".js") || entry.name.endsWith(".tsx") || entry.name.endsWith(".jsx"))) {
|