@toolpath/tool-scraper 0.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +41 -11
  2. package/dist/columns.d.ts +62 -0
  3. package/dist/columns.js +62 -0
  4. package/dist/conventions.d.ts +89 -15
  5. package/dist/conventions.js +108 -15
  6. package/dist/families/emuge.d.ts +183 -0
  7. package/dist/families/emuge.js +159 -0
  8. package/dist/families/harvey.d.ts +1782 -0
  9. package/dist/families/harvey.js +1328 -0
  10. package/dist/families/index.js +5 -2
  11. package/dist/families/kennametal.d.ts +21 -0
  12. package/dist/families/kennametal.js +10 -0
  13. package/dist/families/maritool.d.ts +120 -0
  14. package/dist/families/maritool.js +175 -0
  15. package/dist/family.d.ts +35 -1
  16. package/dist/family.js +28 -0
  17. package/dist/identity.d.ts +18 -0
  18. package/dist/identity.js +46 -0
  19. package/dist/index.d.ts +16 -0
  20. package/dist/index.js +16 -0
  21. package/dist/measure.d.ts +112 -0
  22. package/dist/measure.js +130 -0
  23. package/dist/node/cli.d.ts +3 -0
  24. package/dist/node/cli.js +152 -2
  25. package/dist/node/main.js +0 -0
  26. package/dist/records.d.ts +169 -12
  27. package/dist/records.js +116 -5
  28. package/dist/registry.d.ts +31 -1
  29. package/dist/registry.js +40 -1
  30. package/dist/scrape.d.ts +15 -0
  31. package/dist/scrape.js +24 -0
  32. package/dist/vendors/destinytool/records.d.ts +13 -3
  33. package/dist/vendors/destinytool/records.js +41 -37
  34. package/dist/vendors/emuge/index.d.ts +17 -0
  35. package/dist/vendors/emuge/index.js +17 -0
  36. package/dist/vendors/emuge/records.d.ts +150 -0
  37. package/dist/vendors/emuge/records.js +375 -0
  38. package/dist/vendors/emuge/scrape.d.ts +227 -0
  39. package/dist/vendors/emuge/scrape.js +358 -0
  40. package/dist/vendors/emuge/value.d.ts +75 -0
  41. package/dist/vendors/emuge/value.js +116 -0
  42. package/dist/vendors/harvey/catalog.d.ts +53 -0
  43. package/dist/vendors/harvey/catalog.js +120 -0
  44. package/dist/vendors/harvey/header.d.ts +89 -0
  45. package/dist/vendors/harvey/header.js +185 -0
  46. package/dist/vendors/harvey/index.d.ts +21 -0
  47. package/dist/vendors/harvey/index.js +21 -0
  48. package/dist/vendors/harvey/lexicon.d.ts +73 -0
  49. package/dist/vendors/harvey/lexicon.js +126 -0
  50. package/dist/vendors/harvey/literal.d.ts +68 -0
  51. package/dist/vendors/harvey/literal.js +214 -0
  52. package/dist/vendors/harvey/records.d.ts +79 -0
  53. package/dist/vendors/harvey/records.js +163 -0
  54. package/dist/vendors/harvey/scrape.d.ts +187 -0
  55. package/dist/vendors/harvey/scrape.js +483 -0
  56. package/dist/vendors/harvey/value.d.ts +74 -0
  57. package/dist/vendors/harvey/value.js +119 -0
  58. package/dist/vendors/kennametal/records.d.ts +13 -4
  59. package/dist/vendors/kennametal/records.js +61 -26
  60. package/dist/vendors/maritool/catalog.d.ts +81 -0
  61. package/dist/vendors/maritool/catalog.js +132 -0
  62. package/dist/vendors/maritool/index.d.ts +10 -0
  63. package/dist/vendors/maritool/index.js +10 -0
  64. package/dist/vendors/maritool/scrape.d.ts +297 -0
  65. package/dist/vendors/maritool/scrape.js +593 -0
  66. package/dist/vendors/regofix/scrape.d.ts +8 -11
  67. package/dist/vendors/regofix/scrape.js +21 -36
  68. package/package.json +31 -7
