smiles-js 1.1.0 → 2.0.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 (61) hide show
  1. package/API.md +43 -20
  2. package/README.md +1 -1
  3. package/docs/EXAMPLES.md +2 -2
  4. package/docs/IMPLEMENTATION_ROADMAP.md +3 -3
  5. package/docs/PARSER_REFACTOR_PLAN.md +18 -18
  6. package/docs/PRODUCTION_AUDIT.md +7 -7
  7. package/docs/TEST_DRIVE_RESULTS.md +2 -2
  8. package/docs/atorvastatin-named.js +9 -9
  9. package/docs/esomeprazole-showcase.js +1 -1
  10. package/docs/readable-generated-code/{READABLE_GENERATED_CODE.md → 10_READABLE_GENERATED_CODE.md} +6 -6
  11. package/docs/readable-generated-code/{AFTER_ACTION_REPORT.md → 40_AFTER_ACTION_REPORT.md} +4 -4
  12. package/docs/readable-generated-code/{UPDATED_PLAN.md → 50_UPDATED_PLAN.md} +2 -2
  13. package/docs/readable-generated-code/60_PLAN.md +155 -0
  14. package/docs/readable-generated-code/PLAN.md +24 -0
  15. package/docs/ritonavir-synthesis.js +3 -3
  16. package/docs/sildenafil-synthesis.js +3 -3
  17. package/examples/basic-usage.js +5 -5
  18. package/examples/decompiler-demo.js +1 -1
  19. package/examples/fused-ring-manipulation.js +6 -6
  20. package/examples/linear-manipulation.js +5 -5
  21. package/package.json +1 -1
  22. package/src/codegen/branch-crossing-ring.test.js +0 -3
  23. package/src/codegen/branch-walker.js +5 -0
  24. package/src/codegen/index.test.js +1 -1
  25. package/src/codegen/interleaved-fused-ring.test.js +0 -2
  26. package/src/codegen/simple-fused-ring.js +115 -3
  27. package/src/codegen/smiles-codegen-core.js +11 -7
  28. package/src/codegen/smiles-codegen-core.test.js +4 -4
  29. package/src/constructors.test.js +7 -7
  30. package/src/decompiler.js +510 -488
  31. package/src/decompiler.test.js +130 -195
  32. package/src/layout/index.js +122 -26
  33. package/src/layout/index.test.js +26 -8
  34. package/src/manipulation.js +230 -33
  35. package/src/manipulation.test.js +25 -25
  36. package/src/method-attachers.js +10 -10
  37. package/src/parser/ast-builder.js +16 -2
  38. package/src/parser/branch-utils.test.js +8 -0
  39. package/src/parser/ring-group-builder.js +129 -23
  40. package/src/parser/ring-node-builder.js +3 -0
  41. package/src/parser/ring-utils.test.js +5 -0
  42. package/src/parser/smiles-parser-core.test.js +1 -1
  43. package/src/roundtrip.test.js +9 -22
  44. package/src/sequential-rings.test.js +48 -16
  45. package/test-integration/__snapshots__/acetaminophen.test.js.snap +4 -4
  46. package/test-integration/__snapshots__/adjuvant-analgesics.test.js.snap +52 -62
  47. package/test-integration/__snapshots__/cholesterol-drugs.test.js.snap +103 -154
  48. package/test-integration/__snapshots__/dexamethasone.test.js.snap +43 -33
  49. package/test-integration/__snapshots__/endocannabinoids.test.js.snap +64 -61
  50. package/test-integration/__snapshots__/hypertension-medication.test.js.snap +35 -57
  51. package/test-integration/__snapshots__/local-anesthetics.test.js.snap +16 -16
  52. package/test-integration/__snapshots__/nsaids-otc.test.js.snap +19 -23
  53. package/test-integration/__snapshots__/nsaids-prescription.test.js.snap +51 -69
  54. package/test-integration/__snapshots__/opioids.test.js.snap +135 -136
  55. package/test-integration/__snapshots__/steroids.test.js.snap +432 -489
  56. package/test-integration/acetaminophen.smiles.js +2 -2
  57. package/test-integration/cortisone.test.js +5 -1
  58. package/test-integration/leading-bond.test.js +2 -2
  59. package/todo +2 -3
  60. /package/docs/readable-generated-code/{REFACTOR_PLAN.md → 20_REFACTOR_PLAN.md} +0 -0
  61. /package/docs/readable-generated-code/{EXECUTION_PLAN.md → 30_EXECUTION_PLAN.md} +0 -0
package/API.md CHANGED
@@ -21,6 +21,7 @@ Create ring structures with substitutions and attachments.
21
21
  | `substitutions` | `object` | `{}` | Position -> atom substitutions |
22
22
  | `attachments` | `object` | `{}` | Position -> attachment list |
23
23
  | `bonds` | `array` | `[]` | Bond types between atoms |
