@toolpath/tool-scraper 2.3.0 → 2.5.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.
- package/README.md +19 -8
- package/dist/families/emuge.d.ts +51 -3
- package/dist/families/emuge.js +105 -15
- package/dist/families/kennametal.d.ts +291 -2
- package/dist/families/kennametal.js +221 -22
- package/dist/families/maritool.d.ts +31 -0
- package/dist/families/maritool.js +31 -0
- package/dist/family.d.ts +28 -2
- package/dist/holding.d.ts +61 -3
- package/dist/holding.js +42 -1
- package/dist/identity.d.ts +1 -1
- package/dist/identity.js +3 -6
- package/dist/node/cli.js +46 -1
- package/dist/records.d.ts +29 -2
- package/dist/records.js +13 -1
- package/dist/vendors/emuge/records.d.ts +10 -0
- package/dist/vendors/emuge/records.js +12 -0
- package/dist/vendors/kennametal/catalog.d.ts +168 -0
- package/dist/vendors/kennametal/catalog.js +247 -0
- package/dist/vendors/kennametal/holding.js +50 -4
- package/dist/vendors/kennametal/index.d.ts +1 -0
- package/dist/vendors/kennametal/index.js +1 -0
- package/dist/vendors/kennametal/records.js +1 -0
- package/package.json +2 -2
|
@@ -73,6 +73,40 @@ const NO_COOLANT_THROUGH_TAP = {
|
|
|
73
73
|
checked: '2026-08-29',
|
|
74
74
|
by: 'JG',
|
|
75
75
|
};
|
|
76
|
+
/**
|
|
77
|
+
* The tap-type facet, and what it settles.
|
|
78
|
+
*
|
|
79
|
+
* **The variant table does not carry it.** All three tap families publish
|
|
80
|
+
* `D1-TDZ`, `Thread Tolerance Class ANSI`, `Tap Pitch Diameter Limit` and
|
|
81
|
+
* `Type of Thread` — the thread's *class*, never how it is produced. What does
|
|
82
|
+
* carry it is the vendor's own `newTapType` Solr facet, and it narrows the same
|
|
83
|
+
* `.variants.<code>.html` endpoint `vendors/kennametal/scrape.ts` already
|
|
84
|
+
* calls: `scrape.ACTIVE_ONLY` is one facet on that query and this is a second,
|
|
85
|
+
* exactly as `vendors/kennametal/materials.ts` appends one.
|
|
86
|
+
*
|
|
87
|
+
* Its vocabulary, read off the threading category listing on 2026-09-07, is
|
|
88
|
+
* `2-Hand Tap`, `3-Forming Tap`, `8-Spiral Flute Tap`, `10-Pipe Tap`,
|
|
89
|
+
* `11-Spiral Point Tap` and `13-Straight Flute Tap`. Exactly one of the six is
|
|
90
|
+
* a forming tap; the other five cut. So a family that answers any value but
|
|
91
|
+
* `3-Forming Tap` is a cutting family, and each of the three below was probed
|
|
92
|
+
* for its own value **and** for `3-Forming Tap`, which returned the vendor's
|
|
93
|
+
* no-results notice every time.
|
|
94
|
+
*
|
|
95
|
+
* That is why these are `vendor-stated` and not `assumed`: the cite is a query
|
|
96
|
+
* anybody can re-run, and the *negative* half of it is the part that matters.
|
|
97
|
+
* A `cite` naming only the CSV's own filename would be this table reading its
|
|
98
|
+
* own name back to itself.
|
|
99
|
+
*/
|
|
100
|
+
const SPIRAL_POINT_TAP = {
|
|
101
|
+
value: 'cutting',
|
|
102
|
+
source: 'vendor-stated',
|
|
103
|
+
cite: "the vendor's own `newTapType` facet on the variants endpoint: every row of this family answers `:relevance:obsoleteFacet:false:newTapType:11-Spiral Point Tap`, and `3-Forming Tap` — the one forming value of the six — returns the no-results notice (JG 2026-09-07)",
|
|
104
|
+
};
|
|
105
|
+
const HAND_TAP = {
|
|
106
|
+
value: 'cutting',
|
|
107
|
+
source: 'vendor-stated',
|
|
108
|
+
cite: 'the same `newTapType` facet: every row of this family answers `2-Hand Tap`, and `3-Forming Tap`, `8-Spiral Flute Tap`, `10-Pipe Tap` and `11-Spiral Point Tap` each return the no-results notice (JG 2026-09-07)',
|
|
109
|
+
};
|
|
76
110
|
const HSS_ASSUMED = {
|
|
77
111
|
value: 'hss',
|
|
78
112
|
source: 'assumed',
|
|
@@ -105,6 +139,51 @@ const ER_COLLET_CHUCK = {
|
|
|
105
139
|
source: 'vendor-stated',
|
|
106
140
|
cite: 'breadcrumb ".../ER Collet Chucks/ER(tm) Collet Adapter -BT30"',
|
|
107
141
|
};
|
|
142
|
+
/**
|
|
143
|
+
* The three ER collet styles, each stated once for the families that share it.
|
|
144
|
+
*
|
|
145
|
+
* `style` is the finer axis beside a holder's `clamping`, and here it is what
|
|
146
|
+
* tells a consumer which kind of collet it is holding. All three are
|
|
147
|
+
* Kennametal's own product lines, read off the collet category tree — see
|
|
148
|
+
* `vendors/kennametal/catalog.ts`, which is what enumerates them.
|
|
149
|
+
*/
|
|
150
|
+
const ER_STANDARD = {
|
|
151
|
+
value: 'er-standard',
|
|
152
|
+
source: 'vendor-stated',
|
|
153
|
+
cite: "the vendor's category is 'ER Collets / ER Standard Collets'; the family publishes a CCCN-CCCX capacity band and no square",
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Sealed coolant-through collets. `style` separates them from the standard ones
|
|
157
|
+
* because they behave differently in a way the numbers show but a label should
|
|
158
|
+
* not hide: `CCCX == CCCN` on every row, so each clamps one exact size rather
|
|
159
|
+
* than a 1 mm band.
|
|
160
|
+
*
|
|
161
|
+
* No special case is needed anywhere — a zero-width range is still a range —
|
|
162
|
+
* but a user choosing one deserves to be told which kind it is.
|
|
163
|
+
*
|
|
164
|
+
* **The ER40 inch line is designated larger than it clamps**, and that is the
|
|
165
|
+
* vendor's own statement rather than a rounding: `40ERSS1000` is named for one
|
|
166
|
+
* inch and publishes a capacity of 0.9938 in / 25.243 mm against a `D1` of
|
|
167
|
+
* 1.0 in / 25.4 mm — both figures agree across both unit columns. Ten of its
|
|
168
|
+
* twelve rows do this, up to 0.193 mm on `40ERSS0812`. `holding.NOMINAL_SLACK`
|
|
169
|
+
* is the gate that lets a designation sit outside the size it measures, and
|
|
170
|
+
* those rows are where its bound comes from.
|
|
171
|
+
*/
|
|
172
|
+
const ER_SEALED = {
|
|
173
|
+
value: 'er-sealed',
|
|
174
|
+
source: 'vendor-stated',
|
|
175
|
+
cite: "the vendor's category is 'ER Collets / ER Coolant Through Collets'; CCCX == CCCN == D1 on all 139 rows of the twelve families, and Kennametal specs an H6 shank, so each clamps one exact size",
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Tap collets. The square drive is the fact that matters and it is scraped
|
|
179
|
+
* rather than declared — `holding.ColletRecord.squareSize` — so this is the
|
|
180
|
+
* label and not the discriminant.
|
|
181
|
+
*/
|
|
182
|
+
const ER_TAP = {
|
|
183
|
+
value: 'er-tap',
|
|
184
|
+
source: 'vendor-stated',
|
|
185
|
+
cite: "the vendor's category is 'ER Collets / ER Tap Collets'; every row publishes an S10 square size and no capacity band",
|
|
186
|
+
};
|
|
108
187
|
const METRIC_CATALOG = {
|
|
109
188
|
value: 'millimeters',
|
|
110
189
|
source: 'vendor-stated',
|
|
@@ -307,6 +386,7 @@ export const FAMILIES = {
|
|
|
307
386
|
facts: {
|
|
308
387
|
bmc: HSS_ASSUMED,
|
|
309
388
|
coolantThrough: NO_COOLANT_THROUGH_TAP,
|
|
389
|
+
threadMethod: SPIRAL_POINT_TAP,
|
|
310
390
|
},
|
|
311
391
|
},
|
|
312
392
|
'khsst_hand_metric_plug.csv': {
|
|
@@ -318,6 +398,7 @@ export const FAMILIES = {
|
|
|
318
398
|
facts: {
|
|
319
399
|
bmc: HSS_ASSUMED,
|
|
320
400
|
coolantThrough: NO_COOLANT_THROUGH_TAP,
|
|
401
|
+
threadMethod: HAND_TAP,
|
|
321
402
|
},
|
|
322
403
|
},
|
|
323
404
|
'spiral_point_metric_plug.csv': {
|
|
@@ -329,6 +410,7 @@ export const FAMILIES = {
|
|
|
329
410
|
facts: {
|
|
330
411
|
bmc: HSS_ASSUMED,
|
|
331
412
|
coolantThrough: NO_COOLANT_THROUGH_TAP,
|
|
413
|
+
threadMethod: SPIRAL_POINT_TAP,
|
|
332
414
|
},
|
|
333
415
|
},
|
|
334
416
|
'gomill_pro_radiused_4fl_necked_metric.csv': {
|
|
@@ -636,35 +718,152 @@ export const HOLDER_FAMILIES = {
|
|
|
636
718
|
},
|
|
637
719
|
},
|
|
638
720
|
};
|
|
721
|
+
/**
|
|
722
|
+
* ER collets, one entry per vendor family.
|
|
723
|
+
*
|
|
724
|
+
* **One entry per vendor family and not one per style.** Kennametal codes the
|
|
725
|
+
* coolant-through line as twelve families — one per series per unit — and their
|
|
726
|
+
* column shapes genuinely differ: the inch half publishes `LF` and `L`, the
|
|
727
|
+
* metric half publishes `L9` instead, and only `109321468` publishes both.
|
|
728
|
+
* Merging them into two CSVs would also destroy `ToolholdingDefinition.rows`,
|
|
729
|
+
* which is a per-family restatement and the only independent check that a
|
|
730
|
+
* re-scrape did not lose parts.
|
|
731
|
+
*
|
|
732
|
+
* Every `familyCode` here came out of `kennametal --collets`, which is what
|
|
733
|
+
* `vendors/kennametal/catalog.ts` exists for. The row counts are that walk's
|
|
734
|
+
* own per-family totals, and each category's parts sum to them exactly —
|
|
735
|
+
* 117 + 107 = 224 standard, 75 + 74 = 149 coolant-through, 47 + 49 = 96 tap.
|
|
736
|
+
*
|
|
737
|
+
* ## The four kit families are deliberately absent
|
|
738
|
+
*
|
|
739
|
+
* `100000428`, `100000425`, `109433662` and `109433658` — 26 parts — publish
|
|
740
|
+
* `Kit Series`, `Number-Kit Items`, `Dimension Range-Kit Items` and
|
|
741
|
+
* `Incremental Division-Kit Items`, and no `D1`, no capacity and no length. A
|
|
742
|
+
* kit is a purchasing unit whose contents are already the per-part families
|
|
743
|
+
* below, and minting one into a `holding.ColletRecord` would mean deriving a
|
|
744
|
+
* capacity band from a range string — authoring tool data, which this package
|
|
745
|
+
* does not do. `kennametal --collets` lists them every run, so they stay
|
|
746
|
+
* visible rather than forgotten.
|
|
747
|
+
*
|
|
748
|
+
* ## ER8 fits nothing in this catalog
|
|
749
|
+
*
|
|
750
|
+
* Nine of the 110 standard metric collets are ER8, and no BT30 ER8 adapter is
|
|
751
|
+
* configured. They are scraped anyway: `holding.HolderRecord.colletSeries`
|
|
752
|
+
* states the direction — a collet nothing takes costs an option, and a collet
|
|
753
|
+
* offered for a holder it does not fit costs a machinist a purchase.
|
|
754
|
+
*/
|
|
639
755
|
export const COLLET_FAMILIES = {
|
|
756
|
+
// ── ER standard collets (JG 2026-09-08) ──────────────────────────────────
|
|
640
757
|
'er_standard_collets_metric.csv': {
|
|
641
758
|
catalogName: 'Kennametal ER Standard Collets Metric',
|
|
642
759
|
rows: 110,
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
},
|
|
760
|
+
familyCode: '100000478',
|
|
761
|
+
facts: { style: ER_STANDARD, unit: METRIC_CATALOG },
|
|
762
|
+
},
|
|
763
|
+
'er_standard_collets_inch.csv': {
|
|
764
|
+
catalogName: 'Kennametal ER Standard Collets Inch',
|
|
765
|
+
rows: 98,
|
|
766
|
+
familyCode: '100000479',
|
|
767
|
+
facts: { style: ER_STANDARD, unit: INCH_CATALOG },
|
|
768
|
+
},
|
|
769
|
+
// ── ER coolant-through collets (JG 2026-09-08) ───────────────────────────
|
|
770
|
+
// Twelve families, one per series per unit. `ER8` has no coolant-through
|
|
771
|
+
// line; `ER11` upward do.
|
|
772
|
+
'er11_collets_coolant_through_metric.csv': {
|
|
773
|
+
catalogName: 'Kennametal ER11 Collets Coolant-Through Metric',
|
|
774
|
+
rows: 5,
|
|
775
|
+
familyCode: '109333979',
|
|
776
|
+
facts: { style: ER_SEALED, unit: METRIC_CATALOG },
|
|
777
|
+
},
|
|
778
|
+
'er16_collets_coolant_through_metric.csv': {
|
|
779
|
+
catalogName: 'Kennametal ER16 Collets Coolant-Through Metric',
|
|
780
|
+
rows: 8,
|
|
781
|
+
familyCode: '109333976',
|
|
782
|
+
facts: { style: ER_SEALED, unit: METRIC_CATALOG },
|
|
783
|
+
},
|
|
784
|
+
'er20_collets_coolant_through_metric.csv': {
|
|
785
|
+
catalogName: 'Kennametal ER20 Collets Coolant-Through Metric',
|
|
786
|
+
rows: 11,
|
|
787
|
+
familyCode: '109333973',
|
|
788
|
+
facts: { style: ER_SEALED, unit: METRIC_CATALOG },
|
|
789
|
+
},
|
|
790
|
+
'er25_collets_coolant_through_metric.csv': {
|
|
791
|
+
catalogName: 'Kennametal ER25 Collets Coolant-Through Metric',
|
|
792
|
+
rows: 11,
|
|
793
|
+
familyCode: '109333627',
|
|
794
|
+
facts: { style: ER_SEALED, unit: METRIC_CATALOG },
|
|
795
|
+
},
|
|
796
|
+
'er32_collets_coolant_through_metric.csv': {
|
|
797
|
+
catalogName: 'Kennametal ER32 Collets Coolant-Through Metric',
|
|
798
|
+
rows: 15,
|
|
799
|
+
familyCode: '109333626',
|
|
800
|
+
facts: { style: ER_SEALED, unit: METRIC_CATALOG },
|
|
801
|
+
},
|
|
802
|
+
'er40_collets_coolant_through_metric.csv': {
|
|
803
|
+
catalogName: 'Kennametal ER40 Collets Coolant-Through Metric',
|
|
804
|
+
rows: 19,
|
|
805
|
+
familyCode: '109321468',
|
|
806
|
+
facts: { style: ER_SEALED, unit: METRIC_CATALOG },
|
|
807
|
+
},
|
|
808
|
+
'er11_collets_coolant_through_inch.csv': {
|
|
809
|
+
catalogName: 'Kennametal ER11 Collets Coolant-Through Inch',
|
|
810
|
+
rows: 4,
|
|
811
|
+
familyCode: '109333978',
|
|
812
|
+
facts: { style: ER_SEALED, unit: INCH_CATALOG },
|
|
651
813
|
},
|
|
652
|
-
// Sealed coolant-through collets. `style` separates them from the standard
|
|
653
|
-
// ones because they behave differently in a way the numbers alone show but
|
|
654
|
-
// a label should not hide: CCCX == CCCN on every row, so each clamps one
|
|
655
|
-
// exact size (Kennametal specs H6 shank tolerance) rather than a 1 mm band.
|
|
656
|
-
// No special case is needed anywhere — a zero-width range is still a range
|
|
657
|
-
// — but a user choosing one deserves to be told which kind it is.
|
|
658
814
|
'er16_collets_coolant_through_inch.csv': {
|
|
659
815
|
catalogName: 'Kennametal ER16 Collets Coolant-Through Inch',
|
|
660
816
|
rows: 10,
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
},
|
|
817
|
+
familyCode: '109333975',
|
|
818
|
+
facts: { style: ER_SEALED, unit: INCH_CATALOG },
|
|
819
|
+
},
|
|
820
|
+
'er20_collets_coolant_through_inch.csv': {
|
|
821
|
+
catalogName: 'Kennametal ER20 Collets Coolant-Through Inch',
|
|
822
|
+
rows: 13,
|
|
823
|
+
familyCode: '109333974',
|
|
824
|
+
facts: { style: ER_SEALED, unit: INCH_CATALOG },
|
|
825
|
+
},
|
|
826
|
+
'er25_collets_coolant_through_inch.csv': {
|
|
827
|
+
catalogName: 'Kennametal ER25 Collets Coolant-Through Inch',
|
|
828
|
+
rows: 14,
|
|
829
|
+
familyCode: '109333628',
|
|
830
|
+
facts: { style: ER_SEALED, unit: INCH_CATALOG },
|
|
831
|
+
},
|
|
832
|
+
'er32_collets_coolant_through_inch.csv': {
|
|
833
|
+
catalogName: 'Kennametal ER32 Collets Coolant-Through Inch',
|
|
834
|
+
rows: 17,
|
|
835
|
+
familyCode: '109333625',
|
|
836
|
+
facts: { style: ER_SEALED, unit: INCH_CATALOG },
|
|
837
|
+
},
|
|
838
|
+
'er40_collets_coolant_through_inch.csv': {
|
|
839
|
+
catalogName: 'Kennametal ER40 Collets Coolant-Through Inch',
|
|
840
|
+
rows: 12,
|
|
841
|
+
familyCode: '109321469',
|
|
842
|
+
facts: { style: ER_SEALED, unit: INCH_CATALOG },
|
|
843
|
+
},
|
|
844
|
+
// ── ER tap collets (JG 2026-09-08) ───────────────────────────────────────
|
|
845
|
+
// Two families, split by thread system rather than by series: each holds
|
|
846
|
+
// ER16 through ER40. Neither publishes `CCCN`/`CCCX` at all — see
|
|
847
|
+
// `vendors/kennametal/holding.ts`'s `colletCapacity` for why that is a
|
|
848
|
+
// zero-width band at `D1` rather than an incomplete row.
|
|
849
|
+
//
|
|
850
|
+
// **`unit` is decided by which column carries the exact value**, the same
|
|
851
|
+
// test the TT HPV split above uses. `100000434` publishes `16ERTC10`'s `D1`
|
|
852
|
+
// as 0.194 in and 4.928 mm — 0.194 in is the ANSI shank of a #10 tap, and
|
|
853
|
+
// 4.928 is it rounded — while `100000435` publishes `16ERTC045034M`'s as
|
|
854
|
+
// 4.5 mm and 0.1772 in. So the ANSI family is inch-native and the DIN/ISO
|
|
855
|
+
// one metric-native, which also decides which of the two `Tap Range` columns
|
|
856
|
+
// a record carries: `#14 & 1/4` on one, `M6 & M6.3` on the other.
|
|
857
|
+
'er_tap_collets_ansi.csv': {
|
|
858
|
+
catalogName: 'Kennametal ER Standard Tap Collets ANSI',
|
|
859
|
+
rows: 47,
|
|
860
|
+
familyCode: '100000434',
|
|
861
|
+
facts: { style: ER_TAP, unit: INCH_CATALOG },
|
|
862
|
+
},
|
|
863
|
+
'er_tap_collets_din_iso.csv': {
|
|
864
|
+
catalogName: 'Kennametal ER Standard Tap Collets DIN and ISO',
|
|
865
|
+
rows: 49,
|
|
866
|
+
familyCode: '100000435',
|
|
867
|
+
facts: { style: ER_TAP, unit: METRIC_CATALOG },
|
|
669
868
|
},
|
|
670
869
|
};
|
|
@@ -46,6 +46,37 @@
|
|
|
46
46
|
*
|
|
47
47
|
* ## What is not scraped
|
|
48
48
|
*
|
|
49
|
+
* **The taps are not scraped, and the evidence for when they are is here.**
|
|
50
|
+
* MariTool sells taps under `c78_148`, and its tree already answers the one
|
|
51
|
+
* question a tap record needs that no vendor of ours states in a column —
|
|
52
|
+
* whether the tap cuts its thread or forms it. Only the forming branch carries
|
|
53
|
+
* the word, so the leaf name settles it (JG 2026-09-07):
|
|
54
|
+
*
|
|
55
|
+
* | Leaf | cPath | Method |
|
|
56
|
+
* | ----------------------- | ------------------ | --------- |
|
|
57
|
+
* | Thread Forming Taps | `78_148_274` | `forming` |
|
|
58
|
+
* | — Plug Form Tap | `78_148_274_275` | `forming` |
|
|
59
|
+
* | — Bottoming Form Tap | `78_148_274_276` | `forming` |
|
|
60
|
+
* | Spiral Flute Taps | `78_148_149` | `cutting` |
|
|
61
|
+
* | Spiral Point Taps | `78_148_224` | `cutting` |
|
|
62
|
+
* | Taps for Aluminum | `78_148_271` | `cutting` |
|
|
63
|
+
* | Taper Pipe Taps | `78_148_283` | `cutting` |
|
|
64
|
+
* | DIN Length HPT Taps | `78_148_284` | `cutting` |
|
|
65
|
+
*
|
|
66
|
+
* **Checked into a table when it is built, not run as a filter**, for the
|
|
67
|
+
* reason the ER leaves above already give: a rule matching a name drops
|
|
68
|
+
* silently when the vendor renames a category, and three leaves here are
|
|
69
|
+
* already named something other than what they hold. `maritool --catalog` walks
|
|
70
|
+
* the tree, which is how the rule gets re-checked rather than trusted.
|
|
71
|
+
*
|
|
72
|
+
* **What still has to be decided before a tap family lands.** A MariTool tap
|
|
73
|
+
* page publishes `Shank Size`, `Total Length`, `Thread Length` and `Size` —
|
|
74
|
+
* enough for `SFDM`, `OAL`, `LCF` and a designation `thread.ts` can already
|
|
75
|
+
* parse — but a metric tap states `Size: M3` and `Pitch Diameter: 0.5` against
|
|
76
|
+
* a body in inches (`Shank Size: 0.141`, `Total Length: 1 15/16`). No family in
|
|
77
|
+
* this package is mixed that way and `ToolRecord.unit` is one value, so that is
|
|
78
|
+
* a decision on its own merits and not a widening of this table.
|
|
79
|
+
*
|
|
49
80
|
* **BT50 and ISO30 produce no CSV.** MariTool's only BT50 category is end mill
|
|
50
81
|
* holders, which is out of scope; ISO30 has three ER parts and none of them
|
|
51
82
|
* publishes a spec table, so the family would be a receipt of nothing.
|
|
@@ -46,6 +46,37 @@
|
|
|
46
46
|
*
|
|
47
47
|
* ## What is not scraped
|
|
48
48
|
*
|
|
49
|
+
* **The taps are not scraped, and the evidence for when they are is here.**
|
|
50
|
+
* MariTool sells taps under `c78_148`, and its tree already answers the one
|
|
51
|
+
* question a tap record needs that no vendor of ours states in a column —
|
|
52
|
+
* whether the tap cuts its thread or forms it. Only the forming branch carries
|
|
53
|
+
* the word, so the leaf name settles it (JG 2026-09-07):
|
|
54
|
+
*
|
|
55
|
+
* | Leaf | cPath | Method |
|
|
56
|
+
* | ----------------------- | ------------------ | --------- |
|
|
57
|
+
* | Thread Forming Taps | `78_148_274` | `forming` |
|
|
58
|
+
* | — Plug Form Tap | `78_148_274_275` | `forming` |
|
|
59
|
+
* | — Bottoming Form Tap | `78_148_274_276` | `forming` |
|
|
60
|
+
* | Spiral Flute Taps | `78_148_149` | `cutting` |
|
|
61
|
+
* | Spiral Point Taps | `78_148_224` | `cutting` |
|
|
62
|
+
* | Taps for Aluminum | `78_148_271` | `cutting` |
|
|
63
|
+
* | Taper Pipe Taps | `78_148_283` | `cutting` |
|
|
64
|
+
* | DIN Length HPT Taps | `78_148_284` | `cutting` |
|
|
65
|
+
*
|
|
66
|
+
* **Checked into a table when it is built, not run as a filter**, for the
|
|
67
|
+
* reason the ER leaves above already give: a rule matching a name drops
|
|
68
|
+
* silently when the vendor renames a category, and three leaves here are
|
|
69
|
+
* already named something other than what they hold. `maritool --catalog` walks
|
|
70
|
+
* the tree, which is how the rule gets re-checked rather than trusted.
|
|
71
|
+
*
|
|
72
|
+
* **What still has to be decided before a tap family lands.** A MariTool tap
|
|
73
|
+
* page publishes `Shank Size`, `Total Length`, `Thread Length` and `Size` —
|
|
74
|
+
* enough for `SFDM`, `OAL`, `LCF` and a designation `thread.ts` can already
|
|
75
|
+
* parse — but a metric tap states `Size: M3` and `Pitch Diameter: 0.5` against
|
|
76
|
+
* a body in inches (`Shank Size: 0.141`, `Total Length: 1 15/16`). No family in
|
|
77
|
+
* this package is mixed that way and `ToolRecord.unit` is one value, so that is
|
|
78
|
+
* a decision on its own merits and not a widening of this table.
|
|
79
|
+
*
|
|
49
80
|
* **BT50 and ISO30 produce no CSV.** MariTool's only BT50 category is end mill
|
|
50
81
|
* holders, which is out of scope; ISO30 has three ER parts and none of them
|
|
51
82
|
* publishes a spec table, so the family would be a receipt of nothing.
|
package/dist/family.d.ts
CHANGED
|
@@ -20,12 +20,12 @@ import type { UnitSystem } from './conventions.js';
|
|
|
20
20
|
import type { BrandName } from './identity.js';
|
|
21
21
|
import type { HoldingMapper, ToolholdingKind } from './holding.js';
|
|
22
22
|
import type { Fact } from './provenance.js';
|
|
23
|
-
import type { ColumnMap, ToolKind, ToolRecord } from './records.js';
|
|
23
|
+
import type { ColumnMap, ThreadMethod, ToolKind, ToolRecord } from './records.js';
|
|
24
24
|
import type { MapperOptions, ScrapedRow } from './scrape.js';
|
|
25
25
|
/**
|
|
26
26
|
* The per-family constants a fact can carry, and their types.
|
|
27
27
|
*
|
|
28
|
-
*
|
|
28
|
+
* Twelve keys, which is the whole vocabulary the catalog uses. Naming them rather
|
|
29
29
|
* than accepting any string is what lets a mapper read `family.coolantThrough`
|
|
30
30
|
* as a `boolean` instead of casting an `unknown` out of a bag — and what makes
|
|
31
31
|
* a fact whose value is the wrong type a compile error where the family is
|
|
@@ -49,6 +49,20 @@ export interface FamilyFacts {
|
|
|
49
49
|
*/
|
|
50
50
|
profile?: string;
|
|
51
51
|
coolantThrough?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* How a tap makes its thread — `cutting` or `forming`.
|
|
54
|
+
*
|
|
55
|
+
* A fact and not a column because neither vendor publishes it per part: it is
|
|
56
|
+
* Kennametal's `newTapType` facet, which the variant table does not carry,
|
|
57
|
+
* and EMUGE's category, which partitions its taps into two. Every declaration
|
|
58
|
+
* in `families/` cites the index it was read off, and each is one value for a
|
|
59
|
+
* whole family — checked against the vendor rather than assumed, which is why
|
|
60
|
+
* they are `vendor-stated`.
|
|
61
|
+
*
|
|
62
|
+
* Every `kind: 'tap'` family must state it: `tapRecord` reads it through
|
|
63
|
+
* {@link fact}, and `records.toolRecord` refuses a tap record without one.
|
|
64
|
+
*/
|
|
65
|
+
threadMethod?: ThreadMethod;
|
|
52
66
|
flutes?: number;
|
|
53
67
|
/** Degrees included. */
|
|
54
68
|
pointAngle?: number;
|
|
@@ -114,6 +128,18 @@ export interface FamilyDefinition extends CommonDefinition {
|
|
|
114
128
|
export interface ToolholdingDefinition extends CommonDefinition {
|
|
115
129
|
/** What a human calls this family. Holders and collets have no `id`. */
|
|
116
130
|
readonly catalogName: string;
|
|
131
|
+
/**
|
|
132
|
+
* The vendor's own code for the family, where the scrape target is a family
|
|
133
|
+
* page — {@link FamilyDefinition.familyCode}'s counterpart.
|
|
134
|
+
*
|
|
135
|
+
* Absent where the vendor has no such page: REGO-FIX and MariTool families
|
|
136
|
+
* are a set of index filters and name none. It was absent on Kennametal's
|
|
137
|
+
* toolholding too until 2026-09-08, because those families were scraped by
|
|
138
|
+
* hand from a code read off the page at the time — which made a re-scrape a
|
|
139
|
+
* trip back to the browser. Recording it is what makes one re-runnable, and
|
|
140
|
+
* it is what `kennametal --collets` reconciles its walk against.
|
|
141
|
+
*/
|
|
142
|
+
readonly familyCode?: string;
|
|
117
143
|
}
|
|
118
144
|
/** A cutting-tool family after {@link FamilyDefinition} has been validated. */
|
|
119
145
|
export type BoundFamily = Omit<FamilyDefinition, 'columns'> & FamilyFacts & {
|
package/dist/holding.d.ts
CHANGED
|
@@ -211,6 +211,41 @@ export interface ColletRecord extends HoldingIdentity {
|
|
|
211
211
|
readonly functionalLength: number | null;
|
|
212
212
|
/** `L` — overall length. */
|
|
213
213
|
readonly overallLength: number | null;
|
|
214
|
+
/**
|
|
215
|
+
* `L9` — how deep the clamping bore is, and therefore how much shank the
|
|
216
|
+
* collet actually holds.
|
|
217
|
+
*
|
|
218
|
+
* Twinned in millimetres because it is **compared and not only displayed**:
|
|
219
|
+
* it is `@toolpath/tool-support`'s `Collet.clampLength`, the one input to
|
|
220
|
+
* `maxStickout`, which answers `null` for every collet without it. That is
|
|
221
|
+
* the rule this module states for `bore`, `gaugeLength` and the capacity
|
|
222
|
+
* pair, and this is the fourth dimension to earn it.
|
|
223
|
+
*
|
|
224
|
+
* Not a second overall length. Kennametal's `109321468` publishes both: `L`
|
|
225
|
+
* is 46 mm down all nineteen ER40 rows while `L9` runs 22 / 28 / 46 by size.
|
|
226
|
+
*/
|
|
227
|
+
readonly clampingLength: number | null;
|
|
228
|
+
readonly clampingLengthMm: number | null;
|
|
229
|
+
/**
|
|
230
|
+
* The vendor's own designation of the taps this collet is for, verbatim —
|
|
231
|
+
* `"M6 & M6.3"`, `"#14 & 1/4"`.
|
|
232
|
+
*
|
|
233
|
+
* **Text, and deliberately not parsed.** It names two thread designations in
|
|
234
|
+
* the vendor's own words and this package does not author tool data; what a
|
|
235
|
+
* fit rule compares is {@link ColletRecord.clampMin}/{@link ColletRecord.clampMax},
|
|
236
|
+
* which are numbers. `null` on every collet whose vendor publishes none.
|
|
237
|
+
*/
|
|
238
|
+
readonly tapRange: string | null;
|
|
239
|
+
/**
|
|
240
|
+
* `S10` — the square drive the bore carries, across flats.
|
|
241
|
+
*
|
|
242
|
+
* The fact that makes a tap collet a tap collet: its bore is not round, so
|
|
243
|
+
* the part it takes is a tap and nothing else. A shank test alone would say
|
|
244
|
+
* yes to an end mill of the same diameter, held by nothing —
|
|
245
|
+
* `@toolpath/tool-support`'s `holderTakesTool` reads this to refuse that.
|
|
246
|
+
* `null` where the vendor publishes no square, which is every round collet.
|
|
247
|
+
*/
|
|
248
|
+
readonly squareSize: number | null;
|
|
214
249
|
}
|
|
215
250
|
/** Either toolholding record. Narrow on {@link HoldingIdentity.kind}. */
|
|
216
251
|
export type HoldingRecord = HolderRecord | ColletRecord;
|
|
@@ -295,6 +330,29 @@ export declare function millimeters(value: number | null, unit: UnitSystem): num
|
|
|
295
330
|
* for a machinist's number and refuses a range rather than summing it.
|
|
296
331
|
*/
|
|
297
332
|
export declare function dim(row: ScrapedRow, label: string, unit: UnitSystem): number | null;
|
|
333
|
+
/**
|
|
334
|
+
* How far a collet's *designation* may sit from its measured capacity, as a
|
|
335
|
+
* fraction of that capacity.
|
|
336
|
+
*
|
|
337
|
+
* {@link ColletRecord.nominal} is the size the vendor **designates** the collet
|
|
338
|
+
* by; {@link ColletRecord.clampMin}/{@link ColletRecord.clampMax} is what it
|
|
339
|
+
* measures. They are not two readings of one number, and on a sealed collet
|
|
340
|
+
* they are routinely different: Kennametal's `40ERSS1000` is designated 1 inch
|
|
341
|
+
* and clamps 0.9938, `40ERSS0500` is designated 1/2 and clamps 0.4943. Both
|
|
342
|
+
* unit columns agree on both figures — 25.4 against 25.243 mm, 12.7 against
|
|
343
|
+
* 12.556 — so this is the vendor stating an undersized capacity for a collet it
|
|
344
|
+
* names by the fraction, not a cell in the wrong column.
|
|
345
|
+
*
|
|
346
|
+
* **Relative rather than absolute, because the gap scales with the collet.**
|
|
347
|
+
* Across the 443-part collet corpus fourteen rows sit outside their own band.
|
|
348
|
+
* The widest is `40ERSS0812` at 0.193 mm, which is 0.94 % of its 20.4 mm
|
|
349
|
+
* capacity; the widest by fraction is `40ERSS0500` at 1.14 %. The error this
|
|
350
|
+
* must still refuse is a cell in the wrong unit system, which is 96 % out — the
|
|
351
|
+
* shape `16ERSS0312` has, where `D1`'s metric cell holds the inch value. This
|
|
352
|
+
* sits 4.4x above the first and 19x below the second. A row that lands in
|
|
353
|
+
* between is a finding to investigate rather than a number to widen.
|
|
354
|
+
*/
|
|
355
|
+
export declare const NOMINAL_SLACK = 0.05;
|
|
298
356
|
/**
|
|
299
357
|
* Report where a vendor's own millimetre and inch cells disagree.
|
|
300
358
|
*
|
|
@@ -387,10 +445,10 @@ type HolderFields = Omit<HolderRecord, 'kind' | 'guid' | 'vendor' | 'productLink
|
|
|
387
445
|
* mutated one would be reaching back across the seam this type exists to draw.
|
|
388
446
|
*/
|
|
389
447
|
export declare function holderRecord(fields: HolderFields): HolderRecord;
|
|
390
|
-
/** The same, for a collet. REGO-FIX publishes none of these
|
|
391
|
-
type OptionalColletFields = 'nominal' | 'bodyDiameter' | 'functionalLength' | 'overallLength';
|
|
448
|
+
/** The same, for a collet. REGO-FIX publishes none of these seven. */
|
|
449
|
+
type OptionalColletFields = 'nominal' | 'bodyDiameter' | 'functionalLength' | 'overallLength' | 'clampingLength' | 'tapRange' | 'squareSize';
|
|
392
450
|
/** What a mapper supplies to build a collet. */
|
|
393
|
-
type ColletFields = Omit<ColletRecord, 'kind' | 'guid' | 'vendor' | 'productLink' | 'clampMinMm' | 'clampMaxMm' | OptionalColletFields> & Partial<Pick<ColletRecord, OptionalColletFields>>;
|
|
451
|
+
type ColletFields = Omit<ColletRecord, 'kind' | 'guid' | 'vendor' | 'productLink' | 'clampMinMm' | 'clampMaxMm' | 'clampingLengthMm' | OptionalColletFields> & Partial<Pick<ColletRecord, OptionalColletFields>>;
|
|
394
452
|
/** Build a {@link ColletRecord}, on the same terms as {@link holderRecord}. */
|
|
395
453
|
export declare function colletRecord(fields: ColletFields): ColletRecord;
|
|
396
454
|
export {};
|
package/dist/holding.js
CHANGED
|
@@ -163,6 +163,29 @@ function halfUlp(raw) {
|
|
|
163
163
|
const fraction = raw.includes('.') ? raw.slice(raw.indexOf('.') + 1) : '';
|
|
164
164
|
return 0.5 * 10 ** -fraction.length;
|
|
165
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* How far a collet's *designation* may sit from its measured capacity, as a
|
|
168
|
+
* fraction of that capacity.
|
|
169
|
+
*
|
|
170
|
+
* {@link ColletRecord.nominal} is the size the vendor **designates** the collet
|
|
171
|
+
* by; {@link ColletRecord.clampMin}/{@link ColletRecord.clampMax} is what it
|
|
172
|
+
* measures. They are not two readings of one number, and on a sealed collet
|
|
173
|
+
* they are routinely different: Kennametal's `40ERSS1000` is designated 1 inch
|
|
174
|
+
* and clamps 0.9938, `40ERSS0500` is designated 1/2 and clamps 0.4943. Both
|
|
175
|
+
* unit columns agree on both figures — 25.4 against 25.243 mm, 12.7 against
|
|
176
|
+
* 12.556 — so this is the vendor stating an undersized capacity for a collet it
|
|
177
|
+
* names by the fraction, not a cell in the wrong column.
|
|
178
|
+
*
|
|
179
|
+
* **Relative rather than absolute, because the gap scales with the collet.**
|
|
180
|
+
* Across the 443-part collet corpus fourteen rows sit outside their own band.
|
|
181
|
+
* The widest is `40ERSS0812` at 0.193 mm, which is 0.94 % of its 20.4 mm
|
|
182
|
+
* capacity; the widest by fraction is `40ERSS0500` at 1.14 %. The error this
|
|
183
|
+
* must still refuse is a cell in the wrong unit system, which is 96 % out — the
|
|
184
|
+
* shape `16ERSS0312` has, where `D1`'s metric cell holds the inch value. This
|
|
185
|
+
* sits 4.4x above the first and 19x below the second. A row that lands in
|
|
186
|
+
* between is a finding to investigate rather than a number to widen.
|
|
187
|
+
*/
|
|
188
|
+
export const NOMINAL_SLACK = 0.05;
|
|
166
189
|
/**
|
|
167
190
|
* Report where a vendor's own millimetre and inch cells disagree.
|
|
168
191
|
*
|
|
@@ -298,11 +321,25 @@ export function checkCollet(record) {
|
|
|
298
321
|
// In the native unit, which is the gate with teeth: these are the values a
|
|
299
322
|
// consumer compares, and the contradictory cells this catalog knows about all
|
|
300
323
|
// sit in the column `dim` ignores.
|
|
324
|
+
//
|
|
325
|
+
// The slack is {@link NOMINAL_SLACK}, because a designation is not a
|
|
326
|
+
// measurement — see that constant for the fourteen rows it exists for and the
|
|
327
|
+
// margin either side of it. Unit-free, so it needs no conversion.
|
|
301
328
|
if (record.nominal !== null &&
|
|
302
|
-
(record.nominal < record.clampMin
|
|
329
|
+
(record.nominal < record.clampMin * (1 - NOMINAL_SLACK) ||
|
|
330
|
+
record.nominal > record.clampMax * (1 + NOMINAL_SLACK))) {
|
|
303
331
|
throw new VendorResponseError(what, `nominal ${record.nominal} is outside its own capacity ` +
|
|
304
332
|
`${record.clampMin}-${record.clampMax}`);
|
|
305
333
|
}
|
|
334
|
+
// A tap's square across flats is inscribed in its shank, so it is smaller
|
|
335
|
+
// than the bore that takes it — true on all 96 rows of both Kennametal tap
|
|
336
|
+
// families. A square at or past the clamping diameter is the inch cell in the
|
|
337
|
+
// metric column or two labels swapped, which is the failure `dim`'s native
|
|
338
|
+
// read cannot see and `checkUnitAgreement` only reports.
|
|
339
|
+
if (record.squareSize !== null && record.squareSize >= record.clampMax) {
|
|
340
|
+
throw new VendorResponseError(what, `square size ${record.squareSize} is not smaller than the ` +
|
|
341
|
+
`${record.clampMax} it clamps — a square is inscribed in the shank`);
|
|
342
|
+
}
|
|
306
343
|
}
|
|
307
344
|
/**
|
|
308
345
|
* Build a {@link HolderRecord}: mint its guid, derive its millimetre twins, and
|
|
@@ -354,6 +391,10 @@ export function colletRecord(fields) {
|
|
|
354
391
|
bodyDiameter: fields.bodyDiameter ?? null,
|
|
355
392
|
functionalLength: fields.functionalLength ?? null,
|
|
356
393
|
overallLength: fields.overallLength ?? null,
|
|
394
|
+
clampingLength: fields.clampingLength ?? null,
|
|
395
|
+
clampingLengthMm: millimeters(fields.clampingLength ?? null, fields.unit),
|
|
396
|
+
tapRange: fields.tapRange ?? null,
|
|
397
|
+
squareSize: fields.squareSize ?? null,
|
|
357
398
|
});
|
|
358
399
|
checkCollet(record);
|
|
359
400
|
return record;
|
package/dist/identity.d.ts
CHANGED
|
@@ -75,7 +75,7 @@ export declare const BRANDS: {
|
|
|
75
75
|
readonly host: "destinytool.com";
|
|
76
76
|
readonly home: "https://destinytool.com";
|
|
77
77
|
readonly vendor: "Destiny Tool";
|
|
78
|
-
readonly productLink: "https://destinytool.com/products
|
|
78
|
+
readonly productLink: "https://destinytool.com/products/{material}";
|
|
79
79
|
};
|
|
80
80
|
readonly harvey: {
|
|
81
81
|
readonly host: "harveytool.com";
|
package/dist/identity.js
CHANGED
|
@@ -54,16 +54,13 @@ export const BRANDS = {
|
|
|
54
54
|
productLink: 'https://us.rego-fix.com/en/productfinder?q={material}',
|
|
55
55
|
},
|
|
56
56
|
// Destiny Tool is a Next.js SPA with no product data in the HTML at all — it
|
|
57
|
-
// reads live from a Firestore database.
|
|
58
|
-
//
|
|
59
|
-
// ProductFinder, nothing here has confirmed a search query parameter the SPA
|
|
60
|
-
// actually reads, so this is a best-effort link to the listing page rather
|
|
61
|
-
// than a verified deep link (JG 2026-08-19).
|
|
57
|
+
// reads live from a Firestore database. Its client-rendered product page is
|
|
58
|
+
// addressable by the vendor's item number in the path.
|
|
62
59
|
destinytool: {
|
|
63
60
|
host: 'destinytool.com',
|
|
64
61
|
home: 'https://destinytool.com',
|
|
65
62
|
vendor: 'Destiny Tool',
|
|
66
|
-
productLink: 'https://destinytool.com/products
|
|
63
|
+
productLink: 'https://destinytool.com/products/{material}',
|
|
67
64
|
},
|
|
68
65
|
// Harvey Tool is the first brand here whose per-part link is a real page the
|
|
69
66
|
// vendor already publishes: every tool number in a product table is rendered
|