smiles-js 2.0.3 → 2.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.
Files changed (49) hide show
  1. package/API.md +162 -0
  2. package/README.md +39 -0
  3. package/docs/MIRROR_PLAN.md +204 -0
  4. package/docs/smiles.peggy +215 -0
  5. package/package.json +1 -1
  6. package/scripts/coverage-summary.js +1 -1
  7. package/src/codegen/branch-crossing-ring.js +27 -6
  8. package/src/codegen/interleaved-fused-ring.js +24 -0
  9. package/src/decompiler.js +236 -51
  10. package/src/decompiler.test.js +232 -60
  11. package/src/fragment.test.js +7 -2
  12. package/src/manipulation.js +409 -4
  13. package/src/manipulation.test.js +359 -1
  14. package/src/method-attachers.js +37 -8
  15. package/src/node-creators.js +7 -0
  16. package/src/parser/ast-builder.js +23 -8
  17. package/src/parser/ring-group-builder.js +14 -2
  18. package/src/parser/ring-utils.js +28 -0
  19. package/test-integration/__snapshots__/acetaminophen.test.js.snap +20 -0
  20. package/test-integration/__snapshots__/adjuvant-analgesics.test.js.snap +63 -1
  21. package/test-integration/__snapshots__/cholesterol-drugs.test.js.snap +437 -0
  22. package/test-integration/__snapshots__/dexamethasone.test.js.snap +31 -0
  23. package/test-integration/__snapshots__/endocannabinoids.test.js.snap +79 -2
  24. package/test-integration/__snapshots__/endogenous-opioids.test.js.snap +1116 -0
  25. package/test-integration/__snapshots__/hypertension-medication.test.js.snap +70 -1
  26. package/test-integration/__snapshots__/local-anesthetics.test.js.snap +97 -0
  27. package/test-integration/__snapshots__/nsaids-otc.test.js.snap +61 -1
  28. package/test-integration/__snapshots__/nsaids-prescription.test.js.snap +115 -2
  29. package/test-integration/__snapshots__/opioids.test.js.snap +113 -4
  30. package/test-integration/__snapshots__/steroids.test.js.snap +381 -2
  31. package/test-integration/acetaminophen.test.js +15 -3
  32. package/test-integration/adjuvant-analgesics.test.js +43 -7
  33. package/test-integration/cholesterol-drugs.test.js +127 -20
  34. package/test-integration/cholesterol.test.js +112 -0
  35. package/test-integration/dexamethasone.test.js +8 -2
  36. package/test-integration/endocannabinoids.test.js +48 -12
  37. package/test-integration/endogenous-opioids.smiles.js +32 -0
  38. package/test-integration/endogenous-opioids.test.js +192 -0
  39. package/test-integration/hypertension-medication.test.js +32 -8
  40. package/test-integration/local-anesthetics.smiles.js +33 -0
  41. package/test-integration/local-anesthetics.test.js +64 -16
  42. package/test-integration/mirror.test.js +151 -0
  43. package/test-integration/nsaids-otc.test.js +40 -10
  44. package/test-integration/nsaids-prescription.test.js +72 -18
  45. package/test-integration/opioids.test.js +56 -14
  46. package/test-integration/polymer.test.js +148 -0
  47. package/test-integration/steroids.test.js +112 -28
  48. package/test-integration/utils.js +4 -2
  49. package/todo +2 -3
@@ -15,42 +15,42 @@ function rawFusedRing(rings) {
15
15
  return { type: 'fused_ring', rings };
16
16
  }
17
17
 
18
- describe('Decompiler - Ring', () => {
18
+ describe('Decompiler - Ring (verbose)', () => {
19
19
  test('decompiles simple ring', () => {
20
20
  const benzene = Ring({ atoms: 'c', size: 6 });
21
- const code = decompile(benzene);
21
+ const code = decompile(benzene, { verbose: true });
22
22
  expect(code).toBe("export const v1 = Ring({ atoms: 'c', size: 6 });");
23
23
  });
24
24
 
25
25
  test('decompiles ring with custom ring number', () => {
26
26
  const ring = Ring({ atoms: 'C', size: 6, ringNumber: 2 });
27
- const code = decompile(ring);
27
+ const code = decompile(ring, { verbose: true });
28
28
  expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 6, ringNumber: 2 });");
29
29
  });
30
30
 
31
31
  test('decompiles ring with offset', () => {
32
32
  const ring = Ring({ atoms: 'C', size: 6, offset: 2 });
33
- const code = decompile(ring);
33
+ const code = decompile(ring, { verbose: true });
34
34
  expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 6, offset: 2 });");
35
35
  });
36
36
 
37
37
  test('decompiles ring with bonds', () => {
38
38
  const ring = Ring({ atoms: 'C', size: 6, bonds: ['=', null, '=', null, '='] });
39
- const code = decompile(ring);
39
+ const code = decompile(ring, { verbose: true });
40
40
  expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 6, bonds: ['=', null, '=', null, '='] });");