24
+ | `branchDepths` | `number[]` | `null` | Per-atom branch depth (for rings that cross branch boundaries) |
24
25
 
25
26
  ```javascript
26
27
  // Simple benzene
@@ -61,7 +62,7 @@ const propene = Linear(['C', 'C', 'C'], [null, '=']);
61
62
  // Ethanol with hydroxyl
62
63
  const ethyl = Linear(['C', 'C']);
63
64
  const hydroxyl = Linear(['O']);
64
- const ethanol = ethyl.attach(hydroxyl, 2);
65
+ const ethanol = ethyl.attach(2, hydroxyl);
65
66
  ```
66
67
 
67
68
  ### `FusedRing(rings)`
@@ -107,7 +108,7 @@ All manipulation methods are **immutable** -- they return new nodes and never mo
107
108
  const benzene = Ring({ atoms: 'c', size: 6 });
108
109
 
109
110
  // Attach substituent at position
110
- const toluene = benzene.attach(Linear(['C']), 1);
111
+ const toluene = benzene.attach(1, Linear(['C']));
111
112
 
112
113
  // Substitute atom at position
113
114
  const pyridine = benzene.substitute(5, 'n');
@@ -117,20 +118,20 @@ const triazine = benzene.substituteMultiple({ 1: 'n', 3: 'n', 5: 'n' });
117
118
 
118
119
  // Fuse with another ring (offset = shared atom count)
119
120
  const ring2 = Ring({ atoms: 'C', size: 6 });
120
- const naphthalene = benzene.fuse(ring2, 2);
121
+ const naphthalene = benzene.fuse(2, ring2);
121
122
 
122
123
  // Clone
123
124
  const benzeneClone = benzene.clone();
124
125
  ```
125
126
 
126
- #### `ring.attach(attachment, position, options?)`
127
+ #### `ring.attach(position, attachment, options?)`
127
128
 
128
129
  Attach a node to the ring at a 1-indexed position.
129
130
 
130
131
  | Parameter | Type | Description |
131
132
  |-----------|------|-------------|
132
- | `attachment` | `object` | Node to attach |
133
133
  | `position` | `number` | 1-indexed ring position |
134
+ | `attachment` | `object` | Node to attach |
134
135
  | `options.sibling` | `boolean` | If set, marks the attachment as sibling (true) or inline (false) |
135
136
 
136
137
  #### `ring.substitute(position, newAtom)`
@@ -141,7 +142,7 @@ Replace the atom at position with a different atom symbol.
141
142
 
142
143
  Replace multiple atoms. `substitutionMap` is `{ position: atomSymbol }`.
143
144
 
144
- #### `ring.fuse(otherRing, offset, options?)`
145
+ #### `ring.fuse(offset, otherRing, options?)`
145
146
 
146
147
  Fuse this ring with another ring. `offset` is how many positions into this ring the other ring starts.
147
148
 
@@ -156,7 +157,7 @@ const butane = Linear(['C', 'C', 'C', 'C']);
156
157
 
157
158
  // Attach branch at position
158
159
  const methyl = Linear(['C']);
159
- const branched = butane.attach(methyl, 2);
160
+ const branched = butane.attach(2, methyl);
160
161
 
161
162
  // Concatenate chains
162
163
  const hexane = butane.concat(Linear(['C', 'C']));
@@ -168,7 +169,7 @@ const isobutane = butane.branchAt({ 2: methyl });
168
169
  const decorated = butane.branch(2, methyl, Linear(['O']));
169
170
  ```
170
171
 
171
- #### `linear.attach(attachment, position)`
172
+ #### `linear.attach(position, attachment)`
172
173
 
173
174
  Attach a node at a 1-indexed position.
174
175
 
@@ -208,10 +209,10 @@ const combined = mol.concat(Molecule([Ring({ atoms: 'c', size: 6 })]));
208
209
  ### FusedRing Methods
209
210
 
210
211
  ```javascript
211
- const fused = ring1.fuse(ring2, 4);
212
+ const fused = ring1.fuse(4, ring2);
212
213
 
213
214
  // Add another ring to the fused system
214
- const triple = fused.addRing(ring3, 8);
215
+ const triple = fused.addRing(8, ring3);
215
216
 
216
217
  // Get a specific ring by number
217
218
  const r = fused.getRing(1);
@@ -220,20 +221,45 @@ const r = fused.getRing(1);
220
221
  const modified = fused.substituteInRing(1, 3, 'N');
221
222
 
222
223
  // Attach to a specific ring
223
- const decorated = fused.attachToRing(1, Linear(['O']), 4);
224
+ const decorated = fused.attachToRing(1, 4, Linear(['O']));
224
225
 
225
226
  // Renumber rings
226
227
  const renumbered = fused.renumber(10);
227
228
 
228
- // Add sequential continuation rings
229
+ // Add sequential continuation rings with depths and chain atoms
229
230
  const withSeq = fused.addSequentialRings([ring3, ring4], {
230
- atomAttachments: { 25: [Linear(['O'], ['='])] }
231
+ depths: [1, 2],
232
+ chainAtoms: [
233
+ { atom: 'C', depth: 2, position: 'before' },
234
+ { atom: 'C', depth: 2, position: 'after', attachments: [Linear(['O'], ['='])] },
235
+ ]
231
236
  });
232
237
 
233
238
  // Add attachment to a sequential atom position
234
239
  const withAtt = fused.addSequentialAtomAttachment(25, Linear(['O']));
235
240
  ```
