@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
@@ -23,7 +23,8 @@
23
23
  * row.
24
24
  */
25
25
  import { VendorResponseError } from '../../errors.js';
26
- import { familyBrand } from '../../family.js';
26
+ import { fractionValue } from '../../measure.js';
27
+ import { fact, familyBrand } from '../../family.js';
27
28
  import { BRANDS } from '../../identity.js';
28
29
  import { ISO_MATERIAL_GROUPS, toolRecord, } from '../../records.js';
29
30
  import { consoleWarn } from '../../scrape.js';
@@ -86,31 +87,18 @@ export function parseFractionInches(text) {
86
87
  const s = text.trim().replace(/"+$/, '');
87
88
  if (!s)
88
89
  throw new RangeError(`empty dimension: ${JSON.stringify(text)}`);
89
- const value = s.includes('.')
90
- ? Number(s)
91
- : s.includes('-')
92
- ? mixed(s)
93
- : s.includes('/')
94
- ? fraction(s)
95
- : Number(s);
96
- if (!Number.isFinite(value)) {
90
+ // The trailing quote is the only thing Destiny Tool puts around a dimension
91
+ // that `measure.fractionValue` does not read; the grammar itself is the one
92
+ // every vendor here publishes, and it is read in one place.
93
+ const value = fractionValue(s);
94
+ if (value === null) {
97
95
  throw new RangeError(`unrecognized dimension: ${JSON.stringify(text)}`);
98
96
  }
99
97
  return value;
100
98
  }
101
- /** `1-1/2` — a whole number and a simple fraction. */
102
- function mixed(s) {
103
- const cut = s.indexOf('-');
104
- return Number(s.slice(0, cut)) + fraction(s.slice(cut + 1));
105
- }
106
- /** `3/4`. */
107
- function fraction(s) {
108
- const [num, den] = s.split('/');
109
- return Number(num) / Number(den);
110
- }
111
99
  /** A dimension the kind requires, parsed as an inch fraction. */
112
- function required(row, columns, canonical, what) {
113
- const column = columns.column(canonical, 'inches');
100
+ function required(row, columns, canonical, unit, what) {
101
+ const column = columns.column(canonical, unit);
114
102
  const raw = column === null ? undefined : row[column];
115
103
  if (raw === undefined || raw.trim() === '') {
116
104
  throw new VendorResponseError(what, `no value for ${canonical} in column ${JSON.stringify(column)}`);
@@ -190,6 +178,13 @@ export function shoulderDiameter(description, dc) {
190
178
  * column when populated, or a fallback keyed on flute count when it is not
191
179
  * (blank on 423 of 3,898 rows, 2026-08-19).
192
180
  *
181
+ * **The two answers are labelled, not blended.** A populated cell is
182
+ * `vendor-stated` and the flute-count fallback is `derived`, so a consumer that
183
+ * will not route a cut off this package's arithmetic can filter on the source
184
+ * rather than having to know which rows Destiny Tool left blank. Neither is
185
+ * ever `null`: the fallback covers every blank cell, so this vendor always has
186
+ * an answer.
187
+ *
193
188
  * The fallback is not a new rule invented for this vendor — it is the split
194
189
  * cutting-data presets are routed by downstream (≤3 flutes non-ferrous, >3
195
190
  * ferrous), applied here to the material-groups facet instead. Real vendor
@@ -209,23 +204,31 @@ export function materialGroups(row, flutes) {
209
204
  const cell = row['isoMaterialGroups'] ?? '';
210
205
  if (cell.trim()) {
211
206
  const present = new Set(cell.split(/\s+/).filter(Boolean));
212
- return ISO_MATERIAL_GROUPS.filter((group) => present.has(group));
207
+ return {
208
+ materialGroups: ISO_MATERIAL_GROUPS.filter((group) => present.has(group)),
209
+ materialGroupsSource: 'vendor-stated',
210
+ };
213
211
  }
214
- if (flutes <= NON_FERROUS_MAX_FLUTES)
215
- return ['N'];
216
- return ['P', 'M', 'K', 'S', 'H'];
212
+ return {
213
+ materialGroups: flutes <= NON_FERROUS_MAX_FLUTES ? ['N'] : ['P', 'M', 'K', 'S', 'H'],
214
+ materialGroupsSource: 'derived',
215
+ };
217
216
  }
218
217
  /**
219
- * A solid end mill, always in inches Destiny Tool publishes no metric line
220
- * (the `unit` fact on the family).
218
+ * A solid end mill, in the family's declared unit.
219
+ *
220
+ * Which is inches on the one family there is — Destiny Tool publishes no metric
221
+ * line — but read from the `unit` fact rather than hardcoded, so the fact stays
222
+ * the single authored copy the way it is for every other vendor here.
221
223
  */
222
224
  export function endmillRecord(row, family, columns, options = {}) {
225
+ const unit = fact(family, 'unit', family.unit);
223
226
  const what = row[ITEM_NUMBER] ?? '';
224
227
  const description = row['description'] ?? '';
225
- const dc = required(row, columns, 'DC', what);
226
- const fluteLength = required(row, columns, 'LCF', what);
227
- const oal = required(row, columns, 'OAL', what);
228
- const radColumn = columns.column('RE', 'inches');
228
+ const dc = required(row, columns, 'DC', unit, what);
229
+ const fluteLength = required(row, columns, 'LCF', unit, what);
230
+ const oal = required(row, columns, 'OAL', unit, what);
231
+ const radColumn = columns.column('RE', unit);
229
232
  const radRaw = radColumn === null ? undefined : row[radColumn];
230
233
  const radCell = radRaw && radRaw.trim() ? parseFractionInches(radRaw) : null;
231
234
  // Refused rather than allowed through as NaN: `materialGroups` reads it, and
@@ -237,17 +240,18 @@ export function endmillRecord(row, family, columns, options = {}) {
237
240
  throw new VendorResponseError(what, `no integer in column "flutes"`);
238
241
  }
239
242
  return toolRecord({
243
+ brand: familyBrand(family),
240
244
  vendor: BRANDS[familyBrand(family)].vendor,
241
245
  materialNumber: what,
242
246
  catalogNumber: what,
243
247
  description,
244
248
  kind: 'endmill',
245
- unit: 'inches',
246
- substrate: (row['material'] || (family.bmc ?? '')).toLowerCase(),
247
- // No carbide grade is published; the coating id fills GRADE instead.
248
- grade: row['coatingId'] ?? '',
249
- materialGroups: materialGroups(row, flutes),
250
- coolantThrough: family.coolantThrough ?? false,
249
+ unit,
250
+ substrate: (row['material'] || fact(family, 'bmc', family.bmc)).toLowerCase(),
251
+ // No carbide grade is published; the coating id is what there is.
252
+ coating: row['coatingId'] ?? '',
253
+ ...materialGroups(row, flutes),
254
+ coolantThrough: fact(family, 'coolantThrough', family.coolantThrough),
251
255
  geometry: {
252
256
  DC: dc,
253
257
  RE: cornerRadius(description, row['endStyle'] ?? '', what, dc, radCell,
@@ -0,0 +1,17 @@
1
+ /**
2
+ * EMUGE-FRANKEN — end mills, twist drills and taps, from a SAP Commerce API.
3
+ *
4
+ * Not a catalog to parse: the storefront renders its listings in the browser,
5
+ * so the "scrape" is a JSON client against the same unauthenticated API the
6
+ * site's own Vue front end reads. See `scrape.ts` for the three calls it takes
7
+ * and why, `value.ts` for the grammar of a vendor value string, and
8
+ * `records.ts` for what the vendor states that no other vendor here does — a
9
+ * point angle per drill, an ISO 513 rating per part — and the one thing it
10
+ * states nowhere at all, which is a tap's flute count.
11
+ *
12
+ * `value.ts` is pure and holds the parsing risk; `scrape.ts` is the only module
13
+ * that reads through a `Fetcher`.
14
+ */
15
+ export * from './records.js';
16
+ export * from './scrape.js';
17
+ export * from './value.js';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * EMUGE-FRANKEN — end mills, twist drills and taps, from a SAP Commerce API.
3
+ *
4
+ * Not a catalog to parse: the storefront renders its listings in the browser,
5
+ * so the "scrape" is a JSON client against the same unauthenticated API the
6
+ * site's own Vue front end reads. See `scrape.ts` for the three calls it takes
7
+ * and why, `value.ts` for the grammar of a vendor value string, and
8
+ * `records.ts` for what the vendor states that no other vendor here does — a
9
+ * point angle per drill, an ISO 513 rating per part — and the one thing it
10
+ * states nowhere at all, which is a tap's flute count.
11
+ *
12
+ * `value.ts` is pure and holds the parsing risk; `scrape.ts` is the only module
13
+ * that reads through a `Fetcher`.
14
+ */
15
+ export * from './records.js';
16
+ export * from './scrape.js';
17
+ export * from './value.js';
@@ -0,0 +1,150 @@
1
+ /**
2
+ * EMUGE-FRANKEN rows -> {@link ToolRecord}.
3
+ *
4
+ * The CSV a scrape writes holds the vendor's own value strings — `1 1/2 "`,
5
+ * `3 mm`, `140 deg`, `4` — rather than parsed numbers, because the file is the
6
+ * receipt and EMUGE's fractional inches and stated units are part of what it
7
+ * published. So this module is where a cell becomes a number, through
8
+ * `value.ts`. The closest precedent is Harvey Tool's, not Kennametal's, whose
9
+ * columns are already decimals.
10
+ *
11
+ * ## What EMUGE publishes that nobody else here does
12
+ *
13
+ * - **Both identity columns.** The 18-digit SAP material number and the catalog
14
+ * article code, per part, so there is no `conventions.IDENTITY_DEVIATIONS`
15
+ * entry — the first vendor since Kennametal that needs none.
16
+ * - **A point angle, per drill.** Kennametal's drill families assume theirs; the
17
+ * detail record states it, so `SIG` is a mapped column here and no fact.
18
+ * - **A per-part ISO 513 index.** `applicationMaterials` returns the vendor's
19
+ * own P/M/K/N/S/H rating for each part, which fills `materialGroups` as
20
+ * `vendor-stated`.
21
+ * - **A description for every part.** The grouped product's `productListInfo`
22
+ * is one sentence of the vendor's own prose per product line, so every kind
23
+ * here fills `description` from `conventions.DESCRIPTION_COLUMN` — including
24
+ * the tap, where Kennametal has to fall back to the thread designation
25
+ * because it publishes no such text. EMUGE's designation is on the CSV as
26
+ * `dimensionFeatureValue` and its thread is on the record as `TP`.
27
+ *
28
+ * ## What it does not publish
29
+ *
30
+ * **No flute count on a drill or a tap.** Not on the grouped product, the
31
+ * variant listing, the per-part detail or any facet. A drill's comes from the
32
+ * family's `flutes` fact — all seventeen drill groups state
33
+ * `Specification: Twist drill` — and a tap's is simply absent, which is why
34
+ * `records.RECORD_GEOMETRY.tap` lists `NOF` under `sometimes`. EMUGE's own tap
35
+ * families run 2, 3 and 4 flutes across a size range, so no per-family constant
36
+ * could be true of every row, and 0 is not a substitute for a number nobody
37
+ * stated.
38
+ *
39
+ * **No CAD a scrape can reach.** The STEP, DXF and DIN 4000 documents are
40
+ * published behind a login — `anonymousAccess: false`, with both URL fields
41
+ * null — so neither `conventions.CAD_COLUMN` nor `CAD_DXF_COLUMN` is written.
42
+ * See `docs/EMUGE_FRANKEN_COMMERCE_API.md`.
43
+ */
44
+ import { type BoundFamily, type RecordMappers } from '../../family.js';
45
+ import { type ColumnMap, type ToolRecord } from '../../records.js';
46
+ import { type MapperOptions, type ScrapedRow } from '../../scrape.js';
47
+ /** The cutting-material property, spelled the same way in all three categories. */
48
+ export declare const SUBSTRATE_COLUMN = "Cutting material";
49
+ /**
50
+ * The coating property, which is **not** spelled the same way.
51
+ *
52
+ * `coating` on a milling part and `Coating` on a drill or a tap, in the same
53
+ * API, on the same day. Both are read and the first non-empty one wins, because
54
+ * relabelling one onto the other in the CSV would be this adapter deciding what
55
+ * the vendor meant — see `scrape.ts`.
56
+ */
57
+ export declare const COATING_COLUMNS: readonly ["coating", "Coating"];
58
+ /**
59
+ * Whether a part is through-coolant, by the vendor's own word for it.
60
+ *
61
+ * Three columns and three vocabularies, one per category, each a closed facet
62
+ * this package read off the vendor's own index on 2026-09-01 — so a value
63
+ * absent from here is a vocabulary that changed rather than a part that is
64
+ * odd, and {@link coolantThrough} refuses it by naming the table to add to.
65
+ *
66
+ * ## The facet does not cover the whole of milling
67
+ *
68
+ * The values below account for 6,862 milling variants, 2,670 drilling and
69
+ * 11,566 tapping. Drilling and tapping are the whole category — the same two
70
+ * numbers `docs/EMUGE_FRANKEN_COMMERCE_API.md` §4 gives — and milling is
71
+ * **159 short** of `FF01`'s 7,021, which is also `families/emuge.ts`'s two row
72
+ * counts added up. So 159 milling variants carry no `internal coolant supply`
73
+ * value at all.
74
+ *
75
+ * That is a gap in the vendor's index rather than a vocabulary that moved, and
76
+ * it is why {@link coolantThrough} separates the two cases: an unrecognised
77
+ * *value* still refuses, and a column nobody filled warns and records `false`.
78
+ * They were one case until 2026-09-01, and because `registry.toRecords` maps
79
+ * its rows, a single such part threw and took the whole family's conversion
80
+ * with it — 159 unindexed variants for two dead end mill CSVs.
81
+ */
82
+ export declare const COOLANT_COLUMNS: Readonly<Record<string, Readonly<Record<string, boolean>>>>;
83
+ /**
84
+ * The vendor's cutting materials, onto the package's own vocabulary.
85
+ *
86
+ * `FamilyFacts.bmc` names a material class — `carbide`, `hss`, `diamond` — and
87
+ * EMUGE's index names an alloy and a production route: HSS, HSSE and HSSE-PM
88
+ * are three grades of high-speed steel and all three are `hss`. `PCD` is
89
+ * `diamond`, which is the call `families/kennametal.ts` already made for its
90
+ * PCD drills: the word names the cutting material rather than the body it is
91
+ * brazed to.
92
+ *
93
+ * `cbn` and `ceramic` keep the vendor's own name, lowercased, because the
94
+ * package's three words have no counterpart for either and inventing one would
95
+ * be worse than recording what is standard. Closed, and it throws on anything
96
+ * else — the seven values below are the whole of the vendor's `HYB_AAM_MAT`
97
+ * facet across all three categories (2026-09-01).
98
+ */
99
+ export declare const SUBSTRATES: Readonly<Record<string, string>>;
100
+ /**
101
+ * The flute count EMUGE publishes where it has none: 64 end mill variants, on
102
+ * 2026-09-01. A sentinel and not a number, so it is refused rather than read.
103
+ */
104
+ export declare const NO_FLUTE_COUNT = 999;
105
+ /**
106
+ * A solid end mill, in the family's declared unit.
107
+ *
108
+ * The two shoulder fields fall back the way every end mill mapper here does: a
109
+ * family with no neck column is a plain tool whose usable length below the
110
+ * shank is its flute length and whose shoulder is its cutting diameter. EMUGE
111
+ * publishes `neck length l₃` and `neck diameter Ød₃` on the necked lines and
112
+ * neither on the plain ones, in the same family, so the fallback is per row
113
+ * rather than per family.
114
+ *
115
+ * A missing `radius r₁` is a square end and 0 is the right answer — the reason
116
+ * `RE` is optional on the end mill contract.
117
+ */
118
+ export declare function endmillRecord(row: ScrapedRow, family: BoundFamily, columns: ColumnMap, options?: MapperOptions): ToolRecord;
119
+ /**
120
+ * A twist drill, in millimetres — the only system EMUGE publishes its drill
121
+ * lengths in.
122
+ *
123
+ * `SIG` is a **mapped column**, which no other drill family in this package
124
+ * manages: the per-part detail record states the point angle outright, so
125
+ * nothing here is derived from a point length or assumed from a product line.
126
+ * `NOF` is the one thing that is a fact, and `nonFerrous` with it — neither has
127
+ * a default anywhere, by design.
128
+ */
129
+ export declare function drillRecord(row: ScrapedRow, family: BoundFamily, columns: ColumnMap, options?: MapperOptions): ToolRecord;
130
+ /**
131
+ * A tap, in millimetres — including an inch-thread one.
132
+ *
133
+ * That is what the vendor published rather than a conversion this package made:
134
+ * a `#4-40 UNC` tap's major diameter is stated as `2.845 mm` and its shank,
135
+ * overall length and cutting-edge length are millimetres too. So the family
136
+ * declares `unit: millimeters` and there is no per-row thread system to read —
137
+ * the shape Kennametal needs, where one family holds both and `Thread System`
138
+ * is a column the scrape tags on.
139
+ *
140
+ * `DC` is **read, not derived**. Kennametal parses a major diameter out of the
141
+ * thread designation because its tap tables publish no diameter column; EMUGE
142
+ * publishes one, and reading the vendor's own number is always the better of
143
+ * the two. The designation is still on the CSV as `dimensionFeatureValue`, and
144
+ * `thread symbol` and `threads per inch` beside it.
145
+ *
146
+ * `NOF` is absent, and its absence is the vendor's silence — see the module
147
+ * docstring and `records.RECORD_GEOMETRY`.
148
+ */
149
+ export declare function tapRecord(row: ScrapedRow, family: BoundFamily, columns: ColumnMap, options?: MapperOptions): ToolRecord;
150
+ export declare const RECORD_MAPPERS: RecordMappers;