41
41
  });
42
42
 
43
43
  test('decompiles ring with branchDepths metadata', () => {
44
44
  const ring = Ring({ atoms: 'C', size: 6 });
45
45
  ring.metaBranchDepths = [0, 0, 0, 1, 1, 1];
46
- const code = decompile(ring);
46
+ const code = decompile(ring, { verbose: true });
47
47
  expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 6, branchDepths: [0, 0, 0, 1, 1, 1] });");
48
48
  });
49
49
 
50
50
  test('decompiles ring with substitutions', () => {
51
51
  const ring = Ring({ atoms: 'C', size: 6 });
52
52
  const substituted = ring.substitute(2, 'N');
53
- const code = decompile(substituted);
53
+ const code = decompile(substituted, { verbose: true });
54
54
  expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
55
55
  export const v2 = v1.substitute(2, 'N');`);
56
56
  });
@@ -59,7 +59,7 @@ export const v2 = v1.substitute(2, 'N');`);
59
59
  const ring = Ring({ atoms: 'C', size: 6 });
60
60
  const methyl = Linear(['C']);
61
61
  const attached = ring.attach(1, methyl);
62
- const code = decompile(attached);
62
+ const code = decompile(attached, { verbose: true });
63
63
  expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
64
64
  export const v2 = Linear(['C']);
65
65
  export const v3 = v1.attach(1, v2);`);
@@ -71,7 +71,7 @@ export const v3 = v1.attach(1, v2);`);
71
71
  const branch2 = Linear(['O']);
72
72
  const withAttach = ring.attach(2, branch1);
73
73
  const withBoth = withAttach.attach(4, branch2);
74
- const code = decompile(withBoth);
74
+ const code = decompile(withBoth, { verbose: true });
75
75
  expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
76
76
  export const v2 = Linear(['N']);
77
77
  export const v3 = v1.attach(2, v2);
@@ -79,35 +79,98 @@ export const v4 = Linear(['O']);
79
79
  export const v5 = v3.attach(4, v4);`);
80
80
  });
81
81
 
82
- test('uses toCode() method', () => {
82
+ test('uses toCode() method with verbose', () => {
83
83
  const benzene = Ring({ atoms: 'c', size: 6 });
84
- expect(benzene.toCode()).toBe("export const ring1 = Ring({ atoms: 'c', size: 6 });");
85
- expect(benzene.toCode('r')).toBe("export const r1 = Ring({ atoms: 'c', size: 6 });");
84
+ expect(benzene.toCode('ring', { verbose: true })).toBe("export const ring1 = Ring({ atoms: 'c', size: 6 });");
85
+ expect(benzene.toCode('r', { verbose: true })).toBe("export const r1 = Ring({ atoms: 'c', size: 6 });");
86
86
  });
87
87
  });
88
88
 
89
- describe('Decompiler - Linear', () => {
89
+ describe('Decompiler - Ring (non-verbose)', () => {
90
+ test('decompiles simple ring as Fragment', () => {
91
+ const benzene = Ring({ atoms: 'c', size: 6 });
92
+ const code = decompile(benzene);
93
+ expect(code).toBe("export const v1 = Fragment('c1ccccc1');");
94
+ });
95
+
96
+ test('decompiles ring with custom ring number as Fragment', () => {
97
+ const ring = Ring({ atoms: 'C', size: 6, ringNumber: 2 });
98
+ const code = decompile(ring);
99
+ expect(code).toBe("export const v1 = Fragment('C2CCCCC2');");
100
+ });
101
+
102
+ test('decompiles ring with bonds as Fragment', () => {
103
+ const ring = Ring({ atoms: 'C', size: 6, bonds: ['=', null, '=', null, '='] });
104
+ const code = decompile(ring);
105
+ expect(code).toBe("export const v1 = Fragment('C1=CC=CC=C1');");
106
+ });
107
+
108
+ test('decompiles ring with branchDepths as Fragment', () => {
109
+ const ring = Ring({ atoms: 'C', size: 6 });
110
+ ring.metaBranchDepths = [0, 0, 0, 1, 1, 1];
111
+ const code = decompile(ring);
112
+ expect(code).toBe("export const v1 = Fragment('C1CC(CCC1)');");
113
+ });
114
+
115
+ test('decompiles ring with substitutions as Fragment', () => {
116
+ const ring = Ring({ atoms: 'C', size: 6 });
117
+ const substituted = ring.substitute(2, 'N');
118
+ const code = decompile(substituted);
119
+ expect(code).toBe(`export const v1 = Fragment('C1CCCCC1');
120
+ export const v2 = Fragment('C1NCCCC1');`);
121
+ });
122
+
123
+ test('decompiles ring with single attachment using Fragment', () => {
124
+ const ring = Ring({ atoms: 'C', size: 6 });
125
+ const methyl = Linear(['C']);
126
+ const attached = ring.attach(1, methyl);
127
+ const code = decompile(attached);
128
+ expect(code).toBe(`export const v1 = Fragment('C1CCCCC1');
129
+ export const v2 = Fragment('C');
130
+ export const v3 = v1.attach(1, v2);`);
131
+ });
132
+
133
+ test('decompiles ring with substitution and attachment using Fragment', () => {
134
+ const ring = Ring({ atoms: 'C', size: 6 });
135
+ const substituted = ring.substitute(2, 'N');
136
+ const branch = Linear(['O']);
137
+ const attached = substituted.attach(1, branch);
138
+ const code = decompile(attached);
139
+ expect(code).toBe(`export const v1 = Fragment('C1CCCCC1');
140
+ export const v2 = Fragment('C1NCCCC1');
141
+ export const v3 = Fragment('O');
142
+ export const v4 = v2.attach(1, v3);`);
143
+ });
144
+
145
+ test('uses toCode() non-verbose by default', () => {
146
+ const benzene = Ring({ atoms: 'c', size: 6 });
147
+ expect(benzene.toCode()).toBe("export const ring1 = Fragment('c1ccccc1');");
148
+ expect(benzene.toCode('r')).toBe("export const r1 = Fragment('c1ccccc1');");
149
+ });
150
+ });
151
+
152
+ describe('Decompiler - Linear (verbose)', () => {
90
153
  test('decompiles simple linear chain', () => {
91
154
  const propane = Linear(['C', 'C', 'C']);
92
- const code = decompile(propane);
155
+ const code = decompile(propane, { verbose: true });
93
156
  expect(code).toBe("export const v1 = Linear(['C', 'C', 'C']);");
94
157
  });
95
158
 
96
159
  test('decompiles single-atom linear', () => {
97
160
  const linear = Linear(['C']);
98
- const code = decompile(linear);
161
+ const code = decompile(linear, { verbose: true });
99
162
  expect(code).toBe("export const v1 = Linear(['C']);");
100
163
  });
101
164
 
102
165
  test('decompiles linear with bonds', () => {
103
166
  const ethene = Linear(['C', 'C'], ['=']);
104
- const code = decompile(ethene);
167
+ const code = decompile(ethene, { verbose: true });
105
168
  expect(code).toBe("export const v1 = Linear(['C', 'C'], ['=']);");
106
169
  });
107
170
 
108
171
  test('decompiles linear with different atom types and bond', () => {
109
172
  const linear = Linear(['C', 'N'], ['=']);
110
- const code = decompile(linear);
173
+ const code = decompile(linear, { verbose: true });
111
174
  expect(code).toBe("export const v1 = Linear(['C', 'N'], ['=']);");
112
175
  });
113
176
 
@@ -115,27 +178,69 @@ describe('Decompiler - Linear', () => {
115
178
  const chain = Linear(['C', 'C', 'C']);
116
179
  const branch = Linear(['N']);
117
180
  const attached = chain.attach(2, branch);
118
- const code = decompile(attached);
181
+ const code = decompile(attached, { verbose: true });
119
182
  expect(code).toBe(`export const v1 = Linear(['C', 'C', 'C']);
