@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 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;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../ts/client.ts"],"names":[],"mappings":"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"}
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
- id: String(n.id),
131
- nodeType: (n.node_type || n.nodeType || n.type || 'UNKNOWN'),
132
- name: n.name || '',
133
- file: n.file || '',
134
- exported: n.exported || false,
135
- metadata: typeof n.metadata === 'string' ? n.metadata : JSON.stringify(n.metadata || {}),
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.1.1-alpha",
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.1.1-alpha"
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
- id: String(n.id),
168
- nodeType: (n.node_type || n.nodeType || n.type || 'UNKNOWN') as NodeType,
169
- name: n.name || '',
170
- file: n.file || '',
171
- exported: n.exported || false,
172
- metadata: typeof n.metadata === 'string' ? n.metadata : JSON.stringify(n.metadata || {}),
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
  }