@optave/codegraph 3.1.0 → 3.1.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 (47) hide show
  1. package/README.md +5 -5
  2. package/grammars/tree-sitter-go.wasm +0 -0
  3. package/package.json +8 -9
  4. package/src/ast-analysis/rules/csharp.js +201 -0
  5. package/src/ast-analysis/rules/go.js +182 -0
  6. package/src/ast-analysis/rules/index.js +82 -0
  7. package/src/ast-analysis/rules/java.js +175 -0
  8. package/src/ast-analysis/rules/javascript.js +246 -0
  9. package/src/ast-analysis/rules/php.js +219 -0
  10. package/src/ast-analysis/rules/python.js +196 -0
  11. package/src/ast-analysis/rules/ruby.js +204 -0
  12. package/src/ast-analysis/rules/rust.js +173 -0
  13. package/src/ast-analysis/shared.js +223 -0
  14. package/src/ast.js +15 -28
  15. package/src/audit.js +4 -5
  16. package/src/boundaries.js +1 -1
  17. package/src/branch-compare.js +84 -79
  18. package/src/builder.js +0 -5
  19. package/src/cfg.js +106 -338
  20. package/src/check.js +3 -3
  21. package/src/cli.js +99 -179
  22. package/src/cochange.js +1 -1
  23. package/src/communities.js +13 -16
  24. package/src/complexity.js +196 -1239
  25. package/src/cycles.js +1 -1
  26. package/src/dataflow.js +269 -694
  27. package/src/db/connection.js +88 -0
  28. package/src/db/migrations.js +312 -0
  29. package/src/db/query-builder.js +280 -0
  30. package/src/db/repository.js +134 -0
  31. package/src/db.js +19 -399
  32. package/src/embedder.js +145 -141
  33. package/src/export.js +1 -1
  34. package/src/flow.js +161 -162
  35. package/src/index.js +34 -1
  36. package/src/kinds.js +49 -0
  37. package/src/manifesto.js +3 -8
  38. package/src/mcp.js +37 -20
  39. package/src/owners.js +132 -132
  40. package/src/queries-cli.js +866 -0
  41. package/src/queries.js +1323 -2267
  42. package/src/result-formatter.js +21 -0
  43. package/src/sequence.js +177 -182
  44. package/src/structure.js +200 -199
  45. package/src/test-filter.js +7 -0
  46. package/src/triage.js +120 -162
  47. package/src/viewer.js +1 -1
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Ruby — AST analysis rules.
3
+ */
4
+
5
+ import { makeCfgRules, makeDataflowRules } from '../shared.js';
6
+
7
+ // ─── Complexity ───────────────────────────────────────────────────────────
8
+
9
+ export const complexity = {
10
+ branchNodes: new Set([
11
+ 'if',
12
+ 'elsif',
13
+ 'else',
14
+ 'unless',
15
+ 'case',
16
+ 'for',
17
+ 'while',
18
+ 'until',
19
+ 'rescue',
20
+ 'conditional',
21
+ ]),
22
+ caseNodes: new Set(['when']),
23
+ logicalOperators: new Set(['and', 'or', '&&', '||']),
24
+ logicalNodeType: 'binary',
25
+ optionalChainType: null,
26
+ nestingNodes: new Set(['if', 'unless', 'case', 'for', 'while', 'until', 'rescue', 'conditional']),
27
+ functionNodes: new Set(['method', 'singleton_method', 'lambda', 'do_block']),
28
+ ifNodeType: 'if',
29
+ elseNodeType: 'else',
30
+ elifNodeType: 'elsif',
31
+ elseViaAlternative: false,
32
+ switchLikeNodes: new Set(['case']),
33
+ };
34
+
35
+ // ─── Halstead ─────────────────────────────────────────────────────────────
36
+
37
+ export const halstead = {
38
+ operatorLeafTypes: new Set([
39
+ '+',
40
+ '-',
41
+ '*',
42
+ '/',
43
+ '%',
44
+ '**',
45
+ '=',
46
+ '+=',
47
+ '-=',
48
+ '*=',
49
+ '/=',
50
+ '%=',
51
+ '**=',
52
+ '&=',
53
+ '|=',
54
+ '^=',
55
+ '<<=',
56
+ '>>=',
57
+ '==',
58
+ '!=',
59
+ '<',
60
+ '>',
61
+ '<=',
62
+ '>=',
63
+ '<=>',
64
+ '===',
65
+ '=~',
66
+ '!~',
67
+ '&&',
68
+ '||',
69
+ '!',
70
+ 'and',
71
+ 'or',
72
+ 'not',
73
+ '&',
74
+ '|',
75
+ '^',
76
+ '~',
77
+ '<<',
78
+ '>>',
79
+ 'if',
80
+ 'else',
81
+ 'elsif',
82
+ 'unless',
83
+ 'case',
84
+ 'when',
85
+ 'for',
86
+ 'while',
87
+ 'until',
88
+ 'do',
89
+ 'begin',
90
+ 'end',
91
+ 'return',
92
+ 'raise',
93
+ 'break',
94
+ 'next',
95
+ 'redo',
96
+ 'retry',
97
+ 'rescue',
98
+ 'ensure',
99
+ 'yield',
100
+ 'def',
101
+ 'class',
102
+ 'module',
103
+ '.',
104
+ ',',
105
+ ':',
106
+ '::',
107
+ '=>',
108
+ '->',
109
+ ]),
110
+ operandLeafTypes: new Set([
111
+ 'identifier',
112
+ 'constant',
113
+ 'instance_variable',
114
+ 'class_variable',
115
+ 'global_variable',
116
+ 'integer',
117
+ 'float',
118
+ 'string_content',
119
+ 'symbol',
120
+ 'true',
121
+ 'false',
122
+ 'nil',
123
+ 'self',
124
+ ]),
125
+ compoundOperators: new Set(['call', 'element_reference']),
126
+ skipTypes: new Set([]),
127
+ };
128
+
129
+ // ─── CFG ──────────────────────────────────────────────────────────────────
130
+
131
+ export const cfg = makeCfgRules({
132
+ ifNode: 'if',
133
+ elifNode: 'elsif',
134
+ elseClause: 'else',
135
+ forNodes: new Set(['for']),
136
+ whileNode: 'while',
137
+ unlessNode: 'unless',
138
+ untilNode: 'until',
139
+ switchNode: 'case',
140
+ caseNode: 'when',
141
+ defaultNode: 'else',
142
+ tryNode: 'begin',
143
+ catchNode: 'rescue',
144
+ finallyNode: 'ensure',
145
+ returnNode: 'return',
146
+ breakNode: 'break',
147
+ continueNode: 'next',
148
+ blockNodes: new Set(['then', 'do', 'body_statement']),
149
+ functionNodes: new Set(['method', 'singleton_method']),
150
+ });
151
+
152
+ // ─── Dataflow ─────────────────────────────────────────────────────────────
153
+
154
+ export const dataflow = makeDataflowRules({
155
+ functionNodes: new Set(['method', 'singleton_method', 'lambda']),
156
+ paramListField: 'parameters',
157
+ returnNode: 'return',
158
+ varDeclaratorNode: null,
159
+ assignmentNode: 'assignment',
160
+ assignLeftField: 'left',
161
+ assignRightField: 'right',
162
+ callNode: 'call',
163
+ callFunctionField: 'method',
164
+ callArgsField: 'arguments',
165
+ spreadType: 'splat_parameter',
166
+ memberNode: 'call',
167
+ memberObjectField: 'receiver',
168
+ memberPropertyField: 'method',
169
+ mutatingMethods: new Set([
170
+ 'push',
171
+ 'pop',
172
+ 'shift',
173
+ 'unshift',
174
+ 'delete',
175
+ 'clear',
176
+ 'sort!',
177
+ 'reverse!',
178
+ 'map!',
179
+ 'select!',
180
+ 'reject!',
181
+ 'compact!',
182
+ 'flatten!',
183
+ 'concat',
184
+ 'replace',
185
+ 'insert',
186
+ ]),
187
+ extractParamName(node) {
188
+ if (node.type === 'identifier') return [node.text];
189
+ if (
190
+ node.type === 'optional_parameter' ||
191
+ node.type === 'keyword_parameter' ||
192
+ node.type === 'splat_parameter' ||
193
+ node.type === 'hash_splat_parameter'
194
+ ) {
195
+ const nameNode = node.childForFieldName('name');
196
+ return nameNode ? [nameNode.text] : null;
197
+ }
198
+ return null;
199
+ },
200
+ });
201
+
202
+ // ─── AST Node Types ───────────────────────────────────────────────────────
203
+
204
+ export const astTypes = null;
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Rust — AST analysis rules.
3
+ */
4
+
5
+ import { makeCfgRules, makeDataflowRules } from '../shared.js';
6
+
7
+ // ─── Complexity ───────────────────────────────────────────────────────────
8
+
9
+ export const complexity = {
10
+ branchNodes: new Set([
11
+ 'if_expression',
12
+ 'else_clause',
13
+ 'for_expression',
14
+ 'while_expression',
15
+ 'loop_expression',
16
+ 'if_let_expression',
17
+ 'while_let_expression',
18
+ 'match_expression',
19
+ ]),
20
+ caseNodes: new Set(['match_arm']),
21
+ logicalOperators: new Set(['&&', '||']),
22
+ logicalNodeType: 'binary_expression',
23
+ optionalChainType: null,
24
+ nestingNodes: new Set([
25
+ 'if_expression',
26
+ 'for_expression',
27
+ 'while_expression',
28
+ 'loop_expression',
29
+ 'if_let_expression',
30
+ 'while_let_expression',
31
+ 'match_expression',
32
+ ]),
33
+ functionNodes: new Set(['function_item', 'closure_expression']),
34
+ ifNodeType: 'if_expression',
35
+ elseNodeType: 'else_clause',
36
+ elifNodeType: null,
37
+ elseViaAlternative: false,
38
+ switchLikeNodes: new Set(['match_expression']),
39
+ };
40
+
41
+ // ─── Halstead ─────────────────────────────────────────────────────────────
42
+
43
+ export const halstead = {
44
+ operatorLeafTypes: new Set([
45
+ '+',
46
+ '-',
47
+ '*',
48
+ '/',
49
+ '%',
50
+ '=',
51
+ '+=',
52
+ '-=',
53
+ '*=',
54
+ '/=',
55
+ '%=',
56
+ '&=',
57
+ '|=',
58
+ '^=',
59
+ '<<=',
60
+ '>>=',
61
+ '==',
62
+ '!=',
63
+ '<',
64
+ '>',
65
+ '<=',
66
+ '>=',
67
+ '&&',
68
+ '||',
69
+ '!',
70
+ '&',
71
+ '|',
72
+ '^',
73
+ '<<',
74
+ '>>',
75
+ 'if',
76
+ 'else',
77
+ 'for',
78
+ 'while',
79
+ 'loop',
80
+ 'match',
81
+ 'return',
82
+ 'break',
83
+ 'continue',
84
+ 'let',
85
+ 'mut',
86
+ 'ref',
87
+ 'as',
88
+ 'in',
89
+ 'move',
90
+ 'fn',
91
+ 'struct',
92
+ 'enum',
93
+ 'trait',
94
+ 'impl',
95
+ 'pub',
96
+ 'mod',
97
+ 'use',
98
+ '.',
99
+ ',',
100
+ ';',
101
+ ':',
102
+ '::',
103
+ '=>',
104
+ '->',
105
+ '?',
106
+ ]),
107
+ operandLeafTypes: new Set([
108
+ 'identifier',
109
+ 'field_identifier',
110
+ 'type_identifier',
111
+ 'integer_literal',
112
+ 'float_literal',
113
+ 'string_content',
114
+ 'char_literal',
115
+ 'true',
116
+ 'false',
117
+ 'self',
118
+ 'Self',
119
+ ]),
120
+ compoundOperators: new Set(['call_expression', 'index_expression', 'field_expression']),
121
+ skipTypes: new Set([]),
122
+ };
123
+
124
+ // ─── CFG ──────────────────────────────────────────────────────────────────
125
+
126
+ export const cfg = makeCfgRules({
127
+ ifNode: 'if_expression',
128
+ ifNodes: new Set(['if_let_expression']),
129
+ elseClause: 'else_clause',
130
+ forNodes: new Set(['for_expression']),
131
+ whileNode: 'while_expression',
132
+ whileNodes: new Set(['while_let_expression']),
133
+ infiniteLoopNode: 'loop_expression',
134
+ switchNode: 'match_expression',
135
+ caseNode: 'match_arm',
136
+ returnNode: 'return_expression',
137
+ breakNode: 'break_expression',
138
+ continueNode: 'continue_expression',
139
+ blockNode: 'block',
140
+ functionNodes: new Set(['function_item', 'closure_expression']),
141
+ });
142
+
143
+ // ─── Dataflow ─────────────────────────────────────────────────────────────
144
+
145
+ export const dataflow = makeDataflowRules({
146
+ functionNodes: new Set(['function_item', 'closure_expression']),
147
+ returnNode: 'return_expression',
148
+ varDeclaratorNode: 'let_declaration',
149
+ varNameField: 'pattern',
150
+ varValueField: 'value',
151
+ assignmentNode: 'assignment_expression',
152
+ callNode: 'call_expression',
153
+ callFunctionField: 'function',
154
+ callArgsField: 'arguments',
155
+ memberNode: 'field_expression',
156
+ memberObjectField: 'value',
157
+ memberPropertyField: 'field',
158
+ awaitNode: 'await_expression',
159
+ mutatingMethods: new Set(['push', 'pop', 'insert', 'remove', 'clear', 'sort', 'reverse']),
160
+ extractParamName(node) {
161
+ if (node.type === 'parameter') {
162
+ const pat = node.childForFieldName('pattern');
163
+ if (pat?.type === 'identifier') return [pat.text];
164
+ return null;
165
+ }
166
+ if (node.type === 'identifier') return [node.text];
167
+ return null;
168
+ },
169
+ });
170
+
171
+ // ─── AST Node Types ───────────────────────────────────────────────────────
172
+
173
+ export const astTypes = null;
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Shared utilities for AST analysis modules (complexity, CFG, dataflow, AST nodes).
3
+ */
4
+
5
+ import { LANGUAGE_REGISTRY } from '../parser.js';
6
+
7
+ // ─── Generic Rule Factory ─────────────────────────────────────────────────
8
+
9
+ /**
10
+ * Merge defaults with overrides, validating that all keys are known.
11
+ *
12
+ * @param {object} defaults - Default rule values (defines the valid key set)
13
+ * @param {object} overrides - Language-specific overrides
14
+ * @param {string} label - Label for error messages (e.g. "CFG", "Dataflow")
15
+ * @returns {object} Merged rules
16
+ */
17
+ export function makeRules(defaults, overrides, label) {
18
+ const validKeys = new Set(Object.keys(defaults));
19
+ for (const key of Object.keys(overrides)) {
20
+ if (!validKeys.has(key)) {
21
+ throw new Error(`${label} rules: unknown key "${key}"`);
22
+ }
23
+ }
24
+ return { ...defaults, ...overrides };
25
+ }
26
+
27
+ // ─── CFG Defaults + Factory ───────────────────────────────────────────────
28
+
29
+ export const CFG_DEFAULTS = {
30
+ ifNode: null,
31
+ ifNodes: null,
32
+ elifNode: null,
33
+ elseClause: null,
34
+ elseViaAlternative: false,
35
+ ifConsequentField: null,
36
+ forNodes: new Set(),
37
+ whileNode: null,
38
+ whileNodes: null,
39
+ doNode: null,
40
+ infiniteLoopNode: null,
41
+ unlessNode: null,
42
+ untilNode: null,
43
+ switchNode: null,
44
+ switchNodes: null,
45
+ caseNode: null,
46
+ caseNodes: null,
47
+ defaultNode: null,
48
+ tryNode: null,
49
+ catchNode: null,
50
+ finallyNode: null,
51
+ returnNode: null,
52
+ throwNode: null,
53
+ breakNode: null,
54
+ continueNode: null,
55
+ blockNode: null,
56
+ blockNodes: null,
57
+ labeledNode: null,
58
+ functionNodes: new Set(),
59
+ };
60
+
61
+ export function makeCfgRules(overrides) {
62
+ const rules = makeRules(CFG_DEFAULTS, overrides, 'CFG');
63
+ if (!(rules.functionNodes instanceof Set) || rules.functionNodes.size === 0) {
64
+ throw new Error('CFG rules: functionNodes must be a non-empty Set');
65
+ }
66
+ if (!(rules.forNodes instanceof Set)) {
67
+ throw new Error('CFG rules: forNodes must be a Set');
68
+ }
69
+ return rules;
70
+ }
71
+
72
+ // ─── Dataflow Defaults + Factory ──────────────────────────────────────────
73
+
74
+ export const DATAFLOW_DEFAULTS = {
75
+ // Scope entry
76
+ functionNodes: new Set(), // REQUIRED: non-empty
77
+
78
+ // Function name extraction
79
+ nameField: 'name',
80
+ varAssignedFnParent: null, // parent type for `const fn = ...` (JS only)
81
+ assignmentFnParent: null, // parent type for `x = function...` (JS only)
82
+ pairFnParent: null, // parent type for `{ key: function }` (JS only)
83
+
84
+ // Parameters
85
+ paramListField: 'parameters',
86
+ paramIdentifier: 'identifier',
87
+ paramWrapperTypes: new Set(),
88
+ defaultParamType: null,
89
+ restParamType: null,
90
+ objectDestructType: null,
91
+ arrayDestructType: null,
92
+ shorthandPropPattern: null,
93
+ pairPatternType: null,
94
+ extractParamName: null, // override: (node) => string[]
95
+
96
+ // Return
97
+ returnNode: null,
98
+
99
+ // Variable declarations
100
+ varDeclaratorNode: null,
101
+ varDeclaratorNodes: null,
102
+ varNameField: 'name',
103
+ varValueField: 'value',
104
+ assignmentNode: null,
105
+ assignLeftField: 'left',
106
+ assignRightField: 'right',
107
+
108
+ // Calls
109
+ callNode: null,
110
+ callNodes: null,
111
+ callFunctionField: 'function',
112
+ callArgsField: 'arguments',
113
+ spreadType: null,
114
+
115
+ // Member access
116
+ memberNode: null,
117
+ memberObjectField: 'object',
118
+ memberPropertyField: 'property',
119
+ optionalChainNode: null,
120
+
121
+ // Await
122
+ awaitNode: null,
123
+
124
+ // Mutation
125
+ mutatingMethods: new Set(),
126
+ expressionStmtNode: 'expression_statement',
127
+ callObjectField: null, // Java: combined call+member has [object] field on call node
128
+
129
+ // Structural wrappers
130
+ expressionListType: null, // Go: expression_list wraps LHS/RHS of short_var_declaration
131
+ equalsClauseType: null, // C#: equals_value_clause wraps variable initializer
132
+ argumentWrapperType: null, // PHP: individual args wrapped in 'argument' nodes
133
+ extraIdentifierTypes: null, // Set of additional identifier-like types (PHP: variable_name, name)
134
+ };
135
+
136
+ export function makeDataflowRules(overrides) {
137
+ const rules = makeRules(DATAFLOW_DEFAULTS, overrides, 'Dataflow');
138
+ if (!(rules.functionNodes instanceof Set) || rules.functionNodes.size === 0) {
139
+ throw new Error('Dataflow rules: functionNodes must be a non-empty Set');
140
+ }
141
+ return rules;
142
+ }
143
+
144
+ // ─── AST Helpers ──────────────────────────────────────────────────────────
145
+
146
+ /**
147
+ * Find the function body node in a parse tree that matches a given line range.
148
+ */
149
+ export function findFunctionNode(rootNode, startLine, _endLine, rules) {
150
+ // tree-sitter lines are 0-indexed
151
+ const targetStart = startLine - 1;
152
+
153
+ let best = null;
154
+
155
+ function search(node) {
156
+ const nodeStart = node.startPosition.row;
157
+ const nodeEnd = node.endPosition.row;
158
+
159
+ // Prune branches outside range
160
+ if (nodeEnd < targetStart || nodeStart > targetStart + 1) return;
161
+
162
+ if (rules.functionNodes.has(node.type) && nodeStart === targetStart) {
163
+ // Found a function node at the right position — pick it
164
+ if (!best || nodeEnd - nodeStart < best.endPosition.row - best.startPosition.row) {
165
+ best = node;
166
+ }
167
+ }
168
+
169
+ for (let i = 0; i < node.childCount; i++) {
170
+ search(node.child(i));
171
+ }
172
+ }
173
+
174
+ search(rootNode);
175
+ return best;
176
+ }
177
+
178
+ /**
179
+ * Truncate a string to a maximum length, appending an ellipsis if truncated.
180
+ *
181
+ * @param {string} str - Input string
182
+ * @param {number} [max=200] - Maximum length
183
+ * @returns {string}
184
+ */
185
+ export function truncate(str, max = 200) {
186
+ if (!str) return '';
187
+ return str.length > max ? `${str.slice(0, max)}…` : str;
188
+ }
189
+
190
+ // ─── Extension / Language Mapping ─────────────────────────────────────────
191
+
192
+ /**
193
+ * Build a Map from file extension → language ID using the parser registry.
194
+ *
195
+ * @param {Iterable} [registry=LANGUAGE_REGISTRY] - Language registry entries
196
+ * @returns {Map<string, string>}
197
+ */
198
+ export function buildExtToLangMap(registry = LANGUAGE_REGISTRY) {
199
+ const map = new Map();
200
+ for (const entry of registry) {
201
+ for (const ext of entry.extensions) {
202
+ map.set(ext, entry.id);
203
+ }
204
+ }
205
+ return map;
206
+ }
207
+
208
+ /**
209
+ * Build a Set of file extensions for languages that have entries in the given rules Map.
210
+ *
211
+ * @param {Map<string, any>} rulesMap - e.g. COMPLEXITY_RULES, CFG_RULES
212
+ * @param {Iterable} [registry=LANGUAGE_REGISTRY] - Language registry entries
213
+ * @returns {Set<string>}
214
+ */
215
+ export function buildExtensionSet(rulesMap, registry = LANGUAGE_REGISTRY) {
216
+ const extensions = new Set();
217
+ for (const entry of registry) {
218
+ if (rulesMap.has(entry.id)) {
219
+ for (const ext of entry.extensions) extensions.add(ext);
220
+ }
221
+ }
222
+ return extensions;
223
+ }
package/src/ast.js CHANGED
@@ -7,10 +7,13 @@
7
7
  */