120
183
  export const v2 = Linear(['N']);
121
184
  export const v3 = v1.attach(2, v2);`);
122
185
  });
123
186
 
124
- test('uses toCode() method', () => {
187
+ test('uses toCode() method with verbose', () => {
188
+ const propane = Linear(['C', 'C', 'C']);
189
+ expect(propane.toCode('linear', { verbose: true })).toBe("export const linear1 = Linear(['C', 'C', 'C']);");
190
+ expect(propane.toCode('c', { verbose: true })).toBe("export const c1 = Linear(['C', 'C', 'C']);");
191
+ });
192
+ });
193
+
194
+ describe('Decompiler - Linear (non-verbose)', () => {
195
+ test('decompiles simple linear as Fragment', () => {
196
+ const propane = Linear(['C', 'C', 'C']);
197
+ const code = decompile(propane);
198
+ expect(code).toBe("export const v1 = Fragment('CCC');");
199
+ });
200
+
201
+ test('decompiles single-atom linear as Fragment', () => {
202
+ const linear = Linear(['C']);
203
+ const code = decompile(linear);
204
+ expect(code).toBe("export const v1 = Fragment('C');");
205
+ });
206
+
207
+ test('keeps linear with bonds as Linear (not Fragment)', () => {
208
+ const ethene = Linear(['C', 'C'], ['=']);
209
+ const code = decompile(ethene);
210
+ expect(code).toBe("export const v1 = Linear(['C', 'C'], ['=']);");
211
+ });
212
+
213
+ test('keeps linear with leadingBond as Linear (not Fragment)', () => {
214
+ const linear = Linear(['C', 'C'], [], {}, '=');
215
+ const code = decompile(linear);
216
+ expect(code).toBe("export const v1 = Linear(['C', 'C'], [], {}, '=');");
217
+ });
218
+
219
+ test('decompiles linear with attachments using Fragment', () => {
220
+ const chain = Linear(['C', 'C', 'C']);
221
+ const branch = Linear(['N']);
222
+ const attached = chain.attach(2, branch);
223
+ const code = decompile(attached);
224
+ expect(code).toBe(`export const v1 = Fragment('CCC');
225
+ export const v2 = Fragment('N');
226
+ export const v3 = v1.attach(2, v2);`);
227
+ });
228
+
229
+ test('uses toCode() non-verbose by default', () => {
125
230
  const propane = Linear(['C', 'C', 'C']);
126
- expect(propane.toCode()).toBe("export const linear1 = Linear(['C', 'C', 'C']);");
127
- expect(propane.toCode('c')).toBe("export const c1 = Linear(['C', 'C', 'C']);");
231
+ expect(propane.toCode()).toBe("export const linear1 = Fragment('CCC');");
232
+ expect(propane.toCode('c')).toBe("export const c1 = Fragment('CCC');");
128
233
  });
129
234
  });
130
235
 
131
- describe('Decompiler - FusedRing (simple path)', () => {
236
+ describe('Decompiler - FusedRing (simple path, verbose)', () => {
132
237
  test('decompiles 2-ring fused ring system via rawFusedRing', () => {
133
238
  const ring1 = Ring({ atoms: 'C', size: 10, ringNumber: 1 });
134
239
  const ring2 = Ring({
135
240
  atoms: 'C', size: 6, ringNumber: 2, offset: 2,
136
241
  });
137
242
  const fusedRing = rawFusedRing([ring1, ring2]);
138
- const code = decompile(fusedRing);
243
+ const code = decompile(fusedRing, { verbose: true });
139
244
  expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 10 });
140
245
  export const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 2 });
141
246
  export const v3 = v1.fuse(2, v2);`);
