@grafema/rfdb-client 0.1.1-alpha → 0.2.1-beta
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/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +18 -8
- package/dist/client.test.d.ts +12 -0
- package/dist/client.test.d.ts.map +1 -0
- package/dist/client.test.js +238 -0
- package/package.json +2 -2
- package/ts/client.test.ts +297 -0
- package/ts/client.ts +21 -8
package/dist/client.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ export declare class RFDBClient extends EventEmitter implements IRFDBClient {
|
|
|
32
32
|
private _send;
|
|
33
33
|
/**
|
|
34
34
|
* Add nodes to the graph
|
|
35
|
+
* Extra properties beyond id/type/name/file/exported/metadata are merged into metadata
|
|
35
36
|
*/
|
|
36
37
|
addNodes(nodes: Array<Partial<WireNode> & {
|
|
37
38
|
id: string;
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../ts/client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAEV,QAAQ,EACR,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,SAAS,EACT,aAAa,EACb,QAAQ,EACR,QAAQ,EACT,MAAM,gBAAgB,CAAC;AAOxB,qBAAa,UAAW,SAAQ,YAAa,YAAW,WAAW;IACjE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,MAAM,CAAgB;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,OAAO,CAA8B;IAC7C,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,MAAM,CAAS;gBAEX,UAAU,GAAE,MAAyB;IAUjD;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAoC9B;;OAEG;IACH,OAAO,CAAC,WAAW;IA0BnB;;OAEG;IACH,OAAO,CAAC,eAAe;IAiBvB;;OAEG;YACW,KAAK;IAwBnB
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../ts/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -124,16 +124,26 @@ export class RFDBClient extends EventEmitter {
|
|
|
124
124
|
// ===========================================================================
|
|
125
125
|
/**
|
|
126
126
|
* Add nodes to the graph
|
|
127
|
+
* Extra properties beyond id/type/name/file/exported/metadata are merged into metadata
|
|
127
128
|
*/
|
|
128
129
|
async addNodes(nodes) {
|
|
129
|
-
const wireNodes = nodes.map(n =>
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
130
|
+
const wireNodes = nodes.map(n => {
|
|
131
|
+
// Cast to Record to allow iteration over extra properties
|
|
132
|
+
const nodeRecord = n;
|
|
133
|
+
// Extract known wire format fields, rest goes to metadata
|
|
134
|
+
const { id, type, node_type, nodeType, name, file, exported, metadata, ...rest } = nodeRecord;
|
|
135
|
+
// Merge explicit metadata with extra properties
|
|
136
|
+
const existingMeta = typeof metadata === 'string' ? JSON.parse(metadata) : (metadata || {});
|
|
137
|
+
const combinedMeta = { ...existingMeta, ...rest };
|
|
138
|
+
return {
|
|
139
|
+
id: String(id),
|
|
140
|
+
nodeType: (node_type || nodeType || type || 'UNKNOWN'),
|
|
141
|
+
name: name || '',
|
|
142
|
+
file: file || '',
|
|
143
|
+
exported: exported || false,
|
|
144
|
+
metadata: JSON.stringify(combinedMeta),
|
|
145
|
+
};
|
|
146
|
+
});
|
|
137
147
|
return this._send('addNodes', { nodes: wireNodes });
|
|
138
148
|
}
|
|
139
149
|
/**
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFDBClient Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for RFDBClient functionality that don't require a running server.
|
|
5
|
+
* Uses mock socket to test message serialization and addNodes behavior.
|
|
6
|
+
*
|
|
7
|
+
* Key tests for REG-274:
|
|
8
|
+
* - addNodes() should preserve extra fields in metadata
|
|
9
|
+
* - Extra fields like constraints, condition, scopeType should not be lost
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=client.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.test.d.ts","sourceRoot":"","sources":["../ts/client.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFDBClient Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for RFDBClient functionality that don't require a running server.
|
|
5
|
+
* Uses mock socket to test message serialization and addNodes behavior.
|
|
6
|
+
*
|
|
7
|
+
* Key tests for REG-274:
|
|
8
|
+
* - addNodes() should preserve extra fields in metadata
|
|
9
|
+
* - Extra fields like constraints, condition, scopeType should not be lost
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it } from 'node:test';
|
|
12
|
+
import assert from 'node:assert';
|
|
13
|
+
/**
|
|
14
|
+
* Mock RFDBClient that captures what would be sent to the server
|
|
15
|
+
*
|
|
16
|
+
* We can't easily mock the socket, so we test the serialization logic
|
|
17
|
+
* by extracting the node mapping logic from addNodes.
|
|
18
|
+
*/
|
|
19
|
+
function mapNodeForWireFormat(n) {
|
|
20
|
+
// This is the CURRENT implementation - extracts only known fields
|
|
21
|
+
// Bug: extra fields are silently discarded
|
|
22
|
+
return {
|
|
23
|
+
id: String(n.id),
|
|
24
|
+
nodeType: (n.node_type || n.nodeType || n.type || 'UNKNOWN'),
|
|
25
|
+
name: n.name || '',
|
|
26
|
+
file: n.file || '',
|
|
27
|
+
exported: n.exported || false,
|
|
28
|
+
metadata: typeof n.metadata === 'string' ? n.metadata : JSON.stringify(n.metadata || {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* FIXED implementation that preserves extra fields
|
|
33
|
+
*/
|
|
34
|
+
function mapNodeForWireFormatFixed(n) {
|
|
35
|
+
// Extract known wire format fields, rest goes to metadata
|
|
36
|
+
const { id, type, node_type, nodeType, name, file, exported, metadata, ...rest } = n;
|
|
37
|
+
// Merge explicit metadata with extra properties
|
|
38
|
+
const existingMeta = typeof metadata === 'string' ? JSON.parse(metadata) : (metadata || {});
|
|
39
|
+
const combinedMeta = { ...existingMeta, ...rest };
|
|
40
|
+
return {
|
|
41
|
+
id: String(id),
|
|
42
|
+
nodeType: (node_type || nodeType || type || 'UNKNOWN'),
|
|
43
|
+
name: name || '',
|
|
44
|
+
file: file || '',
|
|
45
|
+
exported: exported || false,
|
|
46
|
+
metadata: JSON.stringify(combinedMeta),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
describe('RFDBClient.addNodes() Metadata Preservation', () => {
|
|
50
|
+
/**
|
|
51
|
+
* WHY: JSASTAnalyzer collects constraints for SCOPE nodes during analysis.
|
|
52
|
+
* These constraints contain guard information like "someValue !== null".
|
|
53
|
+
* If constraints are lost during serialization, the graph cannot answer
|
|
54
|
+
* questions like "what conditions guard this code execution?"
|
|
55
|
+
*
|
|
56
|
+
* This test documents the BUG: constraints are silently discarded.
|
|
57
|
+
*/
|
|
58
|
+
it('BUG: current implementation loses constraints field', () => {
|
|
59
|
+
const scopeNode = {
|
|
60
|
+
id: 'SCOPE:file.js:10',
|
|
61
|
+
type: 'SCOPE',
|
|
62
|
+
name: 'if_branch',
|
|
63
|
+
file: 'file.js',
|
|
64
|
+
// Extra fields that should be preserved
|
|
65
|
+
constraints: [
|
|
66
|
+
{ variable: 'someValue', operator: '!==', value: 'null' },
|
|
67
|
+
],
|
|
68
|
+
condition: 'someValue !== null',
|
|
69
|
+
scopeType: 'if_statement',
|
|
70
|
+
conditional: true,
|
|
71
|
+
line: 10,
|
|
72
|
+
};
|
|
73
|
+
const wireNode = mapNodeForWireFormat(scopeNode);
|
|
74
|
+
const metadata = JSON.parse(wireNode.metadata);
|
|
75
|
+
// BUG: These assertions FAIL - constraints are lost
|
|
76
|
+
// Once the bug is fixed, these will pass
|
|
77
|
+
assert.strictEqual(metadata.constraints, undefined, 'BUG: constraints should be lost in current impl');
|
|
78
|
+
assert.strictEqual(metadata.condition, undefined, 'BUG: condition should be lost in current impl');
|
|
79
|
+
assert.strictEqual(metadata.scopeType, undefined, 'BUG: scopeType should be lost in current impl');
|
|
80
|
+
assert.strictEqual(metadata.conditional, undefined, 'BUG: conditional should be lost in current impl');
|
|
81
|
+
assert.strictEqual(metadata.line, undefined, 'BUG: line should be lost in current impl');
|
|
82
|
+
});
|
|
83
|
+
/**
|
|
84
|
+
* WHY: After the fix, extra fields should be merged into metadata.
|
|
85
|
+
* This test verifies the EXPECTED behavior after REG-274 is implemented.
|
|
86
|
+
*/
|
|
87
|
+
it('FIXED: should preserve constraints in metadata', () => {
|
|
88
|
+
const scopeNode = {
|
|
89
|
+
id: 'SCOPE:file.js:10',
|
|
90
|
+
type: 'SCOPE',
|
|
91
|
+
name: 'if_branch',
|
|
92
|
+
file: 'file.js',
|
|
93
|
+
// Extra fields that should be preserved
|
|
94
|
+
constraints: [
|
|
95
|
+
{ variable: 'someValue', operator: '!==', value: 'null' },
|
|
96
|
+
],
|
|
97
|
+
condition: 'someValue !== null',
|
|
98
|
+
scopeType: 'if_statement',
|
|
99
|
+
conditional: true,
|
|
100
|
+
line: 10,
|
|
101
|
+
};
|
|
102
|
+
const wireNode = mapNodeForWireFormatFixed(scopeNode);
|
|
103
|
+
const metadata = JSON.parse(wireNode.metadata);
|
|
104
|
+
// These assertions should PASS after fix
|
|
105
|
+
assert.deepStrictEqual(metadata.constraints, [{ variable: 'someValue', operator: '!==', value: 'null' }], 'constraints should be preserved in metadata');
|
|
106
|
+
assert.strictEqual(metadata.condition, 'someValue !== null', 'condition should be preserved');
|
|
107
|
+
assert.strictEqual(metadata.scopeType, 'if_statement', 'scopeType should be preserved');
|
|
108
|
+
assert.strictEqual(metadata.conditional, true, 'conditional should be preserved');
|
|
109
|
+
assert.strictEqual(metadata.line, 10, 'line should be preserved');
|
|
110
|
+
});
|
|
111
|
+
/**
|
|
112
|
+
* WHY: Extra fields should be MERGED with existing metadata, not replace it.
|
|
113
|
+
*/
|
|
114
|
+
it('FIXED: should merge extra fields with existing metadata', () => {
|
|
115
|
+
const node = {
|
|
116
|
+
id: 'NODE:test',
|
|
117
|
+
type: 'SCOPE',
|
|
118
|
+
name: 'test',
|
|
119
|
+
file: 'test.js',
|
|
120
|
+
metadata: { existingField: 'value', semanticId: 'test->scope' },
|
|
121
|
+
// Extra fields
|
|
122
|
+
constraints: [{ variable: 'x', operator: '>', value: '0' }],
|
|
123
|
+
newField: 'newValue',
|
|
124
|
+
};
|
|
125
|
+
const wireNode = mapNodeForWireFormatFixed(node);
|
|
126
|
+
const metadata = JSON.parse(wireNode.metadata);
|
|
127
|
+
assert.strictEqual(metadata.existingField, 'value', 'existing metadata should be preserved');
|
|
128
|
+
assert.strictEqual(metadata.semanticId, 'test->scope', 'semanticId should be preserved');
|
|
129
|
+
assert.deepStrictEqual(metadata.constraints, [{ variable: 'x', operator: '>', value: '0' }], 'new constraints should be added');
|
|
130
|
+
assert.strictEqual(metadata.newField, 'newValue', 'new fields should be added');
|
|
131
|
+
});
|
|
132
|
+
/**
|
|
133
|
+
* WHY: String metadata (JSON string) should be parsed and merged.
|
|
134
|
+
*/
|
|
135
|
+
it('FIXED: should handle string metadata correctly', () => {
|
|
136
|
+
const node = {
|
|
137
|
+
id: 'NODE:test',
|
|
138
|
+
type: 'CALL',
|
|
139
|
+
name: 'test',
|
|
140
|
+
file: 'test.js',
|
|
141
|
+
metadata: JSON.stringify({ callee: 'foo', args: ['a', 'b'] }),
|
|
142
|
+
// Extra field
|
|
143
|
+
resolved: true,
|
|
144
|
+
};
|
|
145
|
+
const wireNode = mapNodeForWireFormatFixed(node);
|
|
146
|
+
const metadata = JSON.parse(wireNode.metadata);
|
|
147
|
+
assert.strictEqual(metadata.callee, 'foo', 'callee from string metadata should be preserved');
|
|
148
|
+
assert.deepStrictEqual(metadata.args, ['a', 'b'], 'args from string metadata should be preserved');
|
|
149
|
+
assert.strictEqual(metadata.resolved, true, 'extra field should be merged');
|
|
150
|
+
});
|
|
151
|
+
/**
|
|
152
|
+
* WHY: Known wire fields (id, type, name, file, exported) should NOT
|
|
153
|
+
* appear in metadata - they have their own fields in the wire format.
|
|
154
|
+
*/
|
|
155
|
+
it('FIXED: should not duplicate known fields in metadata', () => {
|
|
156
|
+
const node = {
|
|
157
|
+
id: 'NODE:test',
|
|
158
|
+
type: 'FUNCTION',
|
|
159
|
+
name: 'myFunc',
|
|
160
|
+
file: 'test.js',
|
|
161
|
+
exported: true,
|
|
162
|
+
// Only extra fields should go to metadata
|
|
163
|
+
async: true,
|
|
164
|
+
generator: false,
|
|
165
|
+
};
|
|
166
|
+
const wireNode = mapNodeForWireFormatFixed(node);
|
|
167
|
+
const metadata = JSON.parse(wireNode.metadata);
|
|
168
|
+
// Known fields should NOT be in metadata (they have dedicated wire fields)
|
|
169
|
+
assert.strictEqual(metadata.id, undefined, 'id should not be duplicated in metadata');
|
|
170
|
+
assert.strictEqual(metadata.type, undefined, 'type should not be duplicated in metadata');
|
|
171
|
+
assert.strictEqual(metadata.name, undefined, 'name should not be duplicated in metadata');
|
|
172
|
+
assert.strictEqual(metadata.file, undefined, 'file should not be duplicated in metadata');
|
|
173
|
+
assert.strictEqual(metadata.exported, undefined, 'exported should not be duplicated in metadata');
|
|
174
|
+
// Extra fields SHOULD be in metadata
|
|
175
|
+
assert.strictEqual(metadata.async, true, 'async should be in metadata');
|
|
176
|
+
assert.strictEqual(metadata.generator, false, 'generator should be in metadata');
|
|
177
|
+
// Verify wire format fields are set correctly
|
|
178
|
+
assert.strictEqual(wireNode.id, 'NODE:test');
|
|
179
|
+
assert.strictEqual(wireNode.nodeType, 'FUNCTION');
|
|
180
|
+
assert.strictEqual(wireNode.name, 'myFunc');
|
|
181
|
+
assert.strictEqual(wireNode.file, 'test.js');
|
|
182
|
+
assert.strictEqual(wireNode.exported, true);
|
|
183
|
+
});
|
|
184
|
+
/**
|
|
185
|
+
* WHY: Empty or undefined metadata should work correctly.
|
|
186
|
+
*/
|
|
187
|
+
it('FIXED: should handle nodes without metadata', () => {
|
|
188
|
+
const node = {
|
|
189
|
+
id: 'NODE:test',
|
|
190
|
+
type: 'MODULE',
|
|
191
|
+
name: 'test',
|
|
192
|
+
file: 'test.js',
|
|
193
|
+
// No metadata field
|
|
194
|
+
version: '1.0.0',
|
|
195
|
+
};
|
|
196
|
+
const wireNode = mapNodeForWireFormatFixed(node);
|
|
197
|
+
const metadata = JSON.parse(wireNode.metadata);
|
|
198
|
+
assert.strictEqual(metadata.version, '1.0.0', 'extra field should become metadata');
|
|
199
|
+
});
|
|
200
|
+
/**
|
|
201
|
+
* WHY: Nested conditional scopes should preserve their constraint chain.
|
|
202
|
+
* This is critical for find_guards to work correctly.
|
|
203
|
+
*/
|
|
204
|
+
it('FIXED: should preserve nested scope constraints', () => {
|
|
205
|
+
const outerScope = {
|
|
206
|
+
id: 'SCOPE:file.js:5',
|
|
207
|
+
type: 'SCOPE',
|
|
208
|
+
name: 'if_branch',
|
|
209
|
+
file: 'file.js',
|
|
210
|
+
constraints: [{ variable: 'user', operator: '!==', value: 'null' }],
|
|
211
|
+
condition: 'user !== null',
|
|
212
|
+
scopeType: 'if_statement',
|
|
213
|
+
conditional: true,
|
|
214
|
+
line: 5,
|
|
215
|
+
};
|
|
216
|
+
const innerScope = {
|
|
217
|
+
id: 'SCOPE:file.js:7',
|
|
218
|
+
type: 'SCOPE',
|
|
219
|
+
name: 'if_branch',
|
|
220
|
+
file: 'file.js',
|
|
221
|
+
constraints: [{ variable: 'user.isAdmin', operator: '===', value: 'true' }],
|
|
222
|
+
condition: 'user.isAdmin',
|
|
223
|
+
scopeType: 'if_statement',
|
|
224
|
+
conditional: true,
|
|
225
|
+
parentScope: 'SCOPE:file.js:5',
|
|
226
|
+
line: 7,
|
|
227
|
+
};
|
|
228
|
+
const outerWire = mapNodeForWireFormatFixed(outerScope);
|
|
229
|
+
const innerWire = mapNodeForWireFormatFixed(innerScope);
|
|
230
|
+
const outerMeta = JSON.parse(outerWire.metadata);
|
|
231
|
+
const innerMeta = JSON.parse(innerWire.metadata);
|
|
232
|
+
// Both scopes should preserve their constraints
|
|
233
|
+
assert.deepStrictEqual(outerMeta.constraints, [{ variable: 'user', operator: '!==', value: 'null' }]);
|
|
234
|
+
assert.deepStrictEqual(innerMeta.constraints, [{ variable: 'user.isAdmin', operator: '===', value: 'true' }]);
|
|
235
|
+
// Inner scope should reference parent
|
|
236
|
+
assert.strictEqual(innerMeta.parentScope, 'SCOPE:file.js:5');
|
|
237
|
+
});
|
|
238
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grafema/rfdb-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1-beta",
|
|
4
4
|
"description": "TypeScript client for RFDB graph database",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@msgpack/msgpack": "^3.1.3",
|
|
41
|
-
"@grafema/types": "0.
|
|
41
|
+
"@grafema/types": "0.2.1-beta"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/node": "^25.0.8",
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFDBClient Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for RFDBClient functionality that don't require a running server.
|
|
5
|
+
* Uses mock socket to test message serialization and addNodes behavior.
|
|
6
|
+
*
|
|
7
|
+
* Key tests for REG-274:
|
|
8
|
+
* - addNodes() should preserve extra fields in metadata
|
|
9
|
+
* - Extra fields like constraints, condition, scopeType should not be lost
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, beforeEach, mock } from 'node:test';
|
|
13
|
+
import assert from 'node:assert';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Mock RFDBClient that captures what would be sent to the server
|
|
17
|
+
*
|
|
18
|
+
* We can't easily mock the socket, so we test the serialization logic
|
|
19
|
+
* by extracting the node mapping logic from addNodes.
|
|
20
|
+
*/
|
|
21
|
+
function mapNodeForWireFormat(n: Record<string, unknown>): {
|
|
22
|
+
id: string;
|
|
23
|
+
nodeType: string;
|
|
24
|
+
name: string;
|
|
25
|
+
file: string;
|
|
26
|
+
exported: boolean;
|
|
27
|
+
metadata: string;
|
|
28
|
+
} {
|
|
29
|
+
// This is the CURRENT implementation - extracts only known fields
|
|
30
|
+
// Bug: extra fields are silently discarded
|
|
31
|
+
return {
|
|
32
|
+
id: String(n.id),
|
|
33
|
+
nodeType: (n.node_type || n.nodeType || n.type || 'UNKNOWN') as string,
|
|
34
|
+
name: (n.name as string) || '',
|
|
35
|
+
file: (n.file as string) || '',
|
|
36
|
+
exported: (n.exported as boolean) || false,
|
|
37
|
+
metadata: typeof n.metadata === 'string' ? n.metadata : JSON.stringify(n.metadata || {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* FIXED implementation that preserves extra fields
|
|
43
|
+
*/
|
|
44
|
+
function mapNodeForWireFormatFixed(n: Record<string, unknown>): {
|
|
45
|
+
id: string;
|
|
46
|
+
nodeType: string;
|
|
47
|
+
name: string;
|
|
48
|
+
file: string;
|
|
49
|
+
exported: boolean;
|
|
50
|
+
metadata: string;
|
|
51
|
+
} {
|
|
52
|
+
// Extract known wire format fields, rest goes to metadata
|
|
53
|
+
const { id, type, node_type, nodeType, name, file, exported, metadata, ...rest } = n;
|
|
54
|
+
|
|
55
|
+
// Merge explicit metadata with extra properties
|
|
56
|
+
const existingMeta = typeof metadata === 'string' ? JSON.parse(metadata as string) : (metadata || {});
|
|
57
|
+
const combinedMeta = { ...existingMeta, ...rest };
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
id: String(id),
|
|
61
|
+
nodeType: (node_type || nodeType || type || 'UNKNOWN') as string,
|
|
62
|
+
name: (name as string) || '',
|
|
63
|
+
file: (file as string) || '',
|
|
64
|
+
exported: (exported as boolean) || false,
|
|
65
|
+
metadata: JSON.stringify(combinedMeta),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('RFDBClient.addNodes() Metadata Preservation', () => {
|
|
70
|
+
/**
|
|
71
|
+
* WHY: JSASTAnalyzer collects constraints for SCOPE nodes during analysis.
|
|
72
|
+
* These constraints contain guard information like "someValue !== null".
|
|
73
|
+
* If constraints are lost during serialization, the graph cannot answer
|
|
74
|
+
* questions like "what conditions guard this code execution?"
|
|
75
|
+
*
|
|
76
|
+
* This test documents the BUG: constraints are silently discarded.
|
|
77
|
+
*/
|
|
78
|
+
it('BUG: current implementation loses constraints field', () => {
|
|
79
|
+
const scopeNode = {
|
|
80
|
+
id: 'SCOPE:file.js:10',
|
|
81
|
+
type: 'SCOPE',
|
|
82
|
+
name: 'if_branch',
|
|
83
|
+
file: 'file.js',
|
|
84
|
+
// Extra fields that should be preserved
|
|
85
|
+
constraints: [
|
|
86
|
+
{ variable: 'someValue', operator: '!==', value: 'null' },
|
|
87
|
+
],
|
|
88
|
+
condition: 'someValue !== null',
|
|
89
|
+
scopeType: 'if_statement',
|
|
90
|
+
conditional: true,
|
|
91
|
+
line: 10,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const wireNode = mapNodeForWireFormat(scopeNode);
|
|
95
|
+
const metadata = JSON.parse(wireNode.metadata);
|
|
96
|
+
|
|
97
|
+
// BUG: These assertions FAIL - constraints are lost
|
|
98
|
+
// Once the bug is fixed, these will pass
|
|
99
|
+
assert.strictEqual(metadata.constraints, undefined, 'BUG: constraints should be lost in current impl');
|
|
100
|
+
assert.strictEqual(metadata.condition, undefined, 'BUG: condition should be lost in current impl');
|
|
101
|
+
assert.strictEqual(metadata.scopeType, undefined, 'BUG: scopeType should be lost in current impl');
|
|
102
|
+
assert.strictEqual(metadata.conditional, undefined, 'BUG: conditional should be lost in current impl');
|
|
103
|
+
assert.strictEqual(metadata.line, undefined, 'BUG: line should be lost in current impl');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* WHY: After the fix, extra fields should be merged into metadata.
|
|
108
|
+
* This test verifies the EXPECTED behavior after REG-274 is implemented.
|
|
109
|
+
*/
|
|
110
|
+
it('FIXED: should preserve constraints in metadata', () => {
|
|
111
|
+
const scopeNode = {
|
|
112
|
+
id: 'SCOPE:file.js:10',
|
|
113
|
+
type: 'SCOPE',
|
|
114
|
+
name: 'if_branch',
|
|
115
|
+
file: 'file.js',
|
|
116
|
+
// Extra fields that should be preserved
|
|
117
|
+
constraints: [
|
|
118
|
+
{ variable: 'someValue', operator: '!==', value: 'null' },
|
|
119
|
+
],
|
|
120
|
+
condition: 'someValue !== null',
|
|
121
|
+
scopeType: 'if_statement',
|
|
122
|
+
conditional: true,
|
|
123
|
+
line: 10,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const wireNode = mapNodeForWireFormatFixed(scopeNode);
|
|
127
|
+
const metadata = JSON.parse(wireNode.metadata);
|
|
128
|
+
|
|
129
|
+
// These assertions should PASS after fix
|
|
130
|
+
assert.deepStrictEqual(
|
|
131
|
+
metadata.constraints,
|
|
132
|
+
[{ variable: 'someValue', operator: '!==', value: 'null' }],
|
|
133
|
+
'constraints should be preserved in metadata'
|
|
134
|
+
);
|
|
135
|
+
assert.strictEqual(metadata.condition, 'someValue !== null', 'condition should be preserved');
|
|
136
|
+
assert.strictEqual(metadata.scopeType, 'if_statement', 'scopeType should be preserved');
|
|
137
|
+
assert.strictEqual(metadata.conditional, true, 'conditional should be preserved');
|
|
138
|
+
assert.strictEqual(metadata.line, 10, 'line should be preserved');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* WHY: Extra fields should be MERGED with existing metadata, not replace it.
|
|
143
|
+
*/
|
|
144
|
+
it('FIXED: should merge extra fields with existing metadata', () => {
|
|
145
|
+
const node = {
|
|
146
|
+
id: 'NODE:test',
|
|
147
|
+
type: 'SCOPE',
|
|
148
|
+
name: 'test',
|
|
149
|
+
file: 'test.js',
|
|
150
|
+
metadata: { existingField: 'value', semanticId: 'test->scope' },
|
|
151
|
+
// Extra fields
|
|
152
|
+
constraints: [{ variable: 'x', operator: '>', value: '0' }],
|
|
153
|
+
newField: 'newValue',
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const wireNode = mapNodeForWireFormatFixed(node);
|
|
157
|
+
const metadata = JSON.parse(wireNode.metadata);
|
|
158
|
+
|
|
159
|
+
assert.strictEqual(metadata.existingField, 'value', 'existing metadata should be preserved');
|
|
160
|
+
assert.strictEqual(metadata.semanticId, 'test->scope', 'semanticId should be preserved');
|
|
161
|
+
assert.deepStrictEqual(
|
|
162
|
+
metadata.constraints,
|
|
163
|
+
[{ variable: 'x', operator: '>', value: '0' }],
|
|
164
|
+
'new constraints should be added'
|
|
165
|
+
);
|
|
166
|
+
assert.strictEqual(metadata.newField, 'newValue', 'new fields should be added');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* WHY: String metadata (JSON string) should be parsed and merged.
|
|
171
|
+
*/
|
|
172
|
+
it('FIXED: should handle string metadata correctly', () => {
|
|
173
|
+
const node = {
|
|
174
|
+
id: 'NODE:test',
|
|
175
|
+
type: 'CALL',
|
|
176
|
+
name: 'test',
|
|
177
|
+
file: 'test.js',
|
|
178
|
+
metadata: JSON.stringify({ callee: 'foo', args: ['a', 'b'] }),
|
|
179
|
+
// Extra field
|
|
180
|
+
resolved: true,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const wireNode = mapNodeForWireFormatFixed(node);
|
|
184
|
+
const metadata = JSON.parse(wireNode.metadata);
|
|
185
|
+
|
|
186
|
+
assert.strictEqual(metadata.callee, 'foo', 'callee from string metadata should be preserved');
|
|
187
|
+
assert.deepStrictEqual(metadata.args, ['a', 'b'], 'args from string metadata should be preserved');
|
|
188
|
+
assert.strictEqual(metadata.resolved, true, 'extra field should be merged');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* WHY: Known wire fields (id, type, name, file, exported) should NOT
|
|
193
|
+
* appear in metadata - they have their own fields in the wire format.
|
|
194
|
+
*/
|
|
195
|
+
it('FIXED: should not duplicate known fields in metadata', () => {
|
|
196
|
+
const node = {
|
|
197
|
+
id: 'NODE:test',
|
|
198
|
+
type: 'FUNCTION',
|
|
199
|
+
name: 'myFunc',
|
|
200
|
+
file: 'test.js',
|
|
201
|
+
exported: true,
|
|
202
|
+
// Only extra fields should go to metadata
|
|
203
|
+
async: true,
|
|
204
|
+
generator: false,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const wireNode = mapNodeForWireFormatFixed(node);
|
|
208
|
+
const metadata = JSON.parse(wireNode.metadata);
|
|
209
|
+
|
|
210
|
+
// Known fields should NOT be in metadata (they have dedicated wire fields)
|
|
211
|
+
assert.strictEqual(metadata.id, undefined, 'id should not be duplicated in metadata');
|
|
212
|
+
assert.strictEqual(metadata.type, undefined, 'type should not be duplicated in metadata');
|
|
213
|
+
assert.strictEqual(metadata.name, undefined, 'name should not be duplicated in metadata');
|
|
214
|
+
assert.strictEqual(metadata.file, undefined, 'file should not be duplicated in metadata');
|
|
215
|
+
assert.strictEqual(metadata.exported, undefined, 'exported should not be duplicated in metadata');
|
|
216
|
+
|
|
217
|
+
// Extra fields SHOULD be in metadata
|
|
218
|
+
assert.strictEqual(metadata.async, true, 'async should be in metadata');
|
|
219
|
+
assert.strictEqual(metadata.generator, false, 'generator should be in metadata');
|
|
220
|
+
|
|
221
|
+
// Verify wire format fields are set correctly
|
|
222
|
+
assert.strictEqual(wireNode.id, 'NODE:test');
|
|
223
|
+
assert.strictEqual(wireNode.nodeType, 'FUNCTION');
|
|
224
|
+
assert.strictEqual(wireNode.name, 'myFunc');
|
|
225
|
+
assert.strictEqual(wireNode.file, 'test.js');
|
|
226
|
+
assert.strictEqual(wireNode.exported, true);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* WHY: Empty or undefined metadata should work correctly.
|
|
231
|
+
*/
|
|
232
|
+
it('FIXED: should handle nodes without metadata', () => {
|
|
233
|
+
const node = {
|
|
234
|
+
id: 'NODE:test',
|
|
235
|
+
type: 'MODULE',
|
|
236
|
+
name: 'test',
|
|
237
|
+
file: 'test.js',
|
|
238
|
+
// No metadata field
|
|
239
|
+
version: '1.0.0',
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const wireNode = mapNodeForWireFormatFixed(node);
|
|
243
|
+
const metadata = JSON.parse(wireNode.metadata);
|
|
244
|
+
|
|
245
|
+
assert.strictEqual(metadata.version, '1.0.0', 'extra field should become metadata');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* WHY: Nested conditional scopes should preserve their constraint chain.
|
|
250
|
+
* This is critical for find_guards to work correctly.
|
|
251
|
+
*/
|
|
252
|
+
it('FIXED: should preserve nested scope constraints', () => {
|
|
253
|
+
const outerScope = {
|
|
254
|
+
id: 'SCOPE:file.js:5',
|
|
255
|
+
type: 'SCOPE',
|
|
256
|
+
name: 'if_branch',
|
|
257
|
+
file: 'file.js',
|
|
258
|
+
constraints: [{ variable: 'user', operator: '!==', value: 'null' }],
|
|
259
|
+
condition: 'user !== null',
|
|
260
|
+
scopeType: 'if_statement',
|
|
261
|
+
conditional: true,
|
|
262
|
+
line: 5,
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const innerScope = {
|
|
266
|
+
id: 'SCOPE:file.js:7',
|
|
267
|
+
type: 'SCOPE',
|
|
268
|
+
name: 'if_branch',
|
|
269
|
+
file: 'file.js',
|
|
270
|
+
constraints: [{ variable: 'user.isAdmin', operator: '===', value: 'true' }],
|
|
271
|
+
condition: 'user.isAdmin',
|
|
272
|
+
scopeType: 'if_statement',
|
|
273
|
+
conditional: true,
|
|
274
|
+
parentScope: 'SCOPE:file.js:5',
|
|
275
|
+
line: 7,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const outerWire = mapNodeForWireFormatFixed(outerScope);
|
|
279
|
+
const innerWire = mapNodeForWireFormatFixed(innerScope);
|
|
280
|
+
|
|
281
|
+
const outerMeta = JSON.parse(outerWire.metadata);
|
|
282
|
+
const innerMeta = JSON.parse(innerWire.metadata);
|
|
283
|
+
|
|
284
|
+
// Both scopes should preserve their constraints
|
|
285
|
+
assert.deepStrictEqual(
|
|
286
|
+
outerMeta.constraints,
|
|
287
|
+
[{ variable: 'user', operator: '!==', value: 'null' }]
|
|
288
|
+
);
|
|
289
|
+
assert.deepStrictEqual(
|
|
290
|
+
innerMeta.constraints,
|
|
291
|
+
[{ variable: 'user.isAdmin', operator: '===', value: 'true' }]
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
// Inner scope should reference parent
|
|
295
|
+
assert.strictEqual(innerMeta.parentScope, 'SCOPE:file.js:5');
|
|
296
|
+
});
|
|
297
|
+
});
|
package/ts/client.ts
CHANGED
|
@@ -161,16 +161,29 @@ export class RFDBClient extends EventEmitter implements IRFDBClient {
|
|
|
161
161
|
|
|
162
162
|
/**
|
|
163
163
|
* Add nodes to the graph
|
|
164
|
+
* Extra properties beyond id/type/name/file/exported/metadata are merged into metadata
|
|
164
165
|
*/
|
|
165
166
|
async addNodes(nodes: Array<Partial<WireNode> & { id: string; type?: string; node_type?: string; nodeType?: string }>): Promise<RFDBResponse> {
|
|
166
|
-
const wireNodes: WireNode[] = nodes.map(n =>
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
167
|
+
const wireNodes: WireNode[] = nodes.map(n => {
|
|
168
|
+
// Cast to Record to allow iteration over extra properties
|
|
169
|
+
const nodeRecord = n as Record<string, unknown>;
|
|
170
|
+
|
|
171
|
+
// Extract known wire format fields, rest goes to metadata
|
|
172
|
+
const { id, type, node_type, nodeType, name, file, exported, metadata, ...rest } = nodeRecord;
|
|
173
|
+
|
|
174
|
+
// Merge explicit metadata with extra properties
|
|
175
|
+
const existingMeta = typeof metadata === 'string' ? JSON.parse(metadata as string) : (metadata || {});
|
|
176
|
+
const combinedMeta = { ...existingMeta, ...rest };
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
id: String(id),
|
|
180
|
+
nodeType: (node_type || nodeType || type || 'UNKNOWN') as NodeType,
|
|
181
|
+
name: (name as string) || '',
|
|
182
|
+
file: (file as string) || '',
|
|
183
|
+
exported: (exported as boolean) || false,
|
|
184
|
+
metadata: JSON.stringify(combinedMeta),
|
|
185
|
+
};
|
|
186
|
+
});
|
|
174
187
|
|
|
175
188
|
return this._send('addNodes', { nodes: wireNodes });
|
|
176
189
|
}
|