@@ -0,0 +1,297 @@
1
+ /**
2
+ * MariTool category listings -> toolholding rows.
3
+ *
4
+ * The fifth vendor and the first whose transport needed no discovery at all:
5
+ * `www.maritool.com` is an osCommerce-family storefront that server-renders
6
+ * everything, so the whole transport is a paced `GET`. There is no JSON API,
7
+ * no sitemap and no application bundle to read — see
8
+ * `docs/MARITOOL_CATALOG.md` for what was tried and what the tree looks like.
9
+ *
10
+ * ## Two sources, and both are needed
11
+ *
12
+ * **The roster** is a leaf category's listing page. Every row carries the
13
+ * part number (`Part#:`), the store's `products_id`, the product name and the
14
+ * CAD assets, and the page states its own row total — `(of 51 products)` —
15
+ * which is the second opinion `receipts.checkRows` gets for free from a
16
+ * hand-counted `rows` and which nothing here computes from the rows it just
17
+ * collected.
18
+ *
19
+ * **The geometry** is a `Product Specifications` name/value table on each
20
+ * part's own page, so this is one request per part. The table's labels are
21
+ * MariTool's own and are carried into the CSV verbatim, which is the standing
22
+ * rule; the header is their union in first-seen order, because which keys a
23
+ * part publishes is a function of its style. A shrink-fit holder states
24
+ * `Shank Size` and `Nose Diameter`, a collet chuck states `Collet Size` and
25
+ * `Collet Grip Range`, and a hydraulic chuck states `Hydraulic Type`.
26
+ *
27
+ * ## What the vendor gets wrong
28
+ *
29
+ * Four faults found on 2026-08-29, all reported as warnings rather than
30
+ * fixed — two disagreeing vendor cells cannot say which one is wrong, and a
31
+ * scraper that corrects one becomes a place tool data is authored by hand.
32
+ * This is the same call `vendors/regofix/scrape.ts` made on its three.
33
+ *
34
+ * 1. **`BT40-ER32-60` publishes no `Taper` row at all**, alone among the 529
35
+ * parts in scope. Its row is kept with `taper` and `contact` empty rather
36
+ * than dropped or filled in from its part number: the CSV is a receipt, and
37
+ * the hole is what the vendor published.
38
+ * 2. **`Collet Size` carries a collet *nut* designation on two parts.**
39
+ * `CAT40-ER25-3.0MD` and `BT30-ER25-60M` both state `ER25M`, and `ER25M` is
40
+ * not a collet series — `HSK40E-ER16-3.0M` puts exactly that shape of value
41
+ * in its own `Collet Nut` cell, which is the column it belongs in. The
42
+ * string is written into `CST` as designated, so it joins to no collet
43
+ * family, and it is warned about. Widening it to `ER25` would offer a
44
+ * machinist a collet that may not seat.
45
+ * 3. **`Collet Size` is spaced inconsistently within one style** — `ER 11`
46
+ * and `ER11` are both published. {@link colletSeries} closes the space,
47
+ * because `CST` is a join key and two spellings of one series join to
48
+ * nothing.
49
+ * 4. **Two parts publish no `Product Specifications` table**, both in the
50
+ * CAT50 `Collet Chucks` leaf. They state their geometry as English prose in
51
+ * a `Product Info` bullet list instead — *"Gage length is 100mm (3.93
52
+ * inch)"* — and that is a sentence, not a table. They are skipped with a
53
+ * named warning; a regex over the prose would be this package authoring
54
+ * tool data by hand.
55
+ *
56
+ * ## Where the units are
57
+ *
58
+ * `Gage Length` is metric on some parts and imperial on others **inside one
59
+ * CSV, and inside one category page**: `HSK40E-ER11-40` gages `40mm` where
60
+ * `HSK40E-ER16-3.0M` gages `3.0` inches, and both are rows of
61
+ * `c23_46_1552_1558`. So no MariTool family declares a `unit`, and the gage
62
+ * length is promoted into an `L1_in`/`L1_mm` pair with exactly one cell filled
63
+ * — the shape `regofix.colletRow` already gives `Square_in`/`Square_mm`.
64
+ *
65
+ * **Nothing is converted.** The vendor's own imperial conversion is unusable
66
+ * (its prose calls 40 mm "1.6 inches" where the figure is 1.5748), and
67
+ * computing one here would put a number in the file that the vendor never
68
+ * published. The raw `Gage Length` cell is kept beside the pair.
69
+ *
70
+ * Every other measured cell — `Shank Size`, `Nose Diameter`, `Collet Grip
71
+ * Range` — stays verbatim under MariTool's own label, unsuffixed and
72
+ * unpromoted. That is deliberate: `conventions.dimensionalColumn` takes its
73
+ * suffix from a family's declared `unit`, and these families have none to give
74
+ * it. An adapter choosing `_in` for those cells itself is exactly the mistake
75
+ * a declared `unit` exists to prevent, and `HSK40E-SF.125-45` is the row that
76
+ * proves it — gaged in millimetres, bored in inches.
77
+ */
78
+ import { type UnitSystem } from '../../conventions.js';
79
+ import type { Fetcher } from '../../fetch.js';
80
+ import { type ScrapeResult, type ScrapedRow, type Warn } from '../../scrape.js';
81
+ export declare const BASE = "https://www.maritool.com";
82
+ /**
83
+ * The CSV columns this adapter builds rather than lifts from a spec table.
84
+ *
85
+ * `products_id` is the store's internal id and an ordinary column: a
86
+ * re-created product would change it, and every guid minted off it with it —
87
+ * which is why identity is the part number. It is carried anyway because it is
88
+ * what a re-fetch of one part is addressed by.
89
+ */
90
+ export declare const MATERIAL_COLUMN = "Material Number";
91
+ export declare const STORE_ID_COLUMN = "products_id";
92
+ export declare const TAPER_COLUMN = "taper";
93
+ export declare const CLAMPING_COLUMN = "clamping";
94
+ export declare const STYLE_COLUMN = "style";
95
+ /** MariTool's own label for the cell `conventions.GAGE_COLUMNS` is promoted from. */
96
+ export declare const GAGE_LABEL = "Gage Length";
97
+ /** MariTool's own label for the cell {@link COLLET_SERIES_COLUMN} comes from. */
98
+ export declare const COLLET_SIZE_LABEL = "Collet Size";
99
+ /** MariTool's own label for the cell {@link TAPER_FORMS} is looked up by. */
100
+ export declare const TAPER_LABEL = "Taper";
101
+ /**
102
+ * Every `Taper` cell this package is willing to read, and what it means.
103
+ *
104
+ * A closed table rather than a pattern, and keyed on the cell upper-cased
105
+ * because MariTool publishes the dual-contact suffix in two casings
106
+ * (`CAT50 DUAL CONTACT` and `CAT50 Dual Contact` are both in the catalog). An
107
+ * unlisted value throws naming itself: a spindle interface this package cannot
108
+ * name is a guess about which machine a holder fits, which is the call
109
+ * `regofix.CONTACT_BY_FORM` made on `BT-OM 30`.
110
+ *
111
+ * **`contact` is `face` on every HSK size, and MariTool does not say so.** It
112
+ * is the interface's definition rather than a property of the part: an HSK
113
+ * shank is a hollow taper that seats on the flange face at the same time as
114
+ * the cone, which is what DIN 69893 / ISO 12164 specify and what the `A`, `E`
115
+ * and `F` forms vary the flange of rather than the contact. MariTool marks
116
+ * `DUAL CONTACT` only on its 7/24 tapers, where it is an option, and there is
117
+ * nothing to mark on an HSK because there is no single-contact HSK to
118
+ * distinguish it from.
119
+ */
120
+ export declare const TAPER_FORMS: Record<string, {
121
+ taper: string;
122
+ contact: string;
123
+ }>;
124
+ /**
125
+ * One leaf category to scrape, and how MariTool classifies what is in it.
126
+ *
127
+ * **`clamping` and `style` come from the leaf, not from a family constant.**
128
+ * Each of the five CSVs is one taper and mixes all three holder styles, so
129
+ * neither can be a family fact; and the leaf name is MariTool's own
130
+ * classification of the part, so the column is vendor-stated rather than a
131
+ * coinage. The values live in `families/maritool.ts` beside the cPath they
132
+ * come from.
133
+ *
134
+ * Declared here and re-declared structurally by the config table rather than
135
+ * imported from it, because `families/` must not import an adapter and an
136
+ * adapter must not import the config — `tests/vendor-boundary.test.ts` refuses
137
+ * both directions.
138
+ */
139
+ export interface LeafTarget {
140
+ /** MariTool's own category path, e.g. `23_25_42`. */
141
+ readonly cPath: string;
142
+ /** How a holder in this leaf grips: `collet`, `shrink`, `hydraulic`. */
143
+ readonly clamping: string;
144
+ /** The product style, as MariTool names the leaf: `er-collet-chuck`. */
145
+ readonly style: string;
146
+ }
147
+ /** One row of a leaf's listing, before its product page has been read. */
148
+ export interface ListingRow {
149
+ /** The store's internal id, from the product link. */
150
+ readonly productsId: string;
151
+ /** The `Part#:` line — the identity, and the guid seed. */
152
+ readonly partNumber: string;
153
+ /** The vendor's own product name, which is where a nose form survives. */
154
+ readonly name: string;
155
+ /** The vendor's own link to this part's page. */
156
+ readonly productUrl: string;
157
+ /** Downloadable assets by their own code — `STP`, `DXF`, `DWG`, `PDF`. */
158
+ readonly assets: Readonly<Record<string, string>>;
159
+ }
160
+ /** One listing page: the vendor's row total, and the rows it rendered. */
161
+ export interface Listing {
162
+ /** What `(of N products)` said. The whole leaf, not this page. */
163
+ readonly total: number;
164
+ readonly rows: ListingRow[];
165
+ }
166
+ /** One product page: its spec table, and what its downloads header calls it. */
167
+ export interface Product {
168
+ /** MariTool's own labels to its own values. Empty where none is published. */
169
+ readonly specs: Record<string, string>;
170
+ /**
171
+ * The part number the downloads header restates, or null where the part
172
+ * publishes no assets and so carries no header.
173
+ */
174
+ readonly statedPartNumber: string | null;
175
+ }
176
+ /**
177
+ * A leaf category's listing page.
178
+ *
179
+ * The slug in a rewritten URL is ignored by the store, so this uses the
180
+ * platform's own unrewritten form and invents no slug: three URL forms reach
181
+ * the same page and only this one is derivable from a cPath alone.
182
+ */
183
+ export declare function categoryUrl(cPath: string, page?: number): string;
184
+ /**
185
+ * One listing page's roster row total and its rendered rows.
186
+ *
187
+ * The rows are read off `<tr class="product-info">`, which is the vendor's own
188
+ * marker for one, and the store id off the product link rather than off the
189
+ * add-to-cart form's hidden input: the link is the first thing in the row and
190
+ * is also what the next request needs.
191
+ */
192
+ export declare function parseListing(html: string): Listing;
193
+ /** Options every MariTool scrape accepts. */
194
+ export interface MaritoolOptions {
195
+ warn?: Warn;
196
+ /** Milliseconds between requests. Zero in a test, {@link REQUEST_DELAY_MS} live. */
197
+ delayMs?: number;
198
+ }
199
+ /**
200
+ * Every part in one leaf category, paged until the vendor's own count is met.
201
+ *
202
+ * **The count is the gate, and it is the vendor's rather than ours.** A roster
203
+ * that stopped a page early is the failure this package is built to notice,
204
+ * and it cannot be noticed by counting the rows that were collected — that
205
+ * number agrees with itself. `(of N products)` is a second opinion the store
206
+ * computes from its own database, which is the check `regofix.search` gets
207
+ * free from `hits.total`.
208
+ *
209
+ * A page that adds no new row stops the walk rather than looping: a paging
210
+ * parameter the store has stopped honouring answers with page 1 forever.
211
+ */
212
+ export declare function roster(fetcher: Fetcher, cPath: string, options?: MaritoolOptions): Promise<ListingRow[]>;
213
+ /**
214
+ * One product page's `Product Specifications` table and downloads header.
215
+ *
216
+ * Both are found by their own `<div class="header">` rather than positionally,
217
+ * because a page carries several such blocks and which ones are present varies
218
+ * — 2 of the 529 parts in scope publish no spec table and roughly one in four
219
+ * publishes no downloads.
220
+ *
221
+ * An empty `specs` is a real state and is not an error here: the caller is
222
+ * what decides that a part with no geometry is skipped rather than written
223
+ * with holes.
224
+ */
225
+ export declare function parseProduct(html: string): Product;
226
+ /** One part's page, fetched through the seam every transport here uses. */
227
+ export declare function fetchProduct(fetcher: Fetcher, url: string): Promise<Product>;
228
+ /** A gage length, read off the cell the vendor printed it in. */
229
+ export interface GageLength {
230
+ readonly value: number;
231
+ readonly unit: UnitSystem;
232
+ }
233
+ /**
234
+ * A `Gage Length` cell as a number and the unit system it is stated in.
235
+ *
236
+ * **A bare number is inches.** That is the vendor's convention rather than
237
+ * this package guessing: MariTool marks every metric cell `mm` and marks
238
+ * nothing on an imperial one — 363 of the 473 in-scope cells sampled are bare
239
+ * or carry an inch mark, and a gage length of "3.0" millimetres is not a
240
+ * holder. The part number says nothing about it either way: `HSK40E-ER11-40`
241
+ * is millimetres and `HSK40E-ER16-3.0M` is inches, where that `M` is a mini
242
+ * nut.
243
+ *
244
+ * **The nose form is parsed off and given no column.** `120mm Tapered` is 120
245
+ * millimetres, and `Tapered` is not a data type — it is not lost, because the
246
+ * vendor states it in the product name too (*"BT40 ER11 120mm Tapered Nose
247
+ * Collet Chuck Tool Holder"*), and the row carries that name verbatim in
248
+ * `Description`. The raw cell stays in the CSV as well.
249
+ *
250
+ * Refused rather than returned as `NaN`, the rule `regofix.parseSize` holds: a
251
+ * cell this cannot read would otherwise travel into a row as a gage length.
252
+ */
253
+ export declare function parseGageLength(cell: string): GageLength;
254
+ /**
255
+ * A `Collet Size` cell as the series `CST` joins a collet family on.
256
+ *
257
+ * Only the spacing is closed. MariTool publishes `ER 11` and `ER11` within one
258
+ * style, and two spellings of one series join to nothing — `CST` is the key
259
+ * `families/kennametal.ts` states the holder-to-collet join against, and the
260
+ * collet side of it spells the series without a space.
261
+ *
262
+ * Nothing else is normalised. `ER25M` is written through as designated even
263
+ * though no such collet series exists, because the alternative is to decide on
264
+ * the vendor's behalf that its `M` is the mini nut its `Collet Nut` column
265
+ * carries elsewhere — see this module's docstring. {@link holderRow} warns.
266
+ */
267
+ export declare function colletSeries(cell: string): string;
268
+ /**
269
+ * One listing row plus its spec table -> one CSV row.
270
+ *
271
+ * The vendor's own labels are carried verbatim and in the order the page
272
+ * states them; what this adds in front of them is the four axes the catalog
273
+ * asks a holder about — `taper`, `contact`, `clamping`, `style` — plus the
274
+ * collet series and the promoted gage length.
275
+ *
276
+ * **No bore column.** A collet chuck grips through a collet and a shrink-fit
277
+ * holder's bore is stated under MariTool's own `Shank Size` label; promoting
278
+ * either to `D1` would make a collet-clamping holder claim a clamping capacity
279
+ * of its own, which is the reason `regofix.holderRow` has no `D1` either. For
280
+ * the same reason `Collet Grip Range` is carried verbatim and never becomes
281
+ * `CCCN`/`CCCX`: it is the ER series' range restated on the holder's page, a
282
+ * pure function of `Collet Size` across every part sampled, and a real one
283
+ * comes from a collet family joined on `CST`.
284
+ */
285
+ export declare function holderRow(leaf: LeafTarget, listing: ListingRow, specs: Readonly<Record<string, string>>, warn?: Warn): ScrapedRow;
286
+ /**
287
+ * Every in-scope holder under `leaves`, one CSV's worth.
288
+ *
289
+ * One request per leaf page for the roster, then one per part for its
290
+ * geometry, paced by the package's shared politeness delay throughout.
291
+ *
292
+ * **Deduped by `products_id` across leaves.** MariTool lists a handful of
293
+ * parts under two leaves, and a part is one row however many places the store
294
+ * files it. The first leaf to reach it wins, which is what makes the row
295
+ * order below independent of which duplicate was found.
296
+ */
297
+ export declare function scrapeHolders(fetcher: Fetcher, leaves: readonly LeafTarget[], options?: MaritoolOptions): Promise<ScrapeResult>;