@@ -148,7 +253,7 @@ export const v3 = v1.fuse(2, v2);`);
148
253
  });
149
254
  const fused = rawFusedRing([ring1, ring2]);
150
255
  fused.metaLeadingBond = '=';
151
- const code = decompile(fused);
256
+ const code = decompile(fused, { verbose: true });
152
257
  expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
153
258
  export const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 1 });
154
259
  export const v3 = v1.fuse(1, v2, { leadingBond: '=' });`);
@@ -164,7 +269,7 @@ export const v3 = v1.fuse(1, v2, { leadingBond: '=' });`);
164
269
  });
165
270
  const fused = rawFusedRing([ring1, ring2, ring3]);
166
271
  fused.metaLeadingBond = '#';
167
- const code = decompile(fused);
272
+ const code = decompile(fused, { verbose: true });
168
273
  expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
169
274
  export const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 1 });
170
275
  export const v3 = Ring({ atoms: 'C', size: 6, ringNumber: 3, offset: 2 });
@@ -181,7 +286,7 @@ export const v5 = v4.addRing(2, v3);`);
181
286
  atoms: 'C', size: 6, ringNumber: 3, offset: 2,
182
287
  });
183
288
  const fused = rawFusedRing([ring1, ring2, ring3]);
184
- const code = decompile(fused);
289
+ const code = decompile(fused, { verbose: true });
185
290
  expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
186
291
  export const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 1 });
187
292
  export const v3 = Ring({ atoms: 'C', size: 6, ringNumber: 3, offset: 2 });
@@ -192,14 +297,14 @@ export const v5 = v4.addRing(2, v3);`);
192
297
  test('decompiles single-ring FusedRing via simple path', () => {
193
298
  const ring1 = Ring({ atoms: 'C', size: 6 });
194
299
  const fused = { type: 'fused_ring', rings: [ring1] };
195
- const code = decompile(fused);
300
+ const code = decompile(fused, { verbose: true });
196
301
  expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 6 });");
197
302
  });
198
303
 
199
304
  test('decompiles single-ring FusedRing with empty sequential rings', () => {
200
305
  const ring1 = Ring({ atoms: 'C', size: 6 });
201
306
  const fused = { type: 'fused_ring', rings: [ring1], metaSequentialRings: [] };
202
- const code = decompile(fused);
307
+ const code = decompile(fused, { verbose: true });
203
308
  expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 6 });");
204
309
  });
205
310
 
@@ -210,14 +315,35 @@ export const v5 = v4.addRing(2, v3);`);
210
315
  });
211
316
  const fused = rawFusedRing([ring1, ring2]);
212
317
  fused.metaSequentialRings = [];
213
- const code = decompile(fused);
318
+ const code = decompile(fused, { verbose: true });
214
319
  expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
215
320
  export const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 1 });