8
8
 
9
9
  import path from 'node:path';
10
+ import { AST_TYPE_MAPS } from './ast-analysis/rules/index.js';
11
+ import { buildExtensionSet } from './ast-analysis/shared.js';
10
12
  import { openReadonlyOrFail } from './db.js';
11
13
  import { debug } from './logger.js';
12
- import { paginateResult, printNdjson } from './paginate.js';
13
- import { LANGUAGE_REGISTRY } from './parser.js';
14
+ import { paginateResult } from './paginate.js';
15
+
16
+ import { outputResult } from './result-formatter.js';
14
17
 
15
18
  // ─── Constants ────────────────────────────────────────────────────────
16
19
 
@@ -28,23 +31,11 @@ const KIND_ICONS = {
28
31
  /** Max length for the `text` column. */
29
32
  const TEXT_MAX = 200;
30
33
 
31
- /** tree-sitter node types that map to our AST node kinds (JS/TS/TSX). */
32
- const JS_TS_AST_TYPES = {
33
- new_expression: 'new',
34
- throw_statement: 'throw',
35
- await_expression: 'await',
36
- string: 'string',
37
- template_string: 'string',
38
- regex: 'regex',
39
- };
34
+ /** tree-sitter node types that map to our AST node kinds — imported from rules. */
35
+ const JS_TS_AST_TYPES = AST_TYPE_MAPS.get('javascript');
40
36
 
41
37
  /** Extensions that support full AST walk (new/throw/await/string/regex). */
42
- const WALK_EXTENSIONS = new Set();
43
- for (const lang of Object.values(LANGUAGE_REGISTRY)) {
44
- if (['javascript', 'typescript', 'tsx'].includes(lang.id)) {
45
- for (const ext of lang.extensions) WALK_EXTENSIONS.add(ext);
46
- }
47
- }
38
+ const WALK_EXTENSIONS = buildExtensionSet(AST_TYPE_MAPS);
48
39
 
49
40
  // ─── Helpers ──────────────────────────────────────────────────────────
50
41
 
@@ -351,8 +342,12 @@ export function astQueryData(pattern, customDbPath, opts = {}) {
351
342
  ORDER BY a.file, a.line
352
343
  `;
353
344
 
354
- const rows = db.prepare(sql).all(...params);
355
- db.close();
345
+ let rows;
346
+ try {
347
+ rows = db.prepare(sql).all(...params);
348
+ } finally {
349
+ db.close();
350
+ }
356
351
 
357
352
  const results = rows.map((r) => ({
358
353
  kind: r.kind,
@@ -382,15 +377,7 @@ export function astQueryData(pattern, customDbPath, opts = {}) {
382
377
  export function astQuery(pattern, customDbPath, opts = {}) {
383
378
  const data = astQueryData(pattern, customDbPath, opts);
384
379
 
385
- if (opts.ndjson) {
386
- printNdjson(data, 'results');
387
- return;
388
- }
389
-
390
- if (opts.json) {
391
- console.log(JSON.stringify(data, null, 2));
392
- return;
393
- }
380
+ if (outputResult(data, 'results', opts)) return;
394
381
 
395
382
  // Human-readable output
396
383
  if (data.results.length === 0) {
package/src/audit.js CHANGED
@@ -10,7 +10,9 @@ import path from 'node:path';
10
10
  import { loadConfig } from './config.js';
11
11
  import { openReadonlyOrFail } from './db.js';
12
12
  import { RULE_DEFS } from './manifesto.js';
13
- import { explainData, isTestFile, kindIcon } from './queries.js';
13
+ import { explainData, kindIcon } from './queries.js';
14
+ import { outputResult } from './result-formatter.js';
15
+ import { isTestFile } from './test-filter.js';
14
16
 
15
17
  // ─── Threshold resolution ───────────────────────────────────────────
16
18
 
@@ -340,10 +342,7 @@ function defaultHealth() {
340
342
  export function audit(target, customDbPath, opts = {}) {
341
343
  const data = auditData(target, customDbPath, opts);
342
344
 
343
- if (opts.json) {
344
- console.log(JSON.stringify(data, null, 2));
345
- return;
346
- }
345
+ if (outputResult(data, null, opts)) return;
347
346
 
348
347
  if (data.functions.length === 0) {
349
348
  console.log(`No ${data.kind === 'file' ? 'file' : 'function/symbol'} matching "${target}"`);
package/src/boundaries.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { debug } from './logger.js';
2
- import { isTestFile } from './queries.js';
2
+ import { isTestFile } from './test-filter.js';
3
3
 
4
4
  // ─── Glob-to-Regex ───────────────────────────────────────────────────
5
5