236
241
 
242
+ #### `fusedRing.addSequentialRings(rings, options?)`
243
+
244
+ Add continuation rings to a fused ring system. Computes all position metadata internally.
245
+
246
+ | Parameter | Type | Description |
247
+ |-----------|------|-------------|
248
+ | `rings` | `Ring[]` | Array of sequential Ring nodes to add |
249
+ | `options.depths` | `number[]` | Per-ring branch depth (e.g., `[1, 2, 1, 2]`) |
250
+ | `options.chainAtoms` | `object[]` | Standalone atoms between rings (see below) |
251
+ | `options.atomAttachments` | `object` | Legacy: position → attachment list map |
252
+
253
+ **chainAtoms entries:**
254
+
255
+ | Property | Type | Description |
256
+ |----------|------|-------------|
257
+ | `atom` | `string` | Atom symbol (e.g., `'C'`, `'N'`, `'O'`) |
258
+ | `depth` | `number` | Branch depth for this atom |
259
+ | `position` | `'before'` \| `'after'` | Whether the atom appears before or after rings at its depth |
260
+ | `bond` | `string` | Optional bond type (e.g., `'='`, `'#'`) |
261
+ | `attachments` | `object[]` | Optional array of nodes attached to this atom |
262
+
237
263
  ---
238
264
 
239
265
  ## Parsing & Serialization
@@ -363,7 +389,7 @@ import {
363
389
  } from 'smiles-js/manipulation';
364
390
 
365
391
  const benzene = Ring({ atoms: 'c', size: 6 });
366
- const toluene = ringAttach(benzene, Linear(['C']), 1);
392
+ const toluene = ringAttach(benzene, 1, Linear(['C']));
367
393
  ```
368
394
 
369
395
  ---
@@ -395,7 +421,7 @@ import RDKit from '@rdkit/rdkit';
395
421
  // Build molecule programmatically
396
422
  const benzene = Ring({ atoms: 'c', size: 6 });
397
423
  const methyl = Linear(['C']);
398
- const toluene = benzene.attach(methyl, 1);
424
+ const toluene = benzene.attach(1, methyl);
399
425
 
400
426
  // Use with RDKit
401
427
  const rdkit = await RDKit.load();
@@ -435,9 +461,6 @@ Some complex molecules may have minor notation differences during round-trip:
435
461
 
436
462
  **Impact**: Low - Structure is preserved, only notation differs.
437
463
 
438
- ### toCode() Limitation
464
+ ### toCode() Generated Code
439
465
 
440
- The `.toCode()` method has a limitation with certain sequential continuation patterns in very complex nested structures. This does NOT affect:
441
- - Parsing SMILES -> AST
442
- - Serializing AST -> SMILES
443
- - Round-trip fidelity
466
+ The `.toCode()` method generates JavaScript constructor code that reconstructs the molecule. For molecules with sequential continuation rings (e.g., Telmisartan), the generated code uses `addSequentialRings()` with `depths` and `chainAtoms` options to produce clean structural API calls with no raw metadata assignments.
package/README.md CHANGED
@@ -64,7 +64,7 @@ console.log(benzene.smiles); // c1ccccc1
64
64
 
65
65
  // Add methyl group to make toluene
66
66
  const methyl = Linear(['C']);
67
- const toluene = benzene.attach(methyl, 1);
67
+ const toluene = benzene.attach(1, methyl);
68
68
  console.log(toluene.smiles); // c1(C)ccccc1
69
69
 
70
70
  // Create pyridine via substitution
package/docs/EXAMPLES.md CHANGED
@@ -108,7 +108,7 @@ node docs/atorvastatin-named.js
108
108
  // Isopropyl group (substituent on pyrrole)
109
109
  const methyl1 = Linear(['C']);
110
110
  const ethyl = Linear(['C', 'C']);
111
- const isopropyl = ethyl.attach(methyl1, 2);
111
+ const isopropyl = ethyl.attach(2, methyl1);
112
112
 
113
113
  // Central pyrrole ring (5-membered nitrogen heterocycle)
114
114
  const pyrroleRing = Ring({ atoms: 'c', size: 5 });
@@ -117,7 +117,7 @@ const pyrrole = pyrroleRing.substitute(5, 'n');
117
117
  // Dihydroxyheptanoic acid side chain (the "statin" pharmacophore)
118
118
  const heptanoicAcidChain = Linear(['C', 'C', 'C', 'C', 'C', 'C', 'C', 'O']);
119
119
  const hydroxyl1 = Linear(['O']);
120
- const statinSideChain = heptanoicAcidChain.attach(hydroxyl1, 3);
120
+ const statinSideChain = heptanoicAcidChain.attach(3, hydroxyl1);
121
121
  ```