216
321
  export const v3 = v1.fuse(1, v2);`);
217
322
  });
218
323
  });
219
324
 
220
- describe('Decompiler - FusedRing (interleaved/complex path)', () => {
325
+ describe('Decompiler - FusedRing (simple path, non-verbose)', () => {
326
+ test('decompiles 2-ring rawFusedRing as Fragment', () => {
327
+ const ring1 = Ring({ atoms: 'C', size: 10, ringNumber: 1 });
328
+ const ring2 = Ring({
329
+ atoms: 'C', size: 6, ringNumber: 2, offset: 2,
330
+ });
331
+ const fusedRing = rawFusedRing([ring1, ring2]);
332
+ const code = decompile(fusedRing);
333
+ expect(code).toBe(`export const v1 = Fragment('C1CCCCCCCCC1');
334
+ export const v2 = Fragment('C2CCCCC2');
335
+ export const v3 = v1.fuse(2, v2);`);
336
+ });
337
+
338
+ test('decompiles single-ring FusedRing as Fragment', () => {
339
+ const ring1 = Ring({ atoms: 'C', size: 6 });
340
+ const fused = { type: 'fused_ring', rings: [ring1] };
341
+ const code = decompile(fused);
342
+ expect(code).toBe("export const v1 = Fragment('C1CCCCC1');");
343
+ });
344
+ });
345
+
346
+ describe('Decompiler - FusedRing (interleaved/complex path, verbose)', () => {
221
347
  // When .fuse() produces parser metadata (interleaved positions), the decompiler
222
348
  // should emit FusedRing({ metadata: { rings: [...] } }) with hierarchical colocated atoms.
223
349
  // No scattered Maps, no mutations.
@@ -230,7 +356,7 @@ describe('Decompiler - FusedRing (interleaved/complex path)', () => {
230
356
  const ring1 = Ring({ atoms: 'C', size: 6 });
231
357
  const ring2 = Ring({ atoms: 'C', size: 6, ringNumber: 2 });
232
358
  const fused = ring1.fuse(1, ring2);
233
- const code = decompile(fused);
359
+ const code = decompile(fused, { verbose: true });
234
360
  expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
235
361
  export const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 1 });
236
362
  export const v3 = FusedRing({ metadata: { rings: [{ ring: v1, start: 0, end: 9, atoms: [{ position: 0, depth: 0 }, { position: 1, depth: 0 }, { position: 6, depth: 0 }, { position: 7, depth: 0 }, { position: 8, depth: 0 }, { position: 9, depth: 0 }] }, { ring: v2, start: 1, end: 6, atoms: [{ position: 1, depth: 0 }, { position: 2, depth: 0 }, { position: 3, depth: 0 }, { position: 4, depth: 0 }, { position: 5, depth: 0 }, { position: 6, depth: 0 }] }] } });`);
@@ -241,7 +367,7 @@ export const v3 = FusedRing({ metadata: { rings: [{ ring: v1, start: 0, end: 9,
241
367
  const ring2 = Ring({ atoms: 'C', size: 6, ringNumber: 2 });
242
368
  const fused = ring1.fuse(1, ring2);
243
369
  fused.metaLeadingBond = '=';
244
- const code = decompile(fused);
370
+ const code = decompile(fused, { verbose: true });
245
371
  expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
246
372
  export const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 1 });
247
373
  export const v3 = FusedRing({ metadata: { leadingBond: '=', rings: [{ ring: v1, start: 0, end: 9, atoms: [{ position: 0, depth: 0 }, { position: 1, depth: 0 }, { position: 6, depth: 0 }, { position: 7, depth: 0 }, { position: 8, depth: 0 }, { position: 9, depth: 0 }] }, { ring: v2, start: 1, end: 6, atoms: [{ position: 1, depth: 0 }, { position: 2, depth: 0 }, { position: 3, depth: 0 }, { position: 4, depth: 0 }, { position: 5, depth: 0 }, { position: 6, depth: 0 }] }] } });`);
@@ -273,7 +399,7 @@ export const v3 = FusedRing({ metadata: { leadingBond: '=', rings: [{ ring: v1,
273
399
  atoms: [{ position: 12, depth: 0, value: 'O' }],
274
400
  },
275
401
  });
276
- const code = decompile(fused);
402
+ const code = decompile(fused, { verbose: true });
277
403
  // Attachments stay as .attach() calls on rings.
278
404
  // FusedRing uses hierarchical metadata with the extra atom (position 12, value 'O') colocated.
279
405
  expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
@@ -307,7 +433,7 @@ export const v5 = FusedRing({ metadata: { rings: [{ ring: v3, start: 0, end: 9,
307
433
  atoms: [{ position: 12, depth: 0, value: 'N' }],
308
434
  },
309
435
  });
310
- const code = decompile(fused);
436
+ const code = decompile(fused, { verbose: true });
311
437
  expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
312
438
  export const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 1 });
313
439
  export const v3 = FusedRing({ metadata: { rings: [{ ring: v1, start: 0, end: 9, atoms: [{ position: 0, depth: 0 }, { position: 1, depth: 0 }, { position: 6, depth: 0 }, { position: 7, depth: 0 }, { position: 8, depth: 0 }, { position: 9, depth: 0 }] }, { ring: v2, start: 1, end: 6, atoms: [{ position: 1, depth: 0 }, { position: 2, depth: 0 }, { position: 3, depth: 0 }, { position: 4, depth: 0 }, { position: 5, depth: 0 }, { position: 6, depth: 0 }] }], atoms: [{ position: 12, depth: 0, value: 'N' }] } });`);
@@ -318,14 +444,14 @@ export const v3 = FusedRing({ metadata: { rings: [{ ring: v1, start: 0, end: 9,
318
444
  const fused = ring1.addSequentialRings([]);
319
445
  fused.metaBranchDepthMap.set(5, 0);
320
446
  fused.metaBondMap.set(5, null);
321
- const code = decompile(fused, { includeMetadata: true });
447
+ const code = decompile(fused, { verbose: true, includeMetadata: true });
322
448
  expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 6 });");
323
449
  });
324
450
 
325
451
  test('decompiles single-ring fused with empty maps returns just Ring', () => {
326
452
  const ring1 = Ring({ atoms: 'C', size: 6 });
327
453
  const fused = ring1.addSequentialRings([]);
328
- const code = decompile(fused, { includeMetadata: true });
454
+ const code = decompile(fused, { verbose: true, includeMetadata: true });
329
455
  expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 6 });");
330
456
  });
331
457
 
@@ -335,12 +461,12 @@ export const v3 = FusedRing({ metadata: { rings: [{ ring: v1, start: 0, end: 9,
335
461
  const fused = ring1.fuse(1, ring2);
336
462
  fused.metaSequentialRings = [];
337
463
  fused.rings = [ring1];
338
- const code = decompile(fused);
464
+ const code = decompile(fused, { verbose: true });
339
465
  expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 6 });");
340
466
  });
341
467
  });
