@toolpath/tool-scraper 2.2.0 → 2.4.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 +18 -8
- package/dist/conventions.d.ts +12 -2
- package/dist/families/emuge.d.ts +51 -3
- package/dist/families/emuge.js +105 -15
- package/dist/families/kennametal.d.ts +15 -0
- package/dist/families/kennametal.js +37 -0
- package/dist/families/maritool.d.ts +31 -0
- package/dist/families/maritool.js +31 -0
- package/dist/family.d.ts +16 -2
- package/dist/identity.d.ts +1 -1
- package/dist/identity.js +3 -6
- package/dist/measure.d.ts +14 -10
- package/dist/measure.js +14 -13
- package/dist/profiles.d.ts +13 -3
- package/dist/provenance.d.ts +9 -1
- package/dist/provenance.js +9 -1
- package/dist/records.d.ts +88 -21
- package/dist/records.js +43 -19
- package/dist/vendors/emuge/records.d.ts +10 -0
- package/dist/vendors/emuge/records.js +12 -0
- package/dist/vendors/kennametal/records.js +1 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -17,14 +17,14 @@ pnpm add @toolpath/tool-scraper
|
|
|
17
17
|
|
|
18
18
|
## Vendors
|
|
19
19
|
|
|
20
|
-
| Vendor | Transport | What it publishes
|
|
21
|
-
| ------------------ | ------------------------------------------------------- |
|
|
22
|
-
| Kennametal / WIDIA | AEM variant-table GET, parsed with `htmlparser2` | tools and toolholding
|
|
23
|
-
| REGO-FIX | Elasticsearch proxy POST + per-part DIN 4000 XML | toolholding
|
|
24
|
-
| Destiny Tool | Firestore REST, paginated | solid end mills
|
|
25
|
-
| Harvey Tool | inline JS literal on a product page, plus its `<thead>` | miniature end mills, keyseat cutters
|
|
26
|
-
| MariTool | osCommerce category listings, then one page per part | toolholding
|
|
27
|
-
| EMUGE-FRANKEN | SAP Commerce JSON API: grouped, variant, batched detail | end mills, twist drills, taps
|
|
20
|
+
| Vendor | Transport | What it publishes |
|
|
21
|
+
| ------------------ | ------------------------------------------------------- | ------------------------------------------------- |
|
|
22
|
+
| Kennametal / WIDIA | AEM variant-table GET, parsed with `htmlparser2` | tools and toolholding |
|
|
23
|
+
| REGO-FIX | Elasticsearch proxy POST + per-part DIN 4000 XML | toolholding |
|
|
24
|
+
| Destiny Tool | Firestore REST, paginated | solid end mills |
|
|
25
|
+
| Harvey Tool | inline JS literal on a product page, plus its `<thead>` | miniature end mills, keyseat cutters |
|
|
26
|
+
| MariTool | osCommerce category listings, then one page per part | toolholding |
|
|
27
|
+
| EMUGE-FRANKEN | SAP Commerce JSON API: grouped, variant, batched detail | end mills, twist drills, taps cutting and forming |
|
|
28
28
|
|
|
29
29
|
## Two entry points
|
|
30
30
|
|
|
@@ -77,6 +77,7 @@ toolpath-scrape harvey --catalog # what the four category trees
|
|
|
77
77
|
toolpath-scrape maritool maritool_cat40_holders.csv # its leaf categories come from its config
|
|
78
78
|
toolpath-scrape maritool --catalog # what the five taper trees hold today
|
|
79
79
|
toolpath-scrape emuge emuge_drills.csv # its category and unit come from its config
|
|
80
|
+
toolpath-scrape emuge emuge_form_taps.csv # FG02, the cold-forming taps
|
|
80
81
|
```
|
|
81
82
|
|
|
82
83
|
`toolpath-scrape --help` lists the rest.
|
|
@@ -99,6 +100,15 @@ here. Every Harvey record is `unspecified`: Harvey's material index is published
|
|
|
99
100
|
than in a variant table, and a scrape cannot reach it — see
|
|
100
101
|
[`docs/HARVEY_PRODUCT_TABLE.md`](docs/HARVEY_PRODUCT_TABLE.md) §1.5.1.
|
|
101
102
|
|
|
103
|
+
`threadMethod` is `cutting` or `forming` on a tap and `null` on everything else,
|
|
104
|
+
because the question does not apply to a drill. It is the one field that
|
|
105
|
+
separates a thread former from a cut tap: the two are the same `DC`, `TP`,
|
|
106
|
+
`SFDM`, `OAL` and `LCF`, often the same substrate and coating, and a shop drills
|
|
107
|
+
a larger hole before one than before the other. Neither vendor publishes it in a
|
|
108
|
+
variant table — Kennametal states it in a `newTapType` facet and EMUGE in the
|
|
109
|
+
split between its `FG01` and `FG02` categories — so it is a per-family fact, and
|
|
110
|
+
each family cites the index it was read off.
|
|
111
|
+
|
|
102
112
|
Every per-family constant no vendor table states carries its provenance — whether it was
|
|
103
113
|
vendor-stated, derived or assumed, and by whom on what date. The types enforce it: an assumed fact
|
|
104
114
|
without a note, a date and initials does not compile.
|
package/dist/conventions.d.ts
CHANGED
|
@@ -40,9 +40,19 @@
|
|
|
40
40
|
* confidently wrong rather than obviously broken. See `records.GEOMETRY_FIELDS`
|
|
41
41
|
* for the canonical side of that line.
|
|
42
42
|
*/
|
|
43
|
+
import type { UnitSystem } from '@toolpath/tool-support';
|
|
43
44
|
import type { BrandName } from './identity.js';
|
|
44
|
-
/**
|
|
45
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Which unit system a family's dimensional columns are published in.
|
|
47
|
+
*
|
|
48
|
+
* `@toolpath/tool-support`'s, re-exported under the name this package has
|
|
49
|
+
* always published. The same two strings were declared here and spelled two
|
|
50
|
+
* other ways downstream — `'metric' | 'inch'`, and a display unit `'mm' | 'in'`
|
|
51
|
+
* — with a lookup table between them on ingest, which is where a metric family
|
|
52
|
+
* quietly becomes an inch one. This spelling won because a scrape originates
|
|
53
|
+
* the fact.
|
|
54
|
+
*/
|
|
55
|
+
export type { UnitSystem } from '@toolpath/tool-support';
|
|
46
56
|
/**
|
|
47
57
|
* The suffix a dimensional column carries, per unit system.
|
|
48
58
|
*
|
package/dist/families/emuge.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* EMUGE-FRANKEN's families.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Five, one per catalog category crossed with the unit system the category is
|
|
5
5
|
* published in. That is coarser than the vendor's own marketing, which splits
|
|
6
6
|
* end mills fifteen ways by product line — TOP-Cut, Hard-Cut, Alu-Cut — and it
|
|
7
7
|
* is deliberate: EMUGE states the product line, the cutting material, the
|
|
@@ -12,8 +12,22 @@
|
|
|
12
12
|
* `PRODUCT_LINE_COLUMNS` reads one per part onto `ToolRecord.productLine`, from
|
|
13
13
|
* a column every scrape already writes.
|
|
14
14
|
*
|
|
15
|
-
* So the only fact
|
|
16
|
-
* two a drill record cannot be built without
|
|
15
|
+
* So the only fact the two milling families state is `unit`; the drill family
|
|
16
|
+
* adds the two a drill record cannot be built without, and each tap family adds
|
|
17
|
+
* how its taps make a thread — see `MACHINE_TAP` and `COLD_FORMING_TAP`.
|
|
18
|
+
*
|
|
19
|
+
* ## The tapping split, which is not a unit split
|
|
20
|
+
*
|
|
21
|
+
* Tapping is the one category EMUGE publishes as **two**: `FG01`, which it
|
|
22
|
+
* titles `Machine taps`, and `FG02`, `Cold forming tap`. They are the same
|
|
23
|
+
* three calls against the same column labels and differ only in what the tools
|
|
24
|
+
* do — one cuts the thread away, the other displaces material into it — so
|
|
25
|
+
* they are two families and not two adapters, and the category is what each
|
|
26
|
+
* one's `threadMethod` fact cites.
|
|
27
|
+
*
|
|
28
|
+
* `FG02` went unscraped until 2026-09-07, which meant `emuge_taps.csv` was the
|
|
29
|
+
* whole of this package's tapping corpus and every row in it was a cutting tap
|
|
30
|
+
* with nothing recording that it was.
|
|
17
31
|
*
|
|
18
32
|
* ## `rows`
|
|
19
33
|
*
|
|
@@ -78,6 +92,9 @@ export declare const SCRAPE_TARGETS: {
|
|
|
78
92
|
readonly 'emuge_taps.csv': {
|
|
79
93
|
readonly category: "FG01";
|
|
80
94
|
};
|
|
95
|
+
readonly 'emuge_form_taps.csv': {
|
|
96
|
+
readonly category: "FG02";
|
|
97
|
+
};
|
|
81
98
|
};
|
|
82
99
|
export declare const FAMILIES: {
|
|
83
100
|
readonly 'emuge_end_mills_inch.csv': {
|
|
@@ -180,6 +197,37 @@ export declare const FAMILIES: {
|
|
|
180
197
|
readonly source: "vendor-stated";
|
|
181
198
|
readonly cite: "every tap dimension is published in millimetres whatever the thread standard — a `#4-40 UNC` tap states `nominal diameter d₁ [mm]` as `2.845 mm`, `pitch [mm]` as `0.635 mm`, and its shank and lengths in `mm` — with `thread symbol`, `nominal size` and `threads per inch` carrying the inch designation beside them";
|
|
182
199
|
};
|
|
200
|
+
readonly threadMethod: {
|
|
201
|
+
readonly value: "cutting";
|
|
202
|
+
readonly source: "vendor-stated";
|
|
203
|
+
readonly cite: "the vendor's own category `FG01`, which it titles `Machine taps`; and independently every one of its 414 grouped products states a `chamfer form` and none states a `lead taper form` (JG 2026-09-07)";
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
readonly 'emuge_form_taps.csv': {
|
|
208
|
+
readonly id: "form-taps";
|
|
209
|
+
readonly brand: "emuge";
|
|
210
|
+
readonly kind: "tap";
|
|
211
|
+
readonly familyCode: "FG02";
|
|
212
|
+
readonly columns: {
|
|
213
|
+
readonly DC: "nominal diameter d₁";
|
|
214
|
+
readonly SFDM: "Shank diameter d₂";
|
|
215
|
+
readonly OAL: "Overall length l₁";
|
|
216
|
+
readonly LCF: "length of cutting edge l₂";
|
|
217
|
+
readonly TP: "pitch";
|
|
218
|
+
};
|
|
219
|
+
readonly rows: 1432;
|
|
220
|
+
readonly facts: {
|
|
221
|
+
readonly unit: {
|
|
222
|
+
readonly value: "millimeters";
|
|
223
|
+
readonly source: "vendor-stated";
|
|
224
|
+
readonly cite: "every tap dimension is published in millimetres whatever the thread standard — a `#4-40 UNC` tap states `nominal diameter d₁ [mm]` as `2.845 mm`, `pitch [mm]` as `0.635 mm`, and its shank and lengths in `mm` — with `thread symbol`, `nominal size` and `threads per inch` carrying the inch designation beside them";
|
|
225
|
+
};
|
|
226
|
+
readonly threadMethod: {
|
|
227
|
+
readonly value: "forming";
|
|
228
|
+
readonly source: "vendor-stated";
|
|
229
|
+
readonly cite: "the vendor's own category `FG02`, which it titles `Cold forming tap`; and independently every one of its 137 grouped products states a `lead taper form` and none states a `chamfer form` (JG 2026-09-07)";
|
|
230
|
+
};
|
|
183
231
|
};
|
|
184
232
|
};
|
|
185
233
|
};
|
package/dist/families/emuge.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* EMUGE-FRANKEN's families.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Five, one per catalog category crossed with the unit system the category is
|
|
5
5
|
* published in. That is coarser than the vendor's own marketing, which splits
|
|
6
6
|
* end mills fifteen ways by product line — TOP-Cut, Hard-Cut, Alu-Cut — and it
|
|
7
7
|
* is deliberate: EMUGE states the product line, the cutting material, the
|
|
@@ -12,8 +12,22 @@
|
|
|
12
12
|
* `PRODUCT_LINE_COLUMNS` reads one per part onto `ToolRecord.productLine`, from
|
|
13
13
|
* a column every scrape already writes.
|
|
14
14
|
*
|
|
15
|
-
* So the only fact
|
|
16
|
-
* two a drill record cannot be built without
|
|
15
|
+
* So the only fact the two milling families state is `unit`; the drill family
|
|
16
|
+
* adds the two a drill record cannot be built without, and each tap family adds
|
|
17
|
+
* how its taps make a thread — see `MACHINE_TAP` and `COLD_FORMING_TAP`.
|
|
18
|
+
*
|
|
19
|
+
* ## The tapping split, which is not a unit split
|
|
20
|
+
*
|
|
21
|
+
* Tapping is the one category EMUGE publishes as **two**: `FG01`, which it
|
|
22
|
+
* titles `Machine taps`, and `FG02`, `Cold forming tap`. They are the same
|
|
23
|
+
* three calls against the same column labels and differ only in what the tools
|
|
24
|
+
* do — one cuts the thread away, the other displaces material into it — so
|
|
25
|
+
* they are two families and not two adapters, and the category is what each
|
|
26
|
+
* one's `threadMethod` fact cites.
|
|
27
|
+
*
|
|
28
|
+
* `FG02` went unscraped until 2026-09-07, which meant `emuge_taps.csv` was the
|
|
29
|
+
* whole of this package's tapping corpus and every row in it was a cutting tap
|
|
30
|
+
* with nothing recording that it was.
|
|
17
31
|
*
|
|
18
32
|
* ## `rows`
|
|
19
33
|
*
|
|
@@ -53,6 +67,74 @@ export const SCRAPE_TARGETS = {
|
|
|
53
67
|
},
|
|
54
68
|
'emuge_drills.csv': { category: 'FB01' },
|
|
55
69
|
'emuge_taps.csv': { category: 'FG01' },
|
|
70
|
+
'emuge_form_taps.csv': { category: 'FG02' },
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Tapping geometry, identical either side of the cutting/forming split.
|
|
74
|
+
*
|
|
75
|
+
* **EMUGE labels a cold-forming tap's lead `length of cutting edge l₂` too**,
|
|
76
|
+
* and it reaches the CSV under that label, because a scraped column keeps the
|
|
77
|
+
* vendor's own name — `conventions.ts` opens on the reason. A former has no
|
|
78
|
+
* cutting edge and the label is wrong about it; renaming it here would swap one
|
|
79
|
+
* vendor's inaccuracy for this package's invention, and the record's `LCF` is
|
|
80
|
+
* the canonical name either way.
|
|
81
|
+
*/
|
|
82
|
+
const TAP_COLUMNS = {
|
|
83
|
+
DC: 'nominal diameter d₁',
|
|
84
|
+
SFDM: 'Shank diameter d₂',
|
|
85
|
+
OAL: 'Overall length l₁',
|
|
86
|
+
LCF: 'length of cutting edge l₂',
|
|
87
|
+
TP: 'pitch',
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Which of EMUGE's two tap categories a family scrapes, and how it is known.
|
|
91
|
+
*
|
|
92
|
+
* The vendor splits its taps into two categories and scraping either one is
|
|
93
|
+
* already the answer — but the *parts* say so as well, independently, which is
|
|
94
|
+
* what makes these `vendor-stated` rather than a claim about a URL. Every
|
|
95
|
+
* grouped product carries a flat `technicalDetails` list, and:
|
|
96
|
+
*
|
|
97
|
+
* - all 414 `FG01` groups state `chamfer form` and none states `lead taper
|
|
98
|
+
* form`;
|
|
99
|
+
* - all 137 `FG02` groups state `lead taper form` and none states `chamfer
|
|
100
|
+
* form`.
|
|
101
|
+
*
|
|
102
|
+
* Mutually exclusive, both directions, at full coverage (JG 2026-09-07). A
|
|
103
|
+
* chamfer is ground onto a tap that cuts and a lead taper is rolled onto one
|
|
104
|
+
* that forms, so the vendor is naming the same distinction twice.
|
|
105
|
+
* `tests/emuge-corpus.test.ts` re-checks that agreement against a real scrape,
|
|
106
|
+
* which is what keeps these two constants honest without either family reading
|
|
107
|
+
* a column.
|
|
108
|
+
*
|
|
109
|
+
* **Two things that look like this discriminator and are not**, recorded so
|
|
110
|
+
* nobody reaches for them: `Geometry` — the column `vendors/emuge/records.ts`
|
|
111
|
+
* reads as the product line — takes `AL`, `GAL`, `H`, `MULTI`, `SPEED`,
|
|
112
|
+
* `STEEL`, `VA` and `Z` in *both* categories; and the 189 `FG01` variants whose
|
|
113
|
+
* `flute characteristic` is `without` are six EMUGE *Robust* groups, reinforced
|
|
114
|
+
* cutting taps that still state a `chamfer form`.
|
|
115
|
+
*/
|
|
116
|
+
/**
|
|
117
|
+
* The unit both tap families are published in.
|
|
118
|
+
*
|
|
119
|
+
* Shared because the vendor's rule is about tapping and not about either
|
|
120
|
+
* category: a `#4-40 UNC` tap states millimetres whichever of the two it sits
|
|
121
|
+
* in, which is the same thing the module note says about there being no unit
|
|
122
|
+
* facet on tapping at all.
|
|
123
|
+
*/
|
|
124
|
+
const TAP_MILLIMETERS = {
|
|
125
|
+
value: 'millimeters',
|
|
126
|
+
source: 'vendor-stated',
|
|
127
|
+
cite: 'every tap dimension is published in millimetres whatever the thread standard — a `#4-40 UNC` tap states `nominal diameter d₁ [mm]` as `2.845 mm`, `pitch [mm]` as `0.635 mm`, and its shank and lengths in `mm` — with `thread symbol`, `nominal size` and `threads per inch` carrying the inch designation beside them',
|
|
128
|
+
};
|
|
129
|
+
const MACHINE_TAP = {
|
|
130
|
+
value: 'cutting',
|
|
131
|
+
source: 'vendor-stated',
|
|
132
|
+
cite: "the vendor's own category `FG01`, which it titles `Machine taps`; and independently every one of its 414 grouped products states a `chamfer form` and none states a `lead taper form` (JG 2026-09-07)",
|
|
133
|
+
};
|
|
134
|
+
const COLD_FORMING_TAP = {
|
|
135
|
+
value: 'forming',
|
|
136
|
+
source: 'vendor-stated',
|
|
137
|
+
cite: "the vendor's own category `FG02`, which it titles `Cold forming tap`; and independently every one of its 137 grouped products states a `lead taper form` and none states a `chamfer form` (JG 2026-09-07)",
|
|
56
138
|
};
|
|
57
139
|
/** Milling geometry, identical either side of the unit split. */
|
|
58
140
|
const MILLING_COLUMNS = {
|
|
@@ -144,20 +226,28 @@ export const FAMILIES = {
|
|
|
144
226
|
// `records.DIMENSIONAL_COLUMNS` excluding it means: the vendor publishes
|
|
145
227
|
// `pitch [mm]` and this family is millimetres, so the column is already in
|
|
146
228
|
// the record's native unit.
|
|
147
|
-
columns:
|
|
148
|
-
DC: 'nominal diameter d₁',
|
|
149
|
-
SFDM: 'Shank diameter d₂',
|
|
150
|
-
OAL: 'Overall length l₁',
|
|
151
|
-
LCF: 'length of cutting edge l₂',
|
|
152
|
-
TP: 'pitch',
|
|
153
|
-
},
|
|
229
|
+
columns: TAP_COLUMNS,
|
|
154
230
|
rows: 11566,
|
|
155
231
|
facts: {
|
|
156
|
-
unit:
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
232
|
+
unit: TAP_MILLIMETERS,
|
|
233
|
+
threadMethod: MACHINE_TAP,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
// The second half of the vendor's own tapping catalog, and the only forming
|
|
237
|
+
// taps this package reaches. It scrapes the same three calls as `FG01`
|
|
238
|
+
// against the same column labels — see `TAP_COLUMNS` — so it is a family
|
|
239
|
+
// rather than an adapter: what differs is the category, and what the
|
|
240
|
+
// category settles.
|
|
241
|
+
'emuge_form_taps.csv': {
|
|
242
|
+
id: 'form-taps',
|
|
243
|
+
brand: 'emuge',
|
|
244
|
+
kind: 'tap',
|
|
245
|
+
familyCode: 'FG02',
|
|
246
|
+
columns: TAP_COLUMNS,
|
|
247
|
+
rows: 1432,
|
|
248
|
+
facts: {
|
|
249
|
+
unit: TAP_MILLIMETERS,
|
|
250
|
+
threadMethod: COLD_FORMING_TAP,
|
|
161
251
|
},
|
|
162
252
|
},
|
|
163
253
|
};
|
|
@@ -237,6 +237,11 @@ export declare const FAMILIES: {
|
|
|
237
237
|
readonly checked: "2026-08-29";
|
|
238
238
|
readonly by: "JG";
|
|
239
239
|
};
|
|
240
|
+
readonly threadMethod: {
|
|
241
|
+
readonly value: "cutting";
|
|
242
|
+
readonly source: "vendor-stated";
|
|
243
|
+
readonly 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)";
|
|
244
|
+
};
|
|
240
245
|
};
|
|
241
246
|
};
|
|
242
247
|
readonly 'khsst_hand_metric_plug.csv': {
|
|
@@ -265,6 +270,11 @@ export declare const FAMILIES: {
|
|
|
265
270
|
readonly checked: "2026-08-29";
|
|
266
271
|
readonly by: "JG";
|
|
267
272
|
};
|
|
273
|
+
readonly threadMethod: {
|
|
274
|
+
readonly value: "cutting";
|
|
275
|
+
readonly source: "vendor-stated";
|
|
276
|
+
readonly 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)";
|
|
277
|
+
};
|
|
268
278
|
};
|
|
269
279
|
};
|
|
270
280
|
readonly 'spiral_point_metric_plug.csv': {
|
|
@@ -293,6 +303,11 @@ export declare const FAMILIES: {
|
|
|
293
303
|
readonly checked: "2026-08-29";
|
|
294
304
|
readonly by: "JG";
|
|
295
305
|
};
|
|
306
|
+
readonly threadMethod: {
|
|
307
|
+
readonly value: "cutting";
|
|
308
|
+
readonly source: "vendor-stated";
|
|
309
|
+
readonly 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)";
|
|
310
|
+
};
|
|
296
311
|
};
|
|
297
312
|
};
|
|
298
313
|
readonly 'gomill_pro_radiused_4fl_necked_metric.csv': {
|
|
@@ -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',
|
|
@@ -307,6 +341,7 @@ export const FAMILIES = {
|
|
|
307
341
|
facts: {
|
|
308
342
|
bmc: HSS_ASSUMED,
|
|
309
343
|
coolantThrough: NO_COOLANT_THROUGH_TAP,
|
|
344
|
+
threadMethod: SPIRAL_POINT_TAP,
|
|
310
345
|
},
|
|
311
346
|
},
|
|
312
347
|
'khsst_hand_metric_plug.csv': {
|
|
@@ -318,6 +353,7 @@ export const FAMILIES = {
|
|
|
318
353
|
facts: {
|
|
319
354
|
bmc: HSS_ASSUMED,
|
|
320
355
|
coolantThrough: NO_COOLANT_THROUGH_TAP,
|
|
356
|
+
threadMethod: HAND_TAP,
|
|
321
357
|
},
|
|
322
358
|
},
|
|
323
359
|
'spiral_point_metric_plug.csv': {
|
|
@@ -329,6 +365,7 @@ export const FAMILIES = {
|
|
|
329
365
|
facts: {
|
|
330
366
|
bmc: HSS_ASSUMED,
|
|
331
367
|
coolantThrough: NO_COOLANT_THROUGH_TAP,
|
|
368
|
+
threadMethod: SPIRAL_POINT_TAP,
|
|
332
369
|
},
|
|
333
370
|
},
|
|
334
371
|
'gomill_pro_radiused_4fl_necked_metric.csv': {
|
|
@@ -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;
|
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
|
package/dist/measure.d.ts
CHANGED
|
@@ -41,16 +41,22 @@
|
|
|
41
41
|
* writes `1 1/2` and admits no hyphen at all. What it hands here is
|
|
42
42
|
* {@link Measured}, which is the part they have in common.
|
|
43
43
|
*
|
|
44
|
-
* **`MM_PER_INCH`
|
|
45
|
-
* exported
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
44
|
+
* **`MM_PER_INCH` and `convertLength` are `@toolpath/tool-support`'s**, and are
|
|
45
|
+
* re-exported here under the names this package has always published.
|
|
46
|
+
*
|
|
47
|
+
* They were declared and exported twice inside this package alone —
|
|
48
|
+
* `vendors/harvey/value.ts` and `vendors/regofix/scrape.ts` — so
|
|
49
|
+
* `@toolpath/tool-scraper/vendors/harvey` and `.../vendors/regofix` each
|
|
50
|
+
* published their own copy of 25.4. `tests/vendor-boundary.test.ts` refuses that
|
|
51
|
+
* by name and moving the constant up here fixed it *within* this package, while
|
|
52
|
+
* a third copy went on standing in the application downstream. The domain
|
|
53
|
+
* package is where a constant every consumer shares can only be declared once,
|
|
54
|
+
* and `@toolpath/tool-support`'s own boundary test now holds the whole tree to
|
|
55
|
+
* one `25.4`.
|
|
49
56
|
*/
|
|
50
|
-
import type
|
|
57
|
+
import { convertLength, MM_PER_INCH, type UnitSystem } from '@toolpath/tool-support';
|
|
51
58
|
import { type Warn } from './scrape.js';
|
|
52
|
-
|
|
53
|
-
export declare const MM_PER_INCH = 25.4;
|
|
59
|
+
export { convertLength, MM_PER_INCH };
|
|
54
60
|
/**
|
|
55
61
|
* `.250` -> 0.25, `3/4` -> 0.75, `1-1/2` -> 1.5, `2` -> 2.
|
|
56
62
|
*
|
|
@@ -63,8 +69,6 @@ export declare const MM_PER_INCH = 25.4;
|
|
|
63
69
|
* that guessed would be the mistake `conventions.UNIT_SUFFIX` exists to prevent.
|
|
64
70
|
*/
|
|
65
71
|
export declare function fractionValue(token: string): number | null;
|
|
66
|
-
/** `value`, converted from `from` to `to`. A no-op when they agree. */
|
|
67
|
-
export declare function convertLength(value: number, from: UnitSystem, to: UnitSystem): number;
|
|
68
72
|
/**
|
|
69
73
|
* What a value states about itself: a length system, or degrees.
|
|
70
74
|
*
|
package/dist/measure.js
CHANGED
|
@@ -41,15 +41,22 @@
|
|
|
41
41
|
* writes `1 1/2` and admits no hyphen at all. What it hands here is
|
|
42
42
|
* {@link Measured}, which is the part they have in common.
|
|
43
43
|
*
|
|
44
|
-
* **`MM_PER_INCH`
|
|
45
|
-
* exported
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
44
|
+
* **`MM_PER_INCH` and `convertLength` are `@toolpath/tool-support`'s**, and are
|
|
45
|
+
* re-exported here under the names this package has always published.
|
|
46
|
+
*
|
|
47
|
+
* They were declared and exported twice inside this package alone —
|
|
48
|
+
* `vendors/harvey/value.ts` and `vendors/regofix/scrape.ts` — so
|
|
49
|
+
* `@toolpath/tool-scraper/vendors/harvey` and `.../vendors/regofix` each
|
|
50
|
+
* published their own copy of 25.4. `tests/vendor-boundary.test.ts` refuses that
|
|
51
|
+
* by name and moving the constant up here fixed it *within* this package, while
|
|
52
|
+
* a third copy went on standing in the application downstream. The domain
|
|
53
|
+
* package is where a constant every consumer shares can only be declared once,
|
|
54
|
+
* and `@toolpath/tool-support`'s own boundary test now holds the whole tree to
|
|
55
|
+
* one `25.4`.
|
|
49
56
|
*/
|
|
57
|
+
import { convertLength, MM_PER_INCH } from '@toolpath/tool-support';
|
|
50
58
|
import { consoleWarn } from './scrape.js';
|
|
51
|
-
|
|
52
|
-
export const MM_PER_INCH = 25.4;
|
|
59
|
+
export { convertLength, MM_PER_INCH };
|
|
53
60
|
/**
|
|
54
61
|
* A decimal, a simple fraction or a mixed number — and nothing else.
|
|
55
62
|
*
|
|
@@ -80,12 +87,6 @@ export function fractionValue(token) {
|
|
|
80
87
|
const value = (whole === undefined ? 0 : Number(whole)) + part;
|
|
81
88
|
return Number.isFinite(value) ? value : null;
|
|
82
89
|
}
|
|
83
|
-
/** `value`, converted from `from` to `to`. A no-op when they agree. */
|
|
84
|
-
export function convertLength(value, from, to) {
|
|
85
|
-
if (from === to)
|
|
86
|
-
return value;
|
|
87
|
-
return to === 'inches' ? value / MM_PER_INCH : value * MM_PER_INCH;
|
|
88
|
-
}
|
|
89
90
|
/**
|
|
90
91
|
* A read cell as a length in `unit`, or null where it publishes none.
|
|
91
92
|
*
|
package/dist/profiles.d.ts
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
* **Collets get no profile.** They publish no CAD model and are not drawn: a
|
|
35
35
|
* collet sits inside the nut, which the holder's own envelope already includes.
|
|
36
36
|
*/
|
|
37
|
+
import type { ProfileDatum, ProfilePoint } from '@toolpath/tool-support';
|
|
37
38
|
import type { HolderRecord } from './holding.js';
|
|
38
39
|
import type { BrandName } from './identity.js';
|
|
39
40
|
/** Bumped when {@link ProfilesDocument}'s shape changes in a way a consumer must handle. */
|
|
@@ -117,8 +118,12 @@ export interface MeasuredHolder {
|
|
|
117
118
|
readonly kernelVersion: string;
|
|
118
119
|
readonly options: ImportOptions;
|
|
119
120
|
}
|
|
120
|
-
/**
|
|
121
|
-
|
|
121
|
+
/**
|
|
122
|
+
* One vertex of a silhouette: `[z, r]`, both in millimetres.
|
|
123
|
+
*
|
|
124
|
+
* `@toolpath/tool-support`'s, re-exported under this package's own name.
|
|
125
|
+
*/
|
|
126
|
+
export type { ProfilePoint } from '@toolpath/tool-support';
|
|
122
127
|
/**
|
|
123
128
|
* What `z = 0` means on a profile.
|
|
124
129
|
*
|
|
@@ -132,8 +137,13 @@ export type ProfilePoint = readonly [z: number, r: number];
|
|
|
132
137
|
* implementation put it. One `datum` over a batch is only true while every
|
|
133
138
|
* holder in it has a taper, and the first Capto or straight-shank holder makes
|
|
134
139
|
* the document's own header wrong about some of its entries.
|
|
140
|
+
*
|
|
141
|
+
* `@toolpath/tool-support`'s, re-exported under this package's own name. It was
|
|
142
|
+
* declared here, in the drawing package, and in the application between them —
|
|
143
|
+
* three copies of two strings, one of which decides whether a consumer may
|
|
144
|
+
* print a gauge length at all.
|
|
135
145
|
*/
|
|
136
|
-
export type ProfileDatum
|
|
146
|
+
export type { ProfileDatum } from '@toolpath/tool-support';
|
|
137
147
|
/** One holder's measured silhouette, and how far it agrees with the vendor. */
|
|
138
148
|
export interface HolderProfile {
|
|
139
149
|
readonly catalogNumber: string;
|
package/dist/provenance.d.ts
CHANGED
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
* is shaped `YYYY-MM-DD` — and `cite: ''` would otherwise satisfy the
|
|
45
45
|
* compiler while citing nothing.
|
|
46
46
|
*/
|
|
47
|
+
import { type Provenance } from '@toolpath/tool-support';
|
|
47
48
|
/** What a per-family constant can be. */
|
|
48
49
|
export type FactValue = string | number | boolean;
|
|
49
50
|
/**
|
|
@@ -53,9 +54,16 @@ export type FactValue = string | number | boolean;
|
|
|
53
54
|
* before the guesses and finishes on the ones only a person stands behind —
|
|
54
55
|
* `derived` first, then `assumed`. (`vendor-stated` leads the order but is
|
|
55
56
|
* filtered out of that document entirely; see {@link assumptions}.)
|
|
57
|
+
*
|
|
58
|
+
* **`@toolpath/tool-support`'s `PROVENANCE`**, re-exported under this package's
|
|
59
|
+
* own name. The order is load-bearing here and is the shared list's, so the
|
|
60
|
+
* ordering that document is read in cannot drift from the vocabulary a drawing
|
|
61
|
+
* marks a derived dimension by. Three identical declarations of these three
|
|
62
|
+
* strings stood before it — this one, the drawing's `Provenance`, and the
|
|
63
|
+
* application's.
|
|
56
64
|
*/
|
|
57
65
|
export declare const SOURCES: readonly ["vendor-stated", "derived", "assumed"];
|
|
58
|
-
export type FactSource =
|
|
66
|
+
export type FactSource = Provenance;
|
|
59
67
|
/**
|
|
60
68
|
* One per-family constant, with its provenance.
|
|
61
69
|
*
|
package/dist/provenance.js
CHANGED
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
* is shaped `YYYY-MM-DD` — and `cite: ''` would otherwise satisfy the
|
|
45
45
|
* compiler while citing nothing.
|
|
46
46
|
*/
|
|
47
|
+
import { PROVENANCE } from '@toolpath/tool-support';
|
|
47
48
|
import { ScraperConfigError } from './errors.js';
|
|
48
49
|
import { compare } from './order.js';
|
|
49
50
|
/**
|
|
@@ -53,8 +54,15 @@ import { compare } from './order.js';
|
|
|
53
54
|
* before the guesses and finishes on the ones only a person stands behind —
|
|
54
55
|
* `derived` first, then `assumed`. (`vendor-stated` leads the order but is
|
|
55
56
|
* filtered out of that document entirely; see {@link assumptions}.)
|
|
57
|
+
*
|
|
58
|
+
* **`@toolpath/tool-support`'s `PROVENANCE`**, re-exported under this package's
|
|
59
|
+
* own name. The order is load-bearing here and is the shared list's, so the
|
|
60
|
+
* ordering that document is read in cannot drift from the vocabulary a drawing
|
|
61
|
+
* marks a derived dimension by. Three identical declarations of these three
|
|
62
|
+
* strings stood before it — this one, the drawing's `Provenance`, and the
|
|
63
|
+
* application's.
|
|
56
64
|
*/
|
|
57
|
-
export const SOURCES =
|
|
65
|
+
export const SOURCES = PROVENANCE;
|
|
58
66
|
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
59
67
|
/** What a note has to say, per source kind. */
|
|
60
68
|
const WANTED = {
|
package/dist/records.d.ts
CHANGED
|
@@ -42,9 +42,43 @@
|
|
|
42
42
|
* always a copy is not a second measurement. An adapter that could supply them
|
|
43
43
|
* separately could supply a tool that claims a holder it does not have.
|
|
44
44
|
*/
|
|
45
|
-
import { type UnitSystem } from '
|
|
45
|
+
import { type ThreadMethod, type UnitSystem } from '@toolpath/tool-support';
|
|
46
46
|
import { type BrandName } from './identity.js';
|
|
47
47
|
import type { FactSource } from './provenance.js';
|
|
48
|
+
/**
|
|
49
|
+
* One canonical geometry name: what it measures, and whose name it is.
|
|
50
|
+
*
|
|
51
|
+
* **Declared here rather than re-exported from `@toolpath/tool-support`**, even
|
|
52
|
+
* though the dictionary's entries are the shared ones. The shared
|
|
53
|
+
* `GeometryField` carries a required `unit` and is `readonly` throughout, and
|
|
54
|
+
* this type has been published since 1.0: adopting it outright would stop a
|
|
55
|
+
* consumer that builds one of these — `{ definition, iso }` — from compiling,
|
|
56
|
+
* for no gain the values below do not already give. The entries *satisfy* the
|
|
57
|
+
* shared shape, so a field renamed upstream is still a compile error here.
|
|
58
|
+
*/
|
|
59
|
+
export interface GeometryField {
|
|
60
|
+
/**
|
|
61
|
+
* What the field measures, phrased so it can be quoted back at whoever
|
|
62
|
+
* mapped a column to the wrong one.
|
|
63
|
+
*/
|
|
64
|
+
definition: string;
|
|
65
|
+
/**
|
|
66
|
+
* The ISO 13399 code for this measurement, or `null` where the standard's
|
|
67
|
+
* counterpart has not been pinned against the dictionary. Equal to the
|
|
68
|
+
* canonical name itself on every field that *is* the standard's code.
|
|
69
|
+
*/
|
|
70
|
+
iso: string | null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* How a tap makes its thread.
|
|
74
|
+
*
|
|
75
|
+
* `@toolpath/tool-support`'s, re-exported under the name this package reads it
|
|
76
|
+
* by — the move `conventions.ts` makes for `UnitSystem` and `provenance.ts` for
|
|
77
|
+
* `PROVENANCE`. A scrape originates the fact and the domain owns the
|
|
78
|
+
* vocabulary, and two declarations of the same two strings is the drift that
|
|
79
|
+
* rule exists to prevent.
|
|
80
|
+
*/
|
|
81
|
+
export type { ThreadMethod } from '@toolpath/tool-support';
|
|
48
82
|
/** The kinds of cutting tool this package maps. */
|
|
49
83
|
export type ToolKind = 'drill' | 'tap' | 'endmill';
|
|
50
84
|
/**
|
|
@@ -87,20 +121,6 @@ export declare const UNSPECIFIED = "unspecified";
|
|
|
87
121
|
* type so that the vocabularies cannot drift apart.
|
|
88
122
|
*/
|
|
89
123
|
export type MaterialGroupsSource = FactSource | typeof UNSPECIFIED;
|
|
90
|
-
/** One canonical geometry name: what it measures, and whose name it is. */
|
|
91
|
-
export interface GeometryField {
|
|
92
|
-
/**
|
|
93
|
-
* What the field measures, phrased so it can be quoted back at whoever
|
|
94
|
-
* mapped a column to the wrong one.
|
|
95
|
-
*/
|
|
96
|
-
definition: string;
|
|
97
|
-
/**
|
|
98
|
-
* The ISO 13399 code for this measurement, or `null` where the standard's
|
|
99
|
-
* counterpart has not been pinned against the dictionary. Equal to the
|
|
100
|
-
* canonical name itself on every field that *is* the standard's code.
|
|
101
|
-
*/
|
|
102
|
-
iso: string | null;
|
|
103
|
-
}
|
|
104
124
|
/**
|
|
105
125
|
* Canonical geometry fields an adapter may supply, and what each means.
|
|
106
126
|
*
|
|
@@ -109,6 +129,26 @@ export interface GeometryField {
|
|
|
109
129
|
* so the vocabulary's source is readable from the code rather than from a plan
|
|
110
130
|
* document.
|
|
111
131
|
*
|
|
132
|
+
* ## What each entry says, and what this table says
|
|
133
|
+
*
|
|
134
|
+
* The definitions and ISO codes are `@toolpath/tool-support`'s — that is the
|
|
135
|
+
* dictionary, and it is shared because a code has to mean one thing in every
|
|
136
|
+
* package that reads one. **This is not that dictionary.** It is the narrower
|
|
137
|
+
* question only a scraper asks: which of those names an *adapter may map a
|
|
138
|
+
* vendor column to*.
|
|
139
|
+
*
|
|
140
|
+
* The two are not the same list and must not become one. The dictionary knows
|
|
141
|
+
* `LBH` and `LD`, which are derived from a tool and a holder downstream and
|
|
142
|
+
* which no vendor publishes; an adapter permitted to map a column to `LBH`
|
|
143
|
+
* could supply a tool that claims a stickout nobody set. It also knows `LSCN`,
|
|
144
|
+
* ISO's clamping-length minimum, which is a real vendor column the day a vendor
|
|
145
|
+
* prints one — and which is deliberately absent below until that day, because
|
|
146
|
+
* the load-time check's job is to refuse a name no scrape can fill.
|
|
147
|
+
*
|
|
148
|
+
* So each entry is an explicit pick out of the shared table rather than a
|
|
149
|
+
* spread of it: a name dropped from the dictionary is a compile error here, and
|
|
150
|
+
* a name added to the dictionary does not silently become mappable.
|
|
151
|
+
*
|
|
112
152
|
* **Seven of the ten are the standard's codes with the standard's meanings.**
|
|
113
153
|
* The three that are not are Autodesk's, and each has an ISO counterpart
|
|
114
154
|
* Autodesk did not use:
|
|
@@ -127,43 +167,53 @@ export interface GeometryField {
|
|
|
127
167
|
*/
|
|
128
168
|
export declare const GEOMETRY_FIELDS: {
|
|
129
169
|
readonly DC: {
|
|
170
|
+
readonly unit: "mm";
|
|
130
171
|
readonly definition: "cutting diameter";
|
|
131
172
|
readonly iso: "DC";
|
|
132
173
|
};
|
|
133
174
|
readonly SFDM: {
|
|
134
|
-
readonly
|
|
175
|
+
readonly unit: "mm";
|
|
176
|
+
readonly definition: "shank diameter \u2014 what the holder grips";
|
|
135
177
|
readonly iso: "DMM";
|
|
136
178
|
};
|
|
137
179
|
readonly OAL: {
|
|
138
|
-
readonly
|
|
180
|
+
readonly unit: "mm";
|
|
181
|
+
readonly definition: "overall length, tip to the end of the shank";
|
|
139
182
|
readonly iso: "OAL";
|
|
140
183
|
};
|
|
141
184
|
readonly LCF: {
|
|
142
|
-
readonly
|
|
185
|
+
readonly unit: "mm";
|
|
186
|
+
readonly definition: "flute length \u2014 the length of the cutting edge";
|
|
143
187
|
readonly iso: "LCF";
|
|
144
188
|
};
|
|
145
189
|
readonly RE: {
|
|
190
|
+
readonly unit: "mm";
|
|
146
191
|
readonly definition: "corner radius; 0 on a square-end tool";
|
|
147
192
|
readonly iso: "RE";
|
|
148
193
|
};
|
|
149
194
|
readonly TP: {
|
|
150
|
-
readonly
|
|
195
|
+
readonly unit: "mm";
|
|
196
|
+
readonly definition: "thread pitch, in the tool\u2019s own unit system";
|
|
151
197
|
readonly iso: "TP";
|
|
152
198
|
};
|
|
153
199
|
readonly NOF: {
|
|
200
|
+
readonly unit: "count";
|
|
154
201
|
readonly definition: "number of flutes";
|
|
155
202
|
readonly iso: "NOF";
|
|
156
203
|
};
|
|
157
204
|
readonly SIG: {
|
|
205
|
+
readonly unit: "deg";
|
|
158
206
|
readonly definition: "point angle, degrees included";
|
|
159
207
|
readonly iso: "SIG";
|
|
160
208
|
};
|
|
161
209
|
readonly 'shoulder-length': {
|
|
210
|
+
readonly unit: "mm";
|
|
162
211
|
readonly definition: "usable length below the full shank";
|
|
163
212
|
readonly iso: null;
|
|
164
213
|
};
|
|
165
214
|
readonly 'shoulder-diameter': {
|
|
166
|
-
readonly
|
|
215
|
+
readonly unit: "mm";
|
|
216
|
+
readonly definition: "diameter at the shoulder \u2014 the neck, where necked";
|
|
167
217
|
readonly iso: null;
|
|
168
218
|
};
|
|
169
219
|
};
|
|
@@ -407,6 +457,23 @@ export interface ToolRecord {
|
|
|
407
457
|
* a PCD tool.
|
|
408
458
|
*/
|
|
409
459
|
readonly nonFerrous: boolean | null;
|
|
460
|
+
/**
|
|
461
|
+
* Taps only, and `null` on every other kind because the question does not
|
|
462
|
+
* apply — the shape {@link ToolRecord.nonFerrous} already keeps for drills.
|
|
463
|
+
*
|
|
464
|
+
* **Not a default, and not derivable from anything else on the record.** A
|
|
465
|
+
* former and a cut tap of the same size share their `DC`, `TP`, `SFDM`,
|
|
466
|
+
* `OAL` and `LCF`; what separates them is that one displaces material and the
|
|
467
|
+
* other removes it, which changes the hole a shop drills first and the feed
|
|
468
|
+
* it runs. Guessing `cutting` because most of a catalog is would ship a
|
|
469
|
+
* former with a cut tap's drill size.
|
|
470
|
+
*
|
|
471
|
+
* It is a per-family fact rather than a column, and each of the five
|
|
472
|
+
* declarations cites the vendor's own index — see `families/kennametal.ts`
|
|
473
|
+
* and `families/emuge.ts`. The invariant below is what stops a sixth tap
|
|
474
|
+
* family arriving without one.
|
|
475
|
+
*/
|
|
476
|
+
readonly threadMethod: ThreadMethod | null;
|
|
410
477
|
}
|
|
411
478
|
/**
|
|
412
479
|
* Build a {@link ToolRecord}: mint its guid, default the fields that have a
|
|
@@ -433,7 +500,7 @@ export interface ToolRecord {
|
|
|
433
500
|
* interchange value, and a mapper that mutated one would be reaching back
|
|
434
501
|
* across the seam this type exists to draw.
|
|
435
502
|
*/
|
|
436
|
-
export declare function toolRecord(fields: Omit<ToolRecord, 'guid' | 'materialGroups' | 'materialGroupsSource' | 'nonFerrous' | 'productLine'> & Partial<Pick<ToolRecord, 'materialGroups' | 'materialGroupsSource' | 'nonFerrous' | 'productLine'>>): ToolRecord;
|
|
503
|
+
export declare function toolRecord(fields: Omit<ToolRecord, 'guid' | 'materialGroups' | 'materialGroupsSource' | 'nonFerrous' | 'productLine' | 'threadMethod'> & Partial<Pick<ToolRecord, 'materialGroups' | 'materialGroupsSource' | 'nonFerrous' | 'productLine' | 'threadMethod'>>): ToolRecord;
|
|
437
504
|
/**
|
|
438
505
|
* A family's canonical-field → CSV-column-label mapping, validated.
|
|
439
506
|
*
|
package/dist/records.js
CHANGED
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
* always a copy is not a second measurement. An adapter that could supply them
|
|
43
43
|
* separately could supply a tool that claims a holder it does not have.
|
|
44
44
|
*/
|
|
45
|
+
import { GEOMETRY_FIELDS as DICTIONARY, } from '@toolpath/tool-support';
|
|
45
46
|
import { dimensionalColumn } from './conventions.js';
|
|
46
47
|
import { ScraperConfigError } from './errors.js';
|
|
47
48
|
import { recordGuid } from './identity.js';
|
|
@@ -81,6 +82,26 @@ export const UNSPECIFIED = 'unspecified';
|
|
|
81
82
|
* so the vocabulary's source is readable from the code rather than from a plan
|
|
82
83
|
* document.
|
|
83
84
|
*
|
|
85
|
+
* ## What each entry says, and what this table says
|
|
86
|
+
*
|
|
87
|
+
* The definitions and ISO codes are `@toolpath/tool-support`'s — that is the
|
|
88
|
+
* dictionary, and it is shared because a code has to mean one thing in every
|
|
89
|
+
* package that reads one. **This is not that dictionary.** It is the narrower
|
|
90
|
+
* question only a scraper asks: which of those names an *adapter may map a
|
|
91
|
+
* vendor column to*.
|
|
92
|
+
*
|
|
93
|
+
* The two are not the same list and must not become one. The dictionary knows
|
|
94
|
+
* `LBH` and `LD`, which are derived from a tool and a holder downstream and
|
|
95
|
+
* which no vendor publishes; an adapter permitted to map a column to `LBH`
|
|
96
|
+
* could supply a tool that claims a stickout nobody set. It also knows `LSCN`,
|
|
97
|
+
* ISO's clamping-length minimum, which is a real vendor column the day a vendor
|
|
98
|
+
* prints one — and which is deliberately absent below until that day, because
|
|
99
|
+
* the load-time check's job is to refuse a name no scrape can fill.
|
|
100
|
+
*
|
|
101
|
+
* So each entry is an explicit pick out of the shared table rather than a
|
|
102
|
+
* spread of it: a name dropped from the dictionary is a compile error here, and
|
|
103
|
+
* a name added to the dictionary does not silently become mappable.
|
|
104
|
+
*
|
|
84
105
|
* **Seven of the ten are the standard's codes with the standard's meanings.**
|
|
85
106
|
* The three that are not are Autodesk's, and each has an ISO counterpart
|
|
86
107
|
* Autodesk did not use:
|
|
@@ -98,25 +119,16 @@ export const UNSPECIFIED = 'unspecified';
|
|
|
98
119
|
* recognises it.
|
|
99
120
|
*/
|
|
100
121
|
export const GEOMETRY_FIELDS = {
|
|
101
|
-
DC:
|
|
102
|
-
SFDM:
|
|
103
|
-
OAL:
|
|
104
|
-
LCF:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
SIG: { definition: 'point angle, degrees included', iso: 'SIG' },
|
|
112
|
-
'shoulder-length': {
|
|
113
|
-
definition: 'usable length below the full shank',
|
|
114
|
-
iso: null,
|
|
115
|
-
},
|
|
116
|
-
'shoulder-diameter': {
|
|
117
|
-
definition: 'diameter at the shoulder — the neck, where necked',
|
|
118
|
-
iso: null,
|
|
119
|
-
},
|
|
122
|
+
DC: DICTIONARY.DC,
|
|
123
|
+
SFDM: DICTIONARY.SFDM,
|
|
124
|
+
OAL: DICTIONARY.OAL,
|
|
125
|
+
LCF: DICTIONARY.LCF,
|
|
126
|
+
RE: DICTIONARY.RE,
|
|
127
|
+
TP: DICTIONARY.TP,
|
|
128
|
+
NOF: DICTIONARY.NOF,
|
|
129
|
+
SIG: DICTIONARY.SIG,
|
|
130
|
+
'shoulder-length': DICTIONARY['shoulder-length'],
|
|
131
|
+
'shoulder-diameter': DICTIONARY['shoulder-diameter'],
|
|
120
132
|
};
|
|
121
133
|
/**
|
|
122
134
|
* The canonical names that are **not** ISO 13399's own codes, derived rather
|
|
@@ -303,9 +315,21 @@ export function toolRecord(fields) {
|
|
|
303
315
|
`and materialGroupsSource is ${JSON.stringify(source)} — ` +
|
|
304
316
|
`groups are ${JSON.stringify(UNSPECIFIED)} exactly when there are none`);
|
|
305
317
|
}
|
|
318
|
+
// A tap says how it makes its thread and nothing else does. Both halves are
|
|
319
|
+
// load-bearing: a tap record with no method is a family that never declared
|
|
320
|
+
// the fact, and a method on a drill is a mapper that copied a line from the
|
|
321
|
+
// tap one. Neither can be recovered downstream from what is left on the
|
|
322
|
+
// record, because the geometry of a former and a cut tap is the same
|
|
323
|
+
// geometry.
|
|
324
|
+
const method = fields.threadMethod ?? null;
|
|
325
|
+
if ((fields.kind === 'tap') !== (method !== null)) {
|
|
326
|
+
throw new ScraperConfigError(fields.materialNumber, `a ${fields.kind} record states threadMethod ${JSON.stringify(method)} — ` +
|
|
327
|
+
`a tap says how it makes its thread and no other kind does`);
|
|
328
|
+
}
|
|
306
329
|
checkGeometry(fields.kind, fields.materialNumber, fields.geometry);
|
|
307
330
|
return Object.freeze({
|
|
308
331
|
...fields,
|
|
332
|
+
threadMethod: method,
|
|
309
333
|
guid: recordGuid(fields.brand, fields.materialNumber),
|
|
310
334
|
geometry: Object.freeze({ ...fields.geometry }),
|
|
311
335
|
materialGroups: groups === null ? null : Object.freeze([...groups]),
|
|
@@ -155,6 +155,16 @@ export declare const PRODUCT_LINE_COLUMNS: Readonly<Record<string, string>>;
|
|
|
155
155
|
* vendor's marketing rather than a hole here: `SPEED`, `FK`, `GAL`, `GG` and
|
|
156
156
|
* `TILEG` are real lines with no `/a/` page on the US storefront, so the
|
|
157
157
|
* honest answer is the vendor's own code until one appears.
|
|
158
|
+
*
|
|
159
|
+
* **`FG02` has no entry at all, on purpose.** Cold-forming taps index by the
|
|
160
|
+
* same eight geometry codes `FG01` uses — `AL`, `GAL`, `H`, `MULTI`, `SPEED`,
|
|
161
|
+
* `STEEL`, `VA`, `Z` — and mean different products by them: a `Z`-geometry
|
|
162
|
+
* former is InnoForm, not the `Rekord B-Z Taps` the `FG01` table would name it.
|
|
163
|
+
* Borrowing that table would put a cutting tap's product line on a forming tap,
|
|
164
|
+
* which is the one error this category split exists to make impossible. So the
|
|
165
|
+
* codes pass through verbatim, which is what the paragraph above already says a
|
|
166
|
+
* code with no article page does, and naming them from the vendor's own
|
|
167
|
+
* cold-forming pages is the follow-up.
|
|
158
168
|
*/
|
|
159
169
|
export declare const PRODUCT_LINES: Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
160
170
|
/**
|
|
@@ -172,6 +172,7 @@ export const PRODUCT_LINE_COLUMNS = {
|
|
|
172
172
|
FF01: 'product line',
|
|
173
173
|
FB01: 'Geometry',
|
|
174
174
|
FG01: 'Geometry',
|
|
175
|
+
FG02: 'Geometry',
|
|
175
176
|
};
|
|
176
177
|
/**
|
|
177
178
|
* A category's product-line codes onto the vendor's own name for each.
|
|
@@ -194,6 +195,16 @@ export const PRODUCT_LINE_COLUMNS = {
|
|
|
194
195
|
* vendor's marketing rather than a hole here: `SPEED`, `FK`, `GAL`, `GG` and
|
|
195
196
|
* `TILEG` are real lines with no `/a/` page on the US storefront, so the
|
|
196
197
|
* honest answer is the vendor's own code until one appears.
|
|
198
|
+
*
|
|
199
|
+
* **`FG02` has no entry at all, on purpose.** Cold-forming taps index by the
|
|
200
|
+
* same eight geometry codes `FG01` uses — `AL`, `GAL`, `H`, `MULTI`, `SPEED`,
|
|
201
|
+
* `STEEL`, `VA`, `Z` — and mean different products by them: a `Z`-geometry
|
|
202
|
+
* former is InnoForm, not the `Rekord B-Z Taps` the `FG01` table would name it.
|
|
203
|
+
* Borrowing that table would put a cutting tap's product line on a forming tap,
|
|
204
|
+
* which is the one error this category split exists to make impossible. So the
|
|
205
|
+
* codes pass through verbatim, which is what the paragraph above already says a
|
|
206
|
+
* code with no article page does, and naming them from the vendor's own
|
|
207
|
+
* cold-forming pages is the follow-up.
|
|
197
208
|
*/
|
|
198
209
|
export const PRODUCT_LINES = {
|
|
199
210
|
// `/us/en/multi-drill/a/MultiDRILL`, `/us/en/steeldrill/a/SteelDrill`,
|
|
@@ -527,6 +538,7 @@ export function tapRecord(row, family, columns, options = {}) {
|
|
|
527
538
|
...common(row, family, what, warn),
|
|
528
539
|
kind: 'tap',
|
|
529
540
|
unit,
|
|
541
|
+
threadMethod: fact(family, 'threadMethod', family.threadMethod),
|
|
530
542
|
geometry: {
|
|
531
543
|
DC: required(row, columns, 'DC', unit, what, opts),
|
|
532
544
|
TP: required(row, columns, 'TP', unit, what, opts),
|
|
@@ -247,6 +247,7 @@ export function tapRecord(row, family, columns) {
|
|
|
247
247
|
coating: row[COATING] ?? '',
|
|
248
248
|
...materialGroups(row),
|
|
249
249
|
coolantThrough: fact(family, 'coolantThrough', family.coolantThrough),
|
|
250
|
+
threadMethod: fact(family, 'threadMethod', family.threadMethod),
|
|
250
251
|
geometry: {
|
|
251
252
|
DC: threadMajorDiameter(tdz, system),
|
|
252
253
|
TP: require_(row, columns, 'TP', unit, what),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toolpath/tool-scraper",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Scrape cutting-tool geometry from vendor catalogs into records and CSVs",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"engines": {
|
|
@@ -82,7 +82,8 @@
|
|
|
82
82
|
"README.md"
|
|
83
83
|
],
|
|
84
84
|
"dependencies": {
|
|
85
|
-
"htmlparser2": "12.0.0"
|
|
85
|
+
"htmlparser2": "12.0.0",
|
|
86
|
+
"@toolpath/tool-support": "^0.2.0"
|
|
86
87
|
},
|
|
87
88
|
"devDependencies": {
|
|
88
89
|
"@types/node": "24.10.1",
|