122
122
 
123
123
  ---
@@ -78,7 +78,7 @@ console.log(propane.smiles); // 'CCC'
78
78
  ```javascript
79
79
  const benzene = Ring({ atoms: 'c', size: 6 });
80
80
  const methyl = Linear(['C']);
81
- const toluene = benzene.attach(methyl, 1);
81
+ const toluene = benzene.attach(1, methyl);
82
82
  console.log(toluene.smiles); // 'c1c(C)cccc1' or equivalent
83
83
  ```
84
84
 
@@ -197,7 +197,7 @@ const triazine = benzene.substituteMultiple({ 1: 'n', 3: 'n', 5: 'n' });
197
197
 
198
198
  const ring1 = Ring({ atoms: 'C', size: 10 });
199
199
  const ring2 = Ring({ atoms: 'C', size: 6 });
200
- const naphthalene = ring1.fuse(ring2, 2);
200
+ const naphthalene = ring1.fuse(2, ring2);
201
201
  ```
202
202
 
203
203
  ---
@@ -387,7 +387,7 @@ console.log(fragment.toCode());
387
387
  // Output:
388
388
  // const linear1 = Linear(['C'])
389
389
  // const ring1 = Ring({ atoms: 'c', size: 6 })
390
- // const ring2 = ring1.attach(linear1, 4)
390
+ // const ring2 = ring1.attach(4, linear1)
391
391
  ```
392
392
 
393
393
  ---
@@ -780,10 +780,10 @@ FusedRing.prototype.substituteInRing = function(ringNumber, position, newAtom) {
780
780
  return replaceRingInFusedSystem(this, ringNumber, updatedRing);
781
781
  };
782
782
 