342
468
 
343
- describe('Decompiler - FusedRing (sequential rings)', () => {
469
+ describe('Decompiler - FusedRing (sequential rings, verbose)', () => {
344
470
  // Sequential rings: all const, no let+reassignment.
345
471
  // Depth colocated per-ring as { ring: v, depth: N }. Depth 0 is default (omitted).
346
472
  // chainAtoms: depth always explicit.
@@ -351,7 +477,7 @@ describe('Decompiler - FusedRing (sequential rings)', () => {
351
477
  const fused = ring1.fuse(1, ring2);
352
478
  const seqRing = Ring({ atoms: 'C', size: 5, ringNumber: 3 });
353
479
  const result = fused.addSequentialRings([seqRing]);
354
- const code = decompile(result);
480
+ const code = decompile(result, { verbose: true });
355
481
  // const for fuse, new const for addSequentialRings, depth 0 omitted
356
482
  expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
357
483
  export const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 1 });
@@ -366,7 +492,7 @@ export const v5 = v3.addSequentialRings([{ ring: v4 }]);`);
366
492
  const fused = ring1.fuse(1, ring2);
367
493
  const seqRing = Ring({ atoms: 'C', size: 5, ringNumber: 3 });
368
494
  const result = fused.addSequentialRings([seqRing]);
369
- const code = decompile(result, { includeMetadata: true });
495
+ const code = decompile(result, { verbose: true, includeMetadata: true });
370
496
  // depth 0 is default, so omitted in the ring entry
371
497
  expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
372
498
  export const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 1 });
@@ -379,7 +505,7 @@ export const v5 = v3.addSequentialRings([{ ring: v4 }]);`);
379
505
  const ring1 = Ring({ atoms: 'C', size: 6 });
380
506
  const seqRing = Ring({ atoms: 'C', size: 5, ringNumber: 2 });
381
507
  const result = ring1.addSequentialRings([seqRing]);
382
- const code = decompile(result, { includeMetadata: true });
508
+ const code = decompile(result, { verbose: true, includeMetadata: true });
383
509
  // Single base ring + sequential ring, all depth 0 (omitted), all const
384
510
  expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
385
511
  export const v2 = Ring({ atoms: 'C', size: 5, ringNumber: 2 });
@@ -395,7 +521,7 @@ export const v3 = v1.addSequentialRings([{ ring: v2 }]);`);
395
521
  atom: 'N', depth: 0, position: 'after', attachments: [attachment],
396
522
  }],
397
523
  });
398
- const code = decompile(result, { includeMetadata: true });
524
+ const code = decompile(result, { verbose: true, includeMetadata: true });
399
525
  // chainAtoms depth always explicit, attachment declared before addSequentialRings
400
526
  expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
401
527
  export const v2 = Ring({ atoms: 'C', size: 5, ringNumber: 2 });
@@ -409,7 +535,7 @@ export const v4 = v1.addSequentialRings([{ ring: v2 }], { chainAtoms: [{ atom: '
409
535
  const result = ring1.addSequentialRings([seqRing]);
410
536
  // Position 10 is within the sequential ring — atom value here doesn't need chainAtom
411
537
  result.metaAtomValueMap.set(10, 'N');
412
- const code = decompile(result, { includeMetadata: true });
538
+ const code = decompile(result, { verbose: true, includeMetadata: true });
413
539
  expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
414
540
  export const v2 = Ring({ atoms: 'C', size: 5, ringNumber: 2 });
415
541
  export const v3 = v1.addSequentialRings([{ ring: v2 }]);`);
@@ -423,7 +549,7 @@ export const v3 = v1.addSequentialRings([{ ring: v2 }]);`);
423
549
  result.metaBondMap.set(1, '=');
424
550
  result.metaBondMap.set(2, null);
425
551
  result.metaBondMap.set(3, '#');
426
- const code = decompile(result, { includeMetadata: true });
552
+ const code = decompile(result, { verbose: true, includeMetadata: true });
427
553
  expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
428
554
  export const v2 = Ring({ atoms: 'C', size: 5, ringNumber: 2 });
429
555
  export const v3 = v1.addSequentialRings([{ ring: v2 }]);`);
@@ -437,7 +563,7 @@ export const v3 = v1.addSequentialRings([{ ring: v2 }]);`);
437
563
  const result = ring1.addSequentialRings([seqRing]);
438
564
  // Attachments at base ring position 3 don't generate chainAtoms
439
565
  result.metaSeqAtomAttachments.set(3, [att1, att2]);
440
- const code = decompile(result, { includeMetadata: true });
566
+ const code = decompile(result, { verbose: true, includeMetadata: true });
441
567
  expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
442
568
  export const v2 = Ring({ atoms: 'C', size: 5, ringNumber: 2 });
443
569
  export const v3 = v1.addSequentialRings([{ ring: v2 }]);`);
@@ -456,7 +582,7 @@ export const v3 = v1.addSequentialRings([{ ring: v2 }]);`);
456
582
  const fused = FusedRing([ring1, ring2, ring3]);
457
583
  const seqRing = Ring({ atoms: 'C', size: 5, ringNumber: 4 });
458
584
  const result = fused.addSequentialRings([seqRing]);
459
- const code = decompile(result, { includeMetadata: true });
585
+ const code = decompile(result, { verbose: true, includeMetadata: true });
460
586
  // All const, new const for addSequentialRings, depth 0 omitted
461
587
  expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
462
588
  export const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 3 });
@@ -467,12 +593,12 @@ export const v6 = v4.addSequentialRings([{ ring: v5 }]);`);
467
593
  });
468
594
  });
469
595
 
470
- describe('Decompiler - Molecule', () => {
596
+ describe('Decompiler - Molecule (verbose)', () => {
471
597
  test('decompiles molecule with multiple components', () => {
472
598
  const propyl = Linear(['C', 'C', 'C']);
473
599
  const benzene = Ring({ atoms: 'c', size: 6 });
474
600
  const molecule = Molecule([propyl, benzene]);
475
- const code = decompile(molecule);
601
+ const code = decompile(molecule, { verbose: true });
476
602
  expect(code).toBe(`export const v1 = Linear(['C', 'C', 'C']);
477
603
  export const v2 = Ring({ atoms: 'c', size: 6 });
478
604
  export const v3 = Molecule([v1, v2]);`);
@@ -480,38 +606,66 @@ export const v3 = Molecule([v1, v2]);`);
480
606
 
481
607
  test('decompiles empty molecule', () => {
482
608
  const empty = Molecule([]);
483
- const code = decompile(empty);
609
+ const code = decompile(empty, { verbose: true });
484
610
  expect(code).toBe('export const v1 = Molecule([]);');
485
611
  });
486
612
 
487
- test('uses toCode() method', () => {
613
+ test('uses toCode() method with verbose', () => {
488
614
  const propyl = Linear(['C', 'C', 'C']);
489
615
  const benzene = Ring({ atoms: 'c', size: 6 });
490
616
  const molecule = Molecule([propyl, benzene]);
491
- const code = molecule.toCode();
617
+ const code = molecule.toCode('molecule', { verbose: true });
492
618
  expect(code).toBe(`export const molecule1 = Linear(['C', 'C', 'C']);
493
619
  export const molecule2 = Ring({ atoms: 'c', size: 6 });
494
620
  export const molecule3 = Molecule([molecule1, molecule2]);`);
495
621
  });
496
622
  });
497
623
 
624
+ describe('Decompiler - Molecule (non-verbose)', () => {
625
+ test('decompiles molecule with Fragment', () => {
626
+ const propyl = Linear(['C', 'C', 'C']);
627
+ const benzene = Ring({ atoms: 'c', size: 6 });
628
+ const molecule = Molecule([propyl, benzene]);
629
+ const code = decompile(molecule);
630
+ expect(code).toBe(`export const v1 = Fragment('CCC');
631
+ export const v2 = Fragment('c1ccccc1');
632
+ export const v3 = Molecule([v1, v2]);`);
633
+ });
634
+
635
+ test('decompiles empty molecule same as verbose', () => {
636
+ const empty = Molecule([]);
637
+ const code = decompile(empty);
638
+ expect(code).toBe('export const v1 = Molecule([]);');
639
+ });
640
+
641
+ test('uses toCode() non-verbose by default', () => {
642
+ const propyl = Linear(['C', 'C', 'C']);
643
+ const benzene = Ring({ atoms: 'c', size: 6 });
644
+ const molecule = Molecule([propyl, benzene]);
645
+ const code = molecule.toCode();
646
+ expect(code).toBe(`export const molecule1 = Fragment('CCC');
647
+ export const molecule2 = Fragment('c1ccccc1');
648
+ export const molecule3 = Molecule([molecule1, molecule2]);`);
649
+ });
650
+ });
651
+
498
652
  describe('Decompiler - Options', () => {
499
653
  test('handles custom indent option', () => {
500
654
  const ring = Ring({ atoms: 'C', size: 6 });
501
- const code = decompile(ring, { indent: 2 });
655
+ const code = decompile(ring, { indent: 2, verbose: true });
502
656
  expect(code).toBe(" export const v1 = Ring({ atoms: 'C', size: 6 });");
503
657
  });
504
658
 
505
659
  test('handles custom varName option', () => {
506
660
  const ring = Ring({ atoms: 'C', size: 6 });
507
- const code = decompile(ring, { varName: 'node' });
661
+ const code = decompile(ring, { varName: 'node', verbose: true });
508
662
  expect(code).toBe("export const node1 = Ring({ atoms: 'C', size: 6 });");
509
663
  });
510
664
 
511
665
  test('includeMetadata false strips branchDepths', () => {
512
666
  const ring = Ring({ atoms: 'C', size: 6 });
513
667
  ring.metaBranchDepths = [0, 0, 0, 1, 1, 1];
514
- const code = decompile(ring, { includeMetadata: false });
668
+ const code = decompile(ring, { verbose: true, includeMetadata: false });
515
669
  expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 6, branchDepths: [0, 0, 0, 1, 1, 1] });");
516
670
  });
517
671
 
@@ -522,21 +676,39 @@ describe('Decompiler - Options', () => {
522
676
  });
523
677
 
524
678
  describe('Decompiler - Round-trip', () => {
525
- test('generated code can be evaluated', () => {
679
+ test('generated code can be evaluated (verbose)', () => {
526
680
  const benzene = Ring({ atoms: 'c', size: 6 });
527
- const code = benzene.toCode('r');
681
+ const code = benzene.toCode('r', { verbose: true });
528
682
  expect(code).toBe("export const r1 = Ring({ atoms: 'c', size: 6 });");
529
683
  });
530
684
 
531
- test('preserves structure through decompile', () => {
685
+ test('generated code can be evaluated (non-verbose)', () => {
686
+ const benzene = Ring({ atoms: 'c', size: 6 });
687
+ const code = benzene.toCode('r');
688
+ expect(code).toBe("export const r1 = Fragment('c1ccccc1');");
689
+ });
690
+
691
+ test('preserves structure through decompile (verbose)', () => {
532
692
  const propane = Linear(['C', 'C', 'C']);
533
- const code = propane.toCode('p');
693
+ const code = propane.toCode('p', { verbose: true });
534
694
  expect(code).toBe("export const p1 = Linear(['C', 'C', 'C']);");
535
695
  });
536
696
 
537
- test('round-trips parsed ring with bonds', () => {
697
+ test('preserves structure through decompile (non-verbose)', () => {
698
+ const propane = Linear(['C', 'C', 'C']);
699
+ const code = propane.toCode('p');
700
+ expect(code).toBe("export const p1 = Fragment('CCC');");
701
+ });
702
+
703
+ test('round-trips parsed ring with bonds (verbose)', () => {
538
704
  const ast = parse('C1CC=CC1');
539
- const code = decompile(ast, { includeMetadata: true });
705
+ const code = decompile(ast, { verbose: true, includeMetadata: true });
540
706
  expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 5, bonds: [null, null, '=', null, null] });");
541
707
  });
708
+
709
+ test('round-trips parsed ring with bonds (non-verbose)', () => {
710
+ const ast = parse('C1CC=CC1');
711
+ const code = decompile(ast, { includeMetadata: true });
712
+ expect(code).toBe("export const v1 = Fragment('C1CC=CC1');");
713
+ });
542
714
  });
@@ -61,8 +61,13 @@ describe('Fragment', () => {
61
61
  });
62
62
  });
63
63
 
64
- test('toCode works on fragment', () => {
64
+ test('toCode works on fragment (non-verbose default)', () => {
65
65
  const benzene = Fragment('c1ccccc1');
66
- expect(benzene.toCode('v')).toBe("export const v1 = Ring({ atoms: 'c', size: 6 });");
66
+ expect(benzene.toCode('v')).toBe("export const v1 = Fragment('c1ccccc1');");
67
+ });
68
+
69
+ test('toCode works on fragment (verbose)', () => {
70
+ const benzene = Fragment('c1ccccc1');
71
+ expect(benzene.toCode('v', { verbose: true })).toBe("export const v1 = Ring({ atoms: 'c', size: 6 });");
67
72
  });
68
73
  });