@optave/codegraph 3.1.5 → 3.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.
- package/README.md +3 -2
- package/package.json +7 -7
- package/src/ast-analysis/engine.js +252 -258
- package/src/ast-analysis/shared.js +0 -12
- package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
- package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
- package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
- package/src/cli/commands/ast.js +2 -1
- package/src/cli/commands/audit.js +2 -1
- package/src/cli/commands/batch.js +2 -1
- package/src/cli/commands/brief.js +12 -0
- package/src/cli/commands/cfg.js +2 -1
- package/src/cli/commands/check.js +20 -23
- package/src/cli/commands/children.js +6 -1
- package/src/cli/commands/complexity.js +2 -1
- package/src/cli/commands/context.js +6 -1
- package/src/cli/commands/dataflow.js +2 -1
- package/src/cli/commands/deps.js +8 -3
- package/src/cli/commands/flow.js +2 -1
- package/src/cli/commands/fn-impact.js +6 -1
- package/src/cli/commands/owners.js +4 -2
- package/src/cli/commands/query.js +6 -1
- package/src/cli/commands/roles.js +2 -1
- package/src/cli/commands/search.js +8 -2
- package/src/cli/commands/sequence.js +2 -1
- package/src/cli/commands/triage.js +38 -27
- package/src/db/connection.js +18 -12
- package/src/db/migrations.js +41 -64
- package/src/db/query-builder.js +60 -4
- package/src/db/repository/in-memory-repository.js +27 -16
- package/src/db/repository/nodes.js +8 -10
- package/src/domain/analysis/brief.js +155 -0
- package/src/domain/analysis/context.js +174 -190
- package/src/domain/analysis/dependencies.js +200 -146
- package/src/domain/analysis/exports.js +3 -2
- package/src/domain/analysis/impact.js +267 -152
- package/src/domain/analysis/module-map.js +247 -221
- package/src/domain/analysis/roles.js +8 -5
- package/src/domain/analysis/symbol-lookup.js +7 -5
- package/src/domain/graph/builder/helpers.js +1 -1
- package/src/domain/graph/builder/incremental.js +116 -90
- package/src/domain/graph/builder/pipeline.js +106 -80
- package/src/domain/graph/builder/stages/build-edges.js +318 -239
- package/src/domain/graph/builder/stages/detect-changes.js +198 -177
- package/src/domain/graph/builder/stages/insert-nodes.js +147 -139
- package/src/domain/graph/watcher.js +2 -2
- package/src/domain/parser.js +20 -11
- package/src/domain/queries.js +1 -0
- package/src/domain/search/search/filters.js +9 -5
- package/src/domain/search/search/keyword.js +12 -5
- package/src/domain/search/search/prepare.js +13 -5
- package/src/extractors/csharp.js +224 -207
- package/src/extractors/go.js +176 -172
- package/src/extractors/hcl.js +94 -78
- package/src/extractors/java.js +213 -207
- package/src/extractors/javascript.js +274 -304
- package/src/extractors/php.js +234 -221
- package/src/extractors/python.js +252 -250
- package/src/extractors/ruby.js +192 -185
- package/src/extractors/rust.js +182 -167
- package/src/features/ast.js +5 -3
- package/src/features/audit.js +4 -2
- package/src/features/boundaries.js +98 -83
- package/src/features/cfg.js +134 -143
- package/src/features/communities.js +68 -53
- package/src/features/complexity.js +143 -132
- package/src/features/dataflow.js +146 -149
- package/src/features/export.js +3 -3
- package/src/features/graph-enrichment.js +2 -2
- package/src/features/manifesto.js +9 -6
- package/src/features/owners.js +4 -3
- package/src/features/sequence.js +152 -141
- package/src/features/shared/find-nodes.js +31 -0
- package/src/features/structure.js +130 -99
- package/src/features/triage.js +83 -68
- package/src/graph/classifiers/risk.js +3 -2
- package/src/graph/classifiers/roles.js +6 -3
- package/src/index.js +1 -0
- package/src/mcp/server.js +65 -56
- package/src/mcp/tool-registry.js +13 -0
- package/src/mcp/tools/brief.js +8 -0
- package/src/mcp/tools/index.js +2 -0
- package/src/presentation/brief.js +51 -0
- package/src/presentation/queries-cli/exports.js +21 -14
- package/src/presentation/queries-cli/impact.js +55 -39
- package/src/presentation/queries-cli/inspect.js +184 -189
- package/src/presentation/queries-cli/overview.js +57 -58
- package/src/presentation/queries-cli/path.js +36 -29
- package/src/presentation/table.js +0 -8
- package/src/shared/generators.js +7 -3
- package/src/shared/kinds.js +1 -1
package/src/extractors/rust.js
CHANGED
|
@@ -4,191 +4,206 @@ import { findChild, nodeEndLine, rustVisibility } from './helpers.js';
|
|
|
4
4
|
* Extract symbols from Rust files.
|
|
5
5
|
*/
|
|
6
6
|
export function extractRustSymbols(tree, _filePath) {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
const ctx = {
|
|
8
|
+
definitions: [],
|
|
9
|
+
calls: [],
|
|
10
|
+
imports: [],
|
|
11
|
+
classes: [],
|
|
12
|
+
exports: [],
|
|
13
|
+
};
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
walkRustNode(tree.rootNode, ctx);
|
|
16
|
+
return ctx;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function walkRustNode(node, ctx) {
|
|
20
|
+
switch (node.type) {
|
|
21
|
+
case 'function_item':
|
|
22
|
+
handleRustFuncItem(node, ctx);
|
|
23
|
+
break;
|
|
24
|
+
case 'struct_item':
|
|
25
|
+
handleRustStructItem(node, ctx);
|
|
26
|
+
break;
|
|
27
|
+
case 'enum_item':
|
|
28
|
+
handleRustEnumItem(node, ctx);
|
|
29
|
+
break;
|
|
30
|
+
case 'const_item':
|
|
31
|
+
handleRustConstItem(node, ctx);
|
|
32
|
+
break;
|
|
33
|
+
case 'trait_item':
|
|
34
|
+
handleRustTraitItem(node, ctx);
|
|
35
|
+
break;
|
|
36
|
+
case 'impl_item':
|
|
37
|
+
handleRustImplItem(node, ctx);
|
|
38
|
+
break;
|
|
39
|
+
case 'use_declaration':
|
|
40
|
+
handleRustUseDecl(node, ctx);
|
|
41
|
+
break;
|
|
42
|
+
case 'call_expression':
|
|
43
|
+
handleRustCallExpr(node, ctx);
|
|
44
|
+
break;
|
|
45
|
+
case 'macro_invocation':
|
|
46
|
+
handleRustMacroInvocation(node, ctx);
|
|
47
|
+
break;
|
|
23
48
|
}
|
|
24
49
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
case 'function_item': {
|
|
28
|
-
const nameNode = node.childForFieldName('name');
|
|
29
|
-
if (nameNode) {
|
|
30
|
-
const implType = findCurrentImpl(node);
|
|
31
|
-
const fullName = implType ? `${implType}.${nameNode.text}` : nameNode.text;
|
|
32
|
-
const kind = implType ? 'method' : 'function';
|
|
33
|
-
const params = extractRustParameters(node.childForFieldName('parameters'));
|
|
34
|
-
definitions.push({
|
|
35
|
-
name: fullName,
|
|
36
|
-
kind,
|
|
37
|
-
line: node.startPosition.row + 1,
|
|
38
|
-
endLine: nodeEndLine(node),
|
|
39
|
-
children: params.length > 0 ? params : undefined,
|
|
40
|
-
visibility: rustVisibility(node),
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
break;
|
|
44
|
-
}
|
|
50
|
+
for (let i = 0; i < node.childCount; i++) walkRustNode(node.child(i), ctx);
|
|
51
|
+
}
|
|
45
52
|
|
|
46
|
-
|
|
47
|
-
const nameNode = node.childForFieldName('name');
|
|
48
|
-
if (nameNode) {
|
|
49
|
-
const fields = extractStructFields(node);
|
|
50
|
-
definitions.push({
|
|
51
|
-
name: nameNode.text,
|
|
52
|
-
kind: 'struct',
|
|
53
|
-
line: node.startPosition.row + 1,
|
|
54
|
-
endLine: nodeEndLine(node),
|
|
55
|
-
children: fields.length > 0 ? fields : undefined,
|
|
56
|
-
visibility: rustVisibility(node),
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
break;
|
|
60
|
-
}
|
|
53
|
+
// ── Walk-path per-node-type handlers ────────────────────────────────────────
|
|
61
54
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
55
|
+
function handleRustFuncItem(node, ctx) {
|
|
56
|
+
// Skip default-impl functions already emitted by handleRustTraitItem
|
|
57
|
+
if (node.parent?.parent?.type === 'trait_item') return;
|
|
58
|
+
const nameNode = node.childForFieldName('name');
|
|
59
|
+
if (!nameNode) return;
|
|
60
|
+
const implType = findCurrentImpl(node);
|
|
61
|
+
const fullName = implType ? `${implType}.${nameNode.text}` : nameNode.text;
|
|
62
|
+
const kind = implType ? 'method' : 'function';
|
|
63
|
+
const params = extractRustParameters(node.childForFieldName('parameters'));
|
|
64
|
+
ctx.definitions.push({
|
|
65
|
+
name: fullName,
|
|
66
|
+
kind,
|
|
67
|
+
line: node.startPosition.row + 1,
|
|
68
|
+
endLine: nodeEndLine(node),
|
|
69
|
+
children: params.length > 0 ? params : undefined,
|
|
70
|
+
visibility: rustVisibility(node),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
76
73
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
74
|
+
function handleRustStructItem(node, ctx) {
|
|
75
|
+
const nameNode = node.childForFieldName('name');
|
|
76
|
+
if (!nameNode) return;
|
|
77
|
+
const fields = extractStructFields(node);
|
|
78
|
+
ctx.definitions.push({
|
|
79
|
+
name: nameNode.text,
|
|
80
|
+
kind: 'struct',
|
|
81
|
+
line: node.startPosition.row + 1,
|
|
82
|
+
endLine: nodeEndLine(node),
|
|
83
|
+
children: fields.length > 0 ? fields : undefined,
|
|
84
|
+
visibility: rustVisibility(node),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
89
87
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
endLine: child.endPosition.row + 1,
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
break;
|
|
121
|
-
}
|
|
88
|
+
function handleRustEnumItem(node, ctx) {
|
|
89
|
+
const nameNode = node.childForFieldName('name');
|
|
90
|
+
if (!nameNode) return;
|
|
91
|
+
const variants = extractEnumVariants(node);
|
|
92
|
+
ctx.definitions.push({
|
|
93
|
+
name: nameNode.text,
|
|
94
|
+
kind: 'enum',
|
|
95
|
+
line: node.startPosition.row + 1,
|
|
96
|
+
endLine: nodeEndLine(node),
|
|
97
|
+
children: variants.length > 0 ? variants : undefined,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function handleRustConstItem(node, ctx) {
|
|
102
|
+
const nameNode = node.childForFieldName('name');
|
|
103
|
+
if (!nameNode) return;
|
|
104
|
+
ctx.definitions.push({
|
|
105
|
+
name: nameNode.text,
|
|
106
|
+
kind: 'constant',
|
|
107
|
+
line: node.startPosition.row + 1,
|
|
108
|
+
endLine: nodeEndLine(node),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
122
111
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
112
|
+
function handleRustTraitItem(node, ctx) {
|
|
113
|
+
const nameNode = node.childForFieldName('name');
|
|
114
|
+
if (!nameNode) return;
|
|
115
|
+
ctx.definitions.push({
|
|
116
|
+
name: nameNode.text,
|
|
117
|
+
kind: 'trait',
|
|
118
|
+
line: node.startPosition.row + 1,
|
|
119
|
+
endLine: nodeEndLine(node),
|
|
120
|
+
});
|
|
121
|
+
const body = node.childForFieldName('body');
|
|
122
|
+
if (body) {
|
|
123
|
+
for (let i = 0; i < body.childCount; i++) {
|
|
124
|
+
const child = body.child(i);
|
|
125
|
+
if (child && (child.type === 'function_signature_item' || child.type === 'function_item')) {
|
|
126
|
+
const methName = child.childForFieldName('name');
|
|
127
|
+
if (methName) {
|
|
128
|
+
ctx.definitions.push({
|
|
129
|
+
name: `${nameNode.text}.${methName.text}`,
|
|
130
|
+
kind: 'method',
|
|
131
|
+
line: child.startPosition.row + 1,
|
|
132
|
+
endLine: child.endPosition.row + 1,
|
|
131
133
|
});
|
|
132
134
|
}
|
|
133
|
-
break;
|
|
134
135
|
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
135
139
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
140
|
+
function handleRustImplItem(node, ctx) {
|
|
141
|
+
const typeNode = node.childForFieldName('type');
|
|
142
|
+
const traitNode = node.childForFieldName('trait');
|
|
143
|
+
if (typeNode && traitNode) {
|
|
144
|
+
ctx.classes.push({
|
|
145
|
+
name: typeNode.text,
|
|
146
|
+
implements: traitNode.text,
|
|
147
|
+
line: node.startPosition.row + 1,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
} else if (fn.type === 'scoped_identifier') {
|
|
166
|
-
const name = fn.childForFieldName('name');
|
|
167
|
-
if (name) {
|
|
168
|
-
const path = fn.childForFieldName('path');
|
|
169
|
-
const call = { name: name.text, line: node.startPosition.row + 1 };
|
|
170
|
-
if (path) call.receiver = path.text;
|
|
171
|
-
calls.push(call);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
152
|
+
function handleRustUseDecl(node, ctx) {
|
|
153
|
+
const argNode = node.child(1);
|
|
154
|
+
if (!argNode) return;
|
|
155
|
+
const usePaths = extractRustUsePath(argNode);
|
|
156
|
+
for (const imp of usePaths) {
|
|
157
|
+
ctx.imports.push({
|
|
158
|
+
source: imp.source,
|
|
159
|
+
names: imp.names,
|
|
160
|
+
line: node.startPosition.row + 1,
|
|
161
|
+
rustUse: true,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
177
165
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
166
|
+
function handleRustCallExpr(node, ctx) {
|
|
167
|
+
const fn = node.childForFieldName('function');
|
|
168
|
+
if (!fn) return;
|
|
169
|
+
if (fn.type === 'identifier') {
|
|
170
|
+
ctx.calls.push({ name: fn.text, line: node.startPosition.row + 1 });
|
|
171
|
+
} else if (fn.type === 'field_expression') {
|
|
172
|
+
const field = fn.childForFieldName('field');
|
|
173
|
+
if (field) {
|
|
174
|
+
const value = fn.childForFieldName('value');
|
|
175
|
+
const call = { name: field.text, line: node.startPosition.row + 1 };
|
|
176
|
+
if (value) call.receiver = value.text;
|
|
177
|
+
ctx.calls.push(call);
|
|
178
|
+
}
|
|
179
|
+
} else if (fn.type === 'scoped_identifier') {
|
|
180
|
+
const name = fn.childForFieldName('name');
|
|
181
|
+
if (name) {
|
|
182
|
+
const path = fn.childForFieldName('path');
|
|
183
|
+
const call = { name: name.text, line: node.startPosition.row + 1 };
|
|
184
|
+
if (path) call.receiver = path.text;
|
|
185
|
+
ctx.calls.push(call);
|
|
185
186
|
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
186
189
|
|
|
187
|
-
|
|
190
|
+
function handleRustMacroInvocation(node, ctx) {
|
|
191
|
+
const macroNode = node.child(0);
|
|
192
|
+
if (macroNode) {
|
|
193
|
+
ctx.calls.push({ name: `${macroNode.text}!`, line: node.startPosition.row + 1 });
|
|
188
194
|
}
|
|
195
|
+
}
|
|
189
196
|
|
|
190
|
-
|
|
191
|
-
|
|
197
|
+
function findCurrentImpl(node) {
|
|
198
|
+
let current = node.parent;
|
|
199
|
+
while (current) {
|
|
200
|
+
if (current.type === 'impl_item') {
|
|
201
|
+
const typeNode = current.childForFieldName('type');
|
|
202
|
+
return typeNode ? typeNode.text : null;
|
|
203
|
+
}
|
|
204
|
+
current = current.parent;
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
192
207
|
}
|
|
193
208
|
|
|
194
209
|
// ── Child extraction helpers ────────────────────────────────────────────────
|
package/src/features/ast.js
CHANGED
|
@@ -12,6 +12,7 @@ import { buildExtensionSet } from '../ast-analysis/shared.js';
|
|
|
12
12
|
import { walkWithVisitors } from '../ast-analysis/visitor.js';
|
|
13
13
|
import { createAstStoreVisitor } from '../ast-analysis/visitors/ast-store-visitor.js';
|
|
14
14
|
import { bulkNodeIdsByFile, openReadonlyOrFail } from '../db/index.js';
|
|
15
|
+
import { buildFileConditionSQL } from '../db/query-builder.js';
|
|
15
16
|
import { debug } from '../infrastructure/logger.js';
|
|
16
17
|
import { outputResult } from '../infrastructure/result-formatter.js';
|
|
17
18
|
import { paginateResult } from '../shared/paginate.js';
|
|
@@ -193,9 +194,10 @@ export function astQueryData(pattern, customDbPath, opts = {}) {
|
|
|
193
194
|
params.push(kind);
|
|
194
195
|
}
|
|
195
196
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
197
|
+
{
|
|
198
|
+
const fc = buildFileConditionSQL(file, 'a.file');
|
|
199
|
+
where += fc.sql;
|
|
200
|
+
params.push(...fc.params);
|
|
199
201
|
}
|
|
200
202
|
|
|
201
203
|
if (noTests) {
|
package/src/features/audit.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { openReadonlyOrFail } from '../db/index.js';
|
|
11
|
+
import { normalizeFileFilter } from '../db/query-builder.js';
|
|
11
12
|
import { bfsTransitiveCallers } from '../domain/analysis/impact.js';
|
|
12
13
|
import { explainData } from '../domain/queries.js';
|
|
13
14
|
import { loadConfig } from '../infrastructure/config.js';
|
|
@@ -100,7 +101,7 @@ function readPhase44(db, nodeId) {
|
|
|
100
101
|
export function auditData(target, customDbPath, opts = {}) {
|
|
101
102
|
const noTests = opts.noTests || false;
|
|
102
103
|
const maxDepth = opts.depth || 3;
|
|
103
|
-
const
|
|
104
|
+
const fileFilters = normalizeFileFilter(opts.file);
|
|
104
105
|
const kind = opts.kind;
|
|
105
106
|
|
|
106
107
|
// 1. Get structure via explainData
|
|
@@ -109,7 +110,8 @@ export function auditData(target, customDbPath, opts = {}) {
|
|
|
109
110
|
// Apply --file and --kind filters for function targets
|
|
110
111
|
let results = explained.results;
|
|
111
112
|
if (explained.kind === 'function') {
|
|
112
|
-
if (
|
|
113
|
+
if (fileFilters.length > 0)
|
|
114
|
+
results = results.filter((r) => fileFilters.some((f) => r.file.includes(f)));
|
|
113
115
|
if (kind) results = results.filter((r) => r.kind === kind);
|
|
114
116
|
}
|
|
115
117
|
|
|
@@ -94,104 +94,119 @@ export function resolveModules(boundaryConfig) {
|
|
|
94
94
|
// ─── Validation ──────────────────────────────────────────────────────
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
|
-
* Validate a boundary
|
|
98
|
-
* @param {object}
|
|
99
|
-
* @
|
|
97
|
+
* Validate the `modules` section of a boundary config.
|
|
98
|
+
* @param {object} modules
|
|
99
|
+
* @param {string[]} errors - Mutated: push any validation errors
|
|
100
100
|
*/
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
function validateModules(modules, errors) {
|
|
102
|
+
if (!modules || typeof modules !== 'object' || Object.keys(modules).length === 0) {
|
|
103
|
+
errors.push('boundaries.modules must be a non-empty object');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
for (const [name, value] of Object.entries(modules)) {
|
|
107
|
+
if (typeof value === 'string') continue;
|
|
108
|
+
if (value && typeof value === 'object' && typeof value.match === 'string') continue;
|
|
109
|
+
errors.push(`boundaries.modules.${name}: must be a glob string or { match: "<glob>" }`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
103
112
|
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Validate the `preset` field of a boundary config.
|
|
115
|
+
* @param {string|null|undefined} preset
|
|
116
|
+
* @param {string[]} errors - Mutated: push any validation errors
|
|
117
|
+
*/
|
|
118
|
+
function validatePreset(preset, errors) {
|
|
119
|
+
if (preset == null) return;
|
|
120
|
+
if (typeof preset !== 'string' || !PRESETS[preset]) {
|
|
121
|
+
errors.push(
|
|
122
|
+
`boundaries.preset: must be one of ${Object.keys(PRESETS).join(', ')} (got "${preset}")`,
|
|
123
|
+
);
|
|
106
124
|
}
|
|
125
|
+
}
|
|
107
126
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Validate a single rule's target list (`notTo` or `onlyTo`).
|
|
129
|
+
* @param {*} list - The target list value
|
|
130
|
+
* @param {string} field - "notTo" or "onlyTo"
|
|
131
|
+
* @param {number} idx - Rule index for error messages
|
|
132
|
+
* @param {Set<string>} moduleNames
|
|
133
|
+
* @param {string[]} errors - Mutated
|
|
134
|
+
*/
|
|
135
|
+
function validateTargetList(list, field, idx, moduleNames, errors) {
|
|
136
|
+
if (!Array.isArray(list)) {
|
|
137
|
+
errors.push(`boundaries.rules[${idx}]: "${field}" must be an array`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
for (const target of list) {
|
|
141
|
+
if (!moduleNames.has(target)) {
|
|
142
|
+
errors.push(`boundaries.rules[${idx}]: "${field}" references unknown module "${target}"`);
|
|
120
143
|
}
|
|
121
144
|
}
|
|
145
|
+
}
|
|
122
146
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Validate the `rules` array of a boundary config.
|
|
149
|
+
* @param {Array} rules
|
|
150
|
+
* @param {object|undefined} modules - The modules config (for cross-referencing names)
|
|
151
|
+
* @param {string[]} errors - Mutated
|
|
152
|
+
*/
|
|
153
|
+
function validateRules(rules, modules, errors) {
|
|
154
|
+
if (!rules) return;
|
|
155
|
+
if (!Array.isArray(rules)) {
|
|
156
|
+
errors.push('boundaries.rules must be an array');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const moduleNames = modules ? new Set(Object.keys(modules)) : new Set();
|
|
160
|
+
for (let i = 0; i < rules.length; i++) {
|
|
161
|
+
const rule = rules[i];
|
|
162
|
+
if (!rule.from) {
|
|
163
|
+
errors.push(`boundaries.rules[${i}]: missing "from" field`);
|
|
164
|
+
} else if (!moduleNames.has(rule.from)) {
|
|
165
|
+
errors.push(`boundaries.rules[${i}]: "from" references unknown module "${rule.from}"`);
|
|
166
|
+
}
|
|
167
|
+
if (rule.notTo && rule.onlyTo) {
|
|
168
|
+
errors.push(`boundaries.rules[${i}]: cannot have both "notTo" and "onlyTo"`);
|
|
169
|
+
}
|
|
170
|
+
if (!rule.notTo && !rule.onlyTo) {
|
|
171
|
+
errors.push(`boundaries.rules[${i}]: must have either "notTo" or "onlyTo"`);
|
|
129
172
|
}
|
|
173
|
+
if (rule.notTo) validateTargetList(rule.notTo, 'notTo', i, moduleNames, errors);
|
|
174
|
+
if (rule.onlyTo) validateTargetList(rule.onlyTo, 'onlyTo', i, moduleNames, errors);
|
|
130
175
|
}
|
|
176
|
+
}
|
|
131
177
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (rule.notTo && rule.onlyTo) {
|
|
146
|
-
errors.push(`boundaries.rules[${i}]: cannot have both "notTo" and "onlyTo"`);
|
|
147
|
-
}
|
|
148
|
-
if (!rule.notTo && !rule.onlyTo) {
|
|
149
|
-
errors.push(`boundaries.rules[${i}]: must have either "notTo" or "onlyTo"`);
|
|
150
|
-
}
|
|
151
|
-
if (rule.notTo) {
|
|
152
|
-
if (!Array.isArray(rule.notTo)) {
|
|
153
|
-
errors.push(`boundaries.rules[${i}]: "notTo" must be an array`);
|
|
154
|
-
} else {
|
|
155
|
-
for (const target of rule.notTo) {
|
|
156
|
-
if (!moduleNames.has(target)) {
|
|
157
|
-
errors.push(
|
|
158
|
-
`boundaries.rules[${i}]: "notTo" references unknown module "${target}"`,
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
if (rule.onlyTo) {
|
|
165
|
-
if (!Array.isArray(rule.onlyTo)) {
|
|
166
|
-
errors.push(`boundaries.rules[${i}]: "onlyTo" must be an array`);
|
|
167
|
-
} else {
|
|
168
|
-
for (const target of rule.onlyTo) {
|
|
169
|
-
if (!moduleNames.has(target)) {
|
|
170
|
-
errors.push(
|
|
171
|
-
`boundaries.rules[${i}]: "onlyTo" references unknown module "${target}"`,
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
+
/**
|
|
179
|
+
* Validate that module layer assignments match preset layers.
|
|
180
|
+
* @param {object} config
|
|
181
|
+
* @param {string[]} errors - Mutated
|
|
182
|
+
*/
|
|
183
|
+
function validateLayerAssignments(config, errors) {
|
|
184
|
+
if (!config.preset || !PRESETS[config.preset] || !config.modules) return;
|
|
185
|
+
const presetLayers = new Set(PRESETS[config.preset].layers);
|
|
186
|
+
for (const [name, value] of Object.entries(config.modules)) {
|
|
187
|
+
if (typeof value === 'object' && value.layer && !presetLayers.has(value.layer)) {
|
|
188
|
+
errors.push(
|
|
189
|
+
`boundaries.modules.${name}: layer "${value.layer}" not in preset "${config.preset}" (valid: ${[...presetLayers].join(', ')})`,
|
|
190
|
+
);
|
|
178
191
|
}
|
|
179
192
|
}
|
|
193
|
+
}
|
|
180
194
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
195
|
+
/**
|
|
196
|
+
* Validate a boundary configuration object.
|
|
197
|
+
* @param {object} config - The `manifesto.boundaries` config
|
|
198
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
199
|
+
*/
|
|
200
|
+
export function validateBoundaryConfig(config) {
|
|
201
|
+
if (!config || typeof config !== 'object') {
|
|
202
|
+
return { valid: false, errors: ['boundaries config must be an object'] };
|
|
193
203
|
}
|
|
194
204
|
|
|
205
|
+
const errors = [];
|
|
206
|
+
validateModules(config.modules, errors);
|
|
207
|
+
validatePreset(config.preset, errors);
|
|
208
|
+
validateRules(config.rules, config.modules, errors);
|
|
209
|
+
validateLayerAssignments(config, errors);
|
|
195
210
|
return { valid: errors.length === 0, errors };
|
|
196
211
|
}
|
|
197
212
|
|