783
- FusedRing.prototype.attachToRing = function(ringNumber, attachment, position) {
783
+ FusedRing.prototype.attachToRing = function(ringNumber, position, attachment) {
784
784
  // Attach to a specific ring in the fused system
785
785
  const targetRing = this.getRing(ringNumber);
786
- const updatedRing = targetRing.attach(attachment, position);
786
+ const updatedRing = targetRing.attach(position, attachment);
787
787
  return replaceRingInFusedSystem(this, ringNumber, updatedRing);
788
788
  };
789
789
 
@@ -832,7 +832,7 @@ Linear.prototype.branchAt = function(branchMap) {
832
832
  // branchMap: { position: branch, ... }
833
833
  let result = this;
834
834
  for (const [position, branchStructure] of Object.entries(branchMap)) {
835
- result = result.attach(branchStructure, position);
835
+ result = result.attach(position, branchStructure);
836
836
  }
837
837
  return result;
838
838
  };
@@ -943,7 +943,7 @@ const methyl = Linear(['C']);
943
943
  console.log(methyl.smiles); // 'C'
944
944
 
945
945
  // Combined structures
946
- const toluene = benzene.attach(methyl, 1);
946
+ const toluene = benzene.attach(1, methyl);
947
947
  console.log(toluene.smiles); // 'c1ccccc1C' or 'c1c(C)cccc1' (depending on serialization)
948
948
 
949
949
  // Fused ring SMILES
@@ -964,7 +964,7 @@ console.log(ring1.smiles); // 'c1ccccc1'
964
964
  const linear1 = Linear(['C']);
965
965
  console.log(linear1.smiles); // 'C'
966
966
 
967
- const molecule1 = ring1.attach(linear1, 1);
967
+ const molecule1 = ring1.attach(1, linear1);
968
968
  console.log(molecule1.smiles); // 'c1c(C)cccc1' (toluene)
969
969
 
970
970
  // Chained operations with SMILES inspection at each step
@@ -978,7 +978,7 @@ const step2 = step1.substitute(3, 'n');
978
978
  console.log('Step 2:', step2.smiles); // 'c1nccncc1'
979
979
 
980
980
  const branch = Linear(['C', 'C']);
981
- const final = step2.attach(branch, 5);
981
+ const final = step2.attach(5, branch);
982
982
  console.log('Final:', final.smiles); // 'c1nccnc(CC)c1'
983
983
  ```
984
984
 
@@ -1005,7 +1005,7 @@ console.log('Final:', final.smiles); // 'c1nccnc(CC)c1'
1005
1005
  // Create benzene and add methyl group
1006
1006
  const benzene = Ring({ atoms: 'c', size: 6 });
1007
1007
  const methyl = Linear(['C']);
1008
- const toluene = benzene.attach(methyl, 1);
1008
+ const toluene = benzene.attach(1, methyl);
1009
1009
 
1010
1010
  // Create pyridine via substitution
1011
1011
  const pyridine = benzene.substitute(1, 'n');
@@ -1016,7 +1016,7 @@ const triazine = benzene.substituteMultiple({ 1: 'n', 3: 'n', 5: 'n' });
1016
1016
  // Fuse two rings
1017
1017
  const ring1 = Ring({ atoms: 'C', size: 10 });
1018
1018
  const ring2 = Ring({ atoms: 'C', size: 6 });
1019
- const naphthalene = ring1.fuse(ring2, 2);
1019
+ const naphthalene = ring1.fuse(2, ring2);
1020
1020
 
1021
1021
  // ============================================
1022
1022
  // FUSED RING MANIPULATION
@@ -1034,10 +1034,10 @@ const substituted = benzimidazole
1034
1034
  .substituteInRing(1, 7, 'n');
1035
1035
 
1036
1036
  // Attach to specific ring in fused system
1037
- const withMethyl = benzimidazole.attachToRing(1, Linear(['C']), 1);
1037
+ const withMethyl = benzimidazole.attachToRing(1, 1, Linear(['C']));
1038
1038
 
1039
1039
  // Add another ring to the fused system
1040
- const expandedSystem = naphthalene.addRing(Ring({ atoms: 'C', size: 5 }), 8);
1040
+ const expandedSystem = naphthalene.addRing(8, Ring({ atoms: 'C', size: 5 }));
1041
1041
 
1042
1042
  // ============================================
1043
1043
  // LINEAR CHAIN MANIPULATION
@@ -1082,12 +1082,12 @@ const phenyl2 = Ring({ atoms: 'C', size: 6 });
1082
1082
  const carboxyl = Linear(['C'], ['=O', '-O']);
1083
1083
 
1084
1084
  // Fluent chaining
1085
- const phenylWithCarboxyl = phenyl2.attach(carboxyl, 6);
1086
- const biphenyl = phenyl1.attach(phenylWithCarboxyl, 6);
1085
+ const phenylWithCarboxyl = phenyl2.attach(6, carboxyl);
1086
+ const biphenyl = phenyl1.attach(6, phenylWithCarboxyl);
1087
1087
  const connector = Linear(['C', 'C']);
1088
1088
  const sideChain = connector.concat(biphenyl);
1089
1089
 
1090
- const coreWithSideChain = benzimidazoleCore.attachToRing(1, sideChain, 9);
1090
+ const coreWithSideChain = benzimidazoleCore.attachToRing(1, 9, sideChain);
1091
1091
 
1092
1092
  // Final assembly using Molecule
1093
1093
  const telmisartan = Molecule([
@@ -1476,7 +1476,7 @@ console.log(toluene.toCode());
1476
1476
  // Output:
1477
1477
  // const linear1 = Linear(['C'])
1478
1478
  // const ring1 = Ring({ atoms: 'c', size: 6 })
1479
- // const ring2 = ring1.attach(linear1, 4)
1479
+ // const ring2 = ring1.attach(4, linear1)
1480
1480
  ```
1481
1481
 
1482
1482
  **Decompiler Options:**
@@ -1493,7 +1493,7 @@ const options = {
1493
1493
  Fragment('c1ccc(C)cc1').toCode(options);
1494
1494
  // Output:
1495
1495
  // const myLinear1 = Linear(['C'])
1496
- // const myRing1 = Ring({ atoms: 'c', size: 6 }).attach(myLinear1, 4)
1496
+ // const myRing1 = Ring({ atoms: 'c', size: 6 }).attach(4, myLinear1)
1497
1497
  ```
1498
1498
 
1499
1499
  **Example Outputs:**
@@ -1507,7 +1507,7 @@ Fragment('c1ccccc1').toCode();
1507
1507
  Fragment('c1ccc(C)cc1').toCode();
1508
1508
  // const linear1 = Linear(['C'])
1509
1509
  // const ring1 = Ring({ atoms: 'c', size: 6 })
1510
- // const ring2 = ring1.attach(linear1, 4)
1510
+ // const ring2 = ring1.attach(4, linear1)
1511
1511
 
1512
1512
  // Pyridine: c1cccnc1
1513
1513
  Fragment('c1cccnc1').toCode();
@@ -1530,9 +1530,9 @@ Fragment('CCCc1ccccc1').toCode();
1530
1530
  Fragment('C(C(C))c1ccccc1').toCode();
1531
1531
  // const linear1 = Linear(['C'])
1532
1532
  // const linear2 = Linear(['C'])
1533
- // const linear3 = linear1.attach(linear2, 1)
1533
+ // const linear3 = linear1.attach(1, linear2)
1534
1534
  // const linear4 = Linear(['C'])
1535
- // const linear5 = linear3.attach(linear4, 1)
1535
+ // const linear5 = linear3.attach(1, linear4)
1536
1536
  // const ring1 = Ring({ atoms: 'c', size: 6 })
1537
1537
  // const molecule1 = Molecule([linear5, ring1])
1538
1538
  ```
@@ -53,7 +53,7 @@ CCC ✅
53
53
  **Status**: COMPLETE
54
54
  **Verification**:
55
55
  ```bash
56
- $ node -e "import {Ring, Linear} from './src/index.js'; const benzene = Ring({ atoms: 'c', size: 6 }); const methyl = Linear(['C']); const toluene = benzene.attach(methyl, 1); console.log(toluene.smiles);"
56
+ $ node -e "import {Ring, Linear} from './src/index.js'; const benzene = Ring({ atoms: 'c', size: 6 }); const methyl = Linear(['C']); const toluene = benzene.attach(1, methyl); console.log(toluene.smiles);"
57
57
  c1(C)ccccc1 ✅
58
58
  ```
59
59
  - `Ring.prototype.attach()` works
@@ -228,7 +228,7 @@ const ring1 = Ring({ atoms: 'c', size: 6 }); ✅
228
228
  $ node -e "import {parse} from './src/index.js'; const toluene = parse('c1ccc(C)cc1'); console.log(toluene.toCode());"
229
229
  const ring1 = Ring({ atoms: 'c', size: 6 });
230
230
  const ring2 = Linear(['C']);
231
- const ring3 = ring1.attach(ring2, 4); ✅
231
+ const ring3 = ring1.attach(4, ring2); ✅
232
232
  ```
233
233
  - All AST node types decompile correctly
234
234
  - Substitutions generate method calls
@@ -296,25 +296,25 @@ Ran 457 tests across 25 files. [163.00ms]
296
296
  - [x] Molecule(components)
297
297
 
298
298
  ### Ring Manipulation API ✅
299
- - [x] ring.attach(attachment, position)
299
+ - [x] ring.attach(position, attachment)
300
300
  - [x] ring.substitute(position, newAtom)
301
301
  - [x] ring.substituteMultiple(substitutionMap)
302
- - [x] ring.fuse(otherRing, offset)
302
+ - [x] ring.fuse(offset, otherRing)
303
303
  - [x] ring.concat(other)
304
304
  - [x] ring.clone()
305
305
 
306
306
  ### Linear Manipulation API ✅
307
- - [x] linear.attach(attachment, atomIndex)
307
+ - [x] linear.attach(atomIndex, attachment)
308
308
  - [x] linear.concat(other)
309
309
  - [x] linear.branch(branchPoint, ...branches)
310
310
  - [x] linear.branchAt(branchMap)
311
311
  - [x] linear.clone()
312
312
 
313
313
  ### FusedRing Manipulation API ✅
314
- - [x] fusedRing.addRing(ring, offset)
314
+ - [x] fusedRing.addRing(offset, ring)
315
315
  - [x] fusedRing.getRing(ringNumber)
316
316
  - [x] fusedRing.substituteInRing(ringNumber, position, newAtom)
317
- - [x] fusedRing.attachToRing(ringNumber, attachment, position)
317
+ - [x] fusedRing.attachToRing(ringNumber, position, attachment)
318
318
  - [x] fusedRing.renumber(startNumber)
319
319
  - [x] fusedRing.concat(other)
320
320
  - [x] fusedRing.clone()
@@ -215,8 +215,8 @@ const molecule2 = Ring({ atoms: 'c', size: 6 });
215
215
  ```javascript
216
216
  const propylChain = Linear(['C', 'C', 'C']);
217
217
  const benzeneRing = Ring({ atoms: 'c', size: 6 });
218
- const isopropylGroup = propylChain.attach(methylGroup, 2);
219
- const phenylAmide = benzeneRing.attach(amideLinker, 1);
218
+ const isopropylGroup = propylChain.attach(2, methylGroup);
219
+ const phenylAmide = benzeneRing.attach(1, amideLinker);
220
220
  ```
221
221
 
222
222
  ---
@@ -16,7 +16,7 @@ function log(...args) {
16
16
  // Isopropyl group (substituent on pyrrole)
17
17
  const methyl1 = Linear(['C']);
18
18
  const ethyl = Linear(['C', 'C']);
19
- const isopropyl = ethyl.attach(methyl1, 2);
19
+ const isopropyl = ethyl.attach(2, methyl1);
20
20
 
21
21
  // Central pyrrole ring (5-membered nitrogen heterocycle)
22
22
  const pyrroleRing = Ring({ atoms: 'c', size: 5 });
@@ -25,31 +25,31 @@ const pyrrole = pyrroleRing.substitute(5, 'n');
25
25
  // Amide linker with phenyl group
26
26
  const amideLinker = Linear(['C', 'N']);
27
27
  const carbonyl = Linear(['O'], ['=']);
28
- const amide = amideLinker.attach(carbonyl, 1);
28
+ const amide = amideLinker.attach(1, carbonyl);
29
29
  const phenylRing1 = Ring({ atoms: 'c', size: 6, ringNumber: 2 });
30
30
  const phenylAmide = Molecule([amide, phenylRing1]);
31
31
 
32
32
  // Attach amide-phenyl to pyrrole
33
- const pyrroleWithAmide = pyrrole.attach(phenylAmide, 2);
33
+ const pyrroleWithAmide = pyrrole.attach(2, phenylAmide);
34
34
 
35
35
  // Second phenyl ring (position 3)
36
36
  const phenylRing2 = Ring({ atoms: 'c', size: 6, ringNumber: 3 });
37
- const pyrroleWithTwoPhenyls = pyrroleWithAmide.attach(phenylRing2, 3);
37
+ const pyrroleWithTwoPhenyls = pyrroleWithAmide.attach(3, phenylRing2);
38
38
 
39
39
  // Fluorophenyl ring (position 4)
40
40
  const phenylRing3 = Ring({ atoms: 'c', size: 6, ringNumber: 4 });
41
41
  const fluorine = Linear(['F']);
42
- const fluorophenyl = phenylRing3.attach(fluorine, 4);
43
- const pyrroleCore = pyrroleWithTwoPhenyls.attach(fluorophenyl, 4);
42
+ const fluorophenyl = phenylRing3.attach(4, fluorine);
43
+ const pyrroleCore = pyrroleWithTwoPhenyls.attach(4, fluorophenyl);
44
44
 
45
45
  // Dihydroxyheptanoic acid side chain (the "statin" pharmacophore)
46
46
  const heptanoicAcidChain = Linear(['C', 'C', 'C', 'C', 'C', 'C', 'C', 'O']);
47
47
  const hydroxyl1 = Linear(['O']);
48
- const dihydroxyChain = heptanoicAcidChain.attach(hydroxyl1, 3);
48
+ const dihydroxyChain = heptanoicAcidChain.attach(3, hydroxyl1);
49
49
  const hydroxyl2 = Linear(['O']);
50
- const trihydroxyChain = dihydroxyChain.attach(hydroxyl2, 5);
50
+ const trihydroxyChain = dihydroxyChain.attach(5, hydroxyl2);
51
51
  const carboxylate = Linear(['O'], ['=']);
52
- const statinSideChain = trihydroxyChain.attach(carboxylate, 7);
52
+ const statinSideChain = trihydroxyChain.attach(7, carboxylate);
53
53
 
54
54
  // Assemble complete molecule
55
55
  const atorvastatin = Molecule([isopropyl, pyrroleCore, statinSideChain]);
@@ -84,7 +84,7 @@ log('3️⃣ Methoxy groups: 2x', methoxyGroup1.smiles);
84
84
  // Sulfoxide linker
85
85
  const sulfoxideLinker = Linear(['S', 'C']);
86
86
  const sulfinylOxygen = Linear(['O'], ['=']);
87
- const sulfoxide = sulfoxideLinker.attach(sulfinylOxygen, 1);
87
+ const sulfoxide = sulfoxideLinker.attach(1, sulfinylOxygen);
88
88
  log('4️⃣ Sulfoxide linker: ', sulfoxide.smiles);
89
89
 
90
90
  // Dimethoxymethylpyridine
@@ -11,7 +11,7 @@ export const v2 = Ring({ atoms: 'C', size: 5, bonds: ['=', null, '=', null, null
11
11
  export const v3 = v2.substitute(2, 'N');
12
12
  export const v4 = v3.substitute(5, 'N');
13
13
  export const v5 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 2, bonds: ['=', null, '=', null, '=', null] });
14
- export const v6 = v4.fuse(v5, 2);
14
+ export const v6 = v4.fuse(2, v5);
15
15
 
16
16
  // Unreadable garbage - what is any of this?
17
17
  v6.rings[0].metaPositions = [3, 4, 5, 10, 11];
@@ -102,7 +102,7 @@ export const v1 = Ring({ atoms: 'C', size: 5, bonds: ['=', null, '=', null, null
102
102
  export const v2 = v1.substitute(2, 'N');
103
103
  export const v3 = v2.substitute(5, 'N');
104
104
  export const v4 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 2, bonds: ['=', null, '=', null, '=', null] });
105
- export const v5 = v3.fuse(v4, 2);
105
+ export const v5 = v3.fuse(2, v4);
106
106
  v5.rings[0].metaPositions = [3, 4, 5, 10, 11];
107
107
  v5.rings[0].metaStart = 3;
108
108
  v5.rings[0].metaEnd = 11;
@@ -119,7 +119,7 @@ export const v1 = Ring({ atoms: 'C', size: 5, bonds: ['=', null, '=', null, null
119
119
  export const v2 = v1.substitute(2, 'N');
120
120
  export const v3 = v2.substitute(5, 'N');
121
121
  export const v4 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 2, bonds: ['=', null, '=', null, '=', null] });
122
- export const v5 = v3.fuse(v4, 2);
122
+ export const v5 = v3.fuse(2, v4);
123
123
  ```
124
124
 
125
125
  That's it. The `fuse()` method (and `FusedRing()` constructor) would call `layout()` internally, computing all the metadata automatically.
@@ -127,14 +127,14 @@ That's it. The `fuse()` method (and `FusedRing()` constructor) would call `layou
127
127
  For cases with sequential rings, we'd need a new API method:
128
128
 
129
129
  ```javascript
130
- export const v5 = v3.fuse(v4, 2);
130
+ export const v5 = v3.fuse(2, v4);
131
131
  export const v6 = Ring({ atoms: 'C', size: 5, ringNumber: 5, bonds: ['=', null, '=', null, null] });
132
132
  export const v7 = v6.substitute(2, 'N');
133
133
  export const v8 = v7.substitute(5, 'N');
134
134
  export const v9 = v5.addSequentialRing(v8);
135
135
 
136
136
  // Or alternatively, pass sequential rings at construction time:
137
- export const v5 = v3.fuse(v4, 2, {
137
+ export const v5 = v3.fuse(2, v4, {
138
138
  sequentialRings: [v8, v10, v11, v12],
139
139
  sequentialAtomAttachments: { 25: [v13] }
140
140
  });
@@ -212,7 +212,7 @@ export const v2 = Ring({ atoms: 'C', size: 5, bonds: ['=', null, '=', null, null
212
212
  export const v3 = v2.substitute(2, 'N');
213
213
  export const v4 = v3.substitute(5, 'N');
214
214
  export const v5 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 2, bonds: ['=', null, '=', null, '=', null], branchDepths: [0, 0, 1, 1, 2, 2] });
215
- export const v6 = v4.fuse(v5, 2);
215
+ export const v6 = v4.fuse(2, v5);
216
216
 
217
217
  export const v7 = Ring({ atoms: 'C', size: 5, ringNumber: 5, bonds: ['=', null, '=', null, null] });
218
218
  export const v8 = v7.substitute(2, 'N');
@@ -30,7 +30,7 @@ export const v1 = Ring({ atoms: 'C', size: 5, bonds: ['=', null, '=', null, null
30
30
  export const v2 = v1.substitute(2, 'N');
31
31
  export const v3 = v2.substitute(5, 'N');
32
32
  export const v4 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 2 });
33
- export const v5 = v3.fuse(v4, 2);
33
+ export const v5 = v3.fuse(2, v4);
34
34
  ```
35
35
 
36
36
  Done. No meta. The `fuse()` call computes everything internally via `layout()`.
@@ -42,7 +42,7 @@ Done. No meta. The `fuse()` call computes everything internally via `layout()`.
42
42
  export const v1 = Linear(['C', 'C', 'C']);
43
43
  export const v2 = Ring({ atoms: 'C', size: 5, ... });
44
44
  // ...
45
- export let v6 = v4.fuse(v5, 2);
45
+ export let v6 = v4.fuse(2, v5);
46
46
  // ...
47
47
  v6 = v6.addSequentialRings([v9, v10, v11, v12], { atomAttachments: { 25: [v13] } });
48
48
 
@@ -135,9 +135,9 @@ Not everything is bad. Simple molecules produce perfectly readable code:
135
135
  ```javascript
136
136
  export const v1 = Linear(['C', 'C', 'N']);
137
137
  export const v2 = Ring({ atoms: 'c', size: 6 });
138
- export const v3 = v2.attach(v1, 3);
138
+ export const v3 = v2.attach(3, v1);
139
139
  export const v4 = Linear(['O']);
140
- export const v5 = v3.attach(v4, 1);
140
+ export const v5 = v3.attach(1, v4);
141
141
  export const v6 = Molecule([v5]);
142
142
  ```
143
143
 
@@ -165,7 +165,7 @@ For the simple path, the decompiler emits:
165
165
  const v1 = Ring({ atoms: 'C', size: 5, bonds: [...], branchDepths: [...] });
166
166
  const v2 = v1.substitute(2, 'N');
167
167
  const v3 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 2, ... });
168
- const v4 = v2.fuse(v3, 2);
168
+ const v4 = v2.fuse(2, v3);
169
169
  ```
170
170
 
171
171
  No meta. The `branchDepths` option on Ring is structural (it's an input to the constructor, not computed metadata).
@@ -210,7 +210,7 @@ export const v2 = Ring({ atoms: 'C', size: 5, bonds: ['=', null, '=', null, null
210
210
  export const v3 = v2.substitute(2, 'N');
211
211
  export const v4 = v3.substitute(5, 'N');
212
212
  export const v5 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 2, bonds: ['=', null, '=', null, '=', null], branchDepths: [0, 0, 1, 1, 2, 2] });
213
- export const v6 = v4.fuse(v5, 2);
213
+ export const v6 = v4.fuse(2, v5);
214
214
  // sequential rings become attachments or are added via addSequentialRings
215
215
  // ...
216
216
  // NO META LINES