@toolpath/tool-scraper 2.0.0 → 2.1.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/dist/columns.d.ts +5 -3
- package/dist/columns.js +11 -5
- package/dist/conventions.d.ts +18 -0
- package/dist/conventions.js +18 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.js +30 -0
- package/dist/families/emuge.d.ts +3 -1
- package/dist/families/emuge.js +8 -4
- package/dist/node/cli.js +6 -1
- package/dist/records.d.ts +57 -12
- package/dist/records.js +37 -13
- package/dist/registry.d.ts +26 -1
- package/dist/registry.js +40 -2
- package/dist/vendors/destinytool/records.js +8 -0
- package/dist/vendors/emuge/records.d.ts +83 -3
- package/dist/vendors/emuge/records.js +184 -16
- package/dist/vendors/kennametal/family.d.ts +119 -0
- package/dist/vendors/kennametal/family.js +155 -0
- package/dist/vendors/kennametal/index.d.ts +1 -0
- package/dist/vendors/kennametal/index.js +1 -0
- package/dist/vendors/kennametal/records.d.ts +16 -0
- package/dist/vendors/kennametal/records.js +32 -0
- package/dist/vendors/kennametal/scrape.d.ts +25 -2
- package/dist/vendors/kennametal/scrape.js +28 -3
- package/package.json +1 -1
package/dist/columns.d.ts
CHANGED
|
@@ -20,9 +20,11 @@
|
|
|
20
20
|
* - a column the family maps to nothing is `undefined`, not an error — a family
|
|
21
21
|
* with no neck column is a plain tool, and that is the mapper's fallback to
|
|
22
22
|
* make;
|
|
23
|
-
* - a **required** field with no reading refuses the row
|
|
24
|
-
*
|
|
25
|
-
* a part
|
|
23
|
+
* - a **required** field with no reading refuses the row with an
|
|
24
|
+
* `errors.IncompletePartError`, naming the canonical field and quoting the
|
|
25
|
+
* cell, because a tool with no cutting diameter is not a part — and that is
|
|
26
|
+
* the one refusal `registry.toRecords` skips past rather than failing the
|
|
27
|
+
* whole family on;
|
|
26
28
|
* - an **optional** one answers null and lets the mapper decide.
|
|
27
29
|
*
|
|
28
30
|
* `vendors/destinytool/records.ts` keeps its own `required` and is not wired
|
package/dist/columns.js
CHANGED
|
@@ -20,9 +20,11 @@
|
|
|
20
20
|
* - a column the family maps to nothing is `undefined`, not an error — a family
|
|
21
21
|
* with no neck column is a plain tool, and that is the mapper's fallback to
|
|
22
22
|
* make;
|
|
23
|
-
* - a **required** field with no reading refuses the row
|
|
24
|
-
*
|
|
25
|
-
* a part
|
|
23
|
+
* - a **required** field with no reading refuses the row with an
|
|
24
|
+
* `errors.IncompletePartError`, naming the canonical field and quoting the
|
|
25
|
+
* cell, because a tool with no cutting diameter is not a part — and that is
|
|
26
|
+
* the one refusal `registry.toRecords` skips past rather than failing the
|
|
27
|
+
* whole family on;
|
|
26
28
|
* - an **optional** one answers null and lets the mapper decide.
|
|
27
29
|
*
|
|
28
30
|
* `vendors/destinytool/records.ts` keeps its own `required` and is not wired
|
|
@@ -31,7 +33,7 @@
|
|
|
31
33
|
* has no `optional` at all. Forcing it into this shape would change how it
|
|
32
34
|
* refuses, which is the one thing `measure.ts` says is a vendor's own call.
|
|
33
35
|
*/
|
|
34
|
-
import {
|
|
36
|
+
import { IncompletePartError } from './errors.js';
|
|
35
37
|
/**
|
|
36
38
|
* The three readers, over one vendor's `read`.
|
|
37
39
|
*
|
|
@@ -54,7 +56,11 @@ export function columnReaders(read) {
|
|
|
54
56
|
const raw = cell(row, columns, canonical, unit);
|
|
55
57
|
const value = raw === undefined ? null : read(raw, unit, what, options.warn);
|
|
56
58
|
if (value === null) {
|
|
57
|
-
|
|
59
|
+
// `IncompletePartError` and not the general vendor fault: this is the
|
|
60
|
+
// one refusal `registry.toRecords` skips past, because a single part the
|
|
61
|
+
// vendor left a cell blank on must not end a family's conversion. See
|
|
62
|
+
// that type for why the others still must not be skipped.
|
|
63
|
+
throw new IncompletePartError(what, `publishes no ${canonical} — its cell is ${JSON.stringify(raw ?? '')}`);
|
|
58
64
|
}
|
|
59
65
|
return value;
|
|
60
66
|
};
|
package/dist/conventions.d.ts
CHANGED
|
@@ -97,6 +97,24 @@ export declare const CAD_DXF_COLUMN = "CAD_DXF_URL";
|
|
|
97
97
|
* `records.ToolRecord.description`, which states the same rule for the record.
|
|
98
98
|
*/
|
|
99
99
|
export declare const DESCRIPTION_COLUMN = "Description";
|
|
100
|
+
/**
|
|
101
|
+
* The CSV column holding the vendor's own full name for the family a part is
|
|
102
|
+
* in — `KenCut™ FF • HPFT • Square End • 6 Flutes • Plain Shank • Inch`.
|
|
103
|
+
*
|
|
104
|
+
* Constant down a family's whole table, and that is what makes it a *family*
|
|
105
|
+
* title rather than a {@link DESCRIPTION_COLUMN}: it names the group, not the
|
|
106
|
+
* part. Kennametal and WIDIA publish one as the `h1` of a family page and it
|
|
107
|
+
* reaches the CSV whole, while `records.ToolRecord.productLine` keeps only its
|
|
108
|
+
* leading segment — the rest is the vendor's own wording for the shape, the
|
|
109
|
+
* flute count, the shank and the unit, and a receipt that dropped it would be
|
|
110
|
+
* throwing away published text to save a column.
|
|
111
|
+
*
|
|
112
|
+
* Vendor-neutral and here rather than in the Kennametal adapter for the reason
|
|
113
|
+
* {@link CAD_COLUMN} is: a second vendor that publishes a family title writes
|
|
114
|
+
* this column rather than inventing another. Nothing forces one to — a vendor
|
|
115
|
+
* whose parts carry no family title simply has no such column.
|
|
116
|
+
*/
|
|
117
|
+
export declare const FAMILY_TITLE_COLUMN = "Family Title";
|
|
100
118
|
/**
|
|
101
119
|
* The CSV column saying how a holder seats in the spindle: `taper` or `face`.
|
|
102
120
|
*
|
package/dist/conventions.js
CHANGED
|
@@ -98,6 +98,24 @@ export const CAD_DXF_COLUMN = 'CAD_DXF_URL';
|
|
|
98
98
|
* `records.ToolRecord.description`, which states the same rule for the record.
|
|
99
99
|
*/
|
|
100
100
|
export const DESCRIPTION_COLUMN = 'Description';
|
|
101
|
+
/**
|
|
102
|
+
* The CSV column holding the vendor's own full name for the family a part is
|
|
103
|
+
* in — `KenCut™ FF • HPFT • Square End • 6 Flutes • Plain Shank • Inch`.
|
|
104
|
+
*
|
|
105
|
+
* Constant down a family's whole table, and that is what makes it a *family*
|
|
106
|
+
* title rather than a {@link DESCRIPTION_COLUMN}: it names the group, not the
|
|
107
|
+
* part. Kennametal and WIDIA publish one as the `h1` of a family page and it
|
|
108
|
+
* reaches the CSV whole, while `records.ToolRecord.productLine` keeps only its
|
|
109
|
+
* leading segment — the rest is the vendor's own wording for the shape, the
|
|
110
|
+
* flute count, the shank and the unit, and a receipt that dropped it would be
|
|
111
|
+
* throwing away published text to save a column.
|
|
112
|
+
*
|
|
113
|
+
* Vendor-neutral and here rather than in the Kennametal adapter for the reason
|
|
114
|
+
* {@link CAD_COLUMN} is: a second vendor that publishes a family title writes
|
|
115
|
+
* this column rather than inventing another. Nothing forces one to — a vendor
|
|
116
|
+
* whose parts carry no family title simply has no such column.
|
|
117
|
+
*/
|
|
118
|
+
export const FAMILY_TITLE_COLUMN = 'Family Title';
|
|
101
119
|
/**
|
|
102
120
|
* The CSV column saying how a holder seats in the spindle: `taper` or `face`.
|
|
103
121
|
*
|
package/dist/errors.d.ts
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* cannot read.** The variants endpoint changed shape, a scrape's row count
|
|
11
11
|
* disagrees with the declared one, a DIN 4000 code that should be pinned is
|
|
12
12
|
* absent. The catalog is fine; the world moved.
|
|
13
|
+
* - {@link IncompletePartError} — **one part is missing a measurement it
|
|
14
|
+
* cannot be a part without.** A narrower case of the second, and the only
|
|
15
|
+
* one `registry.toRecords` may skip past: see below.
|
|
13
16
|
*
|
|
14
17
|
* They throw rather than exiting the process: a Node backend imports this, and
|
|
15
18
|
* a mapped column that moved must not take down somebody's request handler.
|
|
@@ -43,4 +46,31 @@ export declare class ScraperConfigError extends ScraperError {
|
|
|
43
46
|
*/
|
|
44
47
|
export declare class VendorResponseError extends ScraperError {
|
|
45
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* One part does not publish a dimension its kind requires.
|
|
51
|
+
*
|
|
52
|
+
* **The only failure a whole family survives.** `registry.toRecords` maps a
|
|
53
|
+
* family's rows together, so before this type existed every refusal was
|
|
54
|
+
* equally fatal: one part with an unpublished cell ended the conversion and
|
|
55
|
+
* took every other row with it. EMUGE-FRANKEN omits `overall length l₁` on
|
|
56
|
+
* roughly 175 of its 7,021 end mill variants, and both end mill families
|
|
57
|
+
* produced nothing at all because of them.
|
|
58
|
+
*
|
|
59
|
+
* It is a distinct type rather than a flag on {@link VendorResponseError}
|
|
60
|
+
* because the two must not be skipped alike. A cutting material this package
|
|
61
|
+
* has no word for, a point-angle column a family stopped mapping, a variants
|
|
62
|
+
* table that changed shape — those are the vendor's vocabulary or this
|
|
63
|
+
* package's map having moved, and skipping past them quietly is how a scraper
|
|
64
|
+
* starts publishing a catalog nobody checked. Only `columns.required` raises
|
|
65
|
+
* this one, and only for a cell the vendor left unpublished.
|
|
66
|
+
*
|
|
67
|
+
* **It is not a licence to relax a kind's contract.** `records.RECORD_GEOMETRY`
|
|
68
|
+
* still says an end mill always has an `OAL`, and that stays true of every
|
|
69
|
+
* record this package emits: a part without one does not become a record with
|
|
70
|
+
* a hole in it, it becomes no record and a warning. Where a *vendor* genuinely
|
|
71
|
+
* never publishes a field, the answer is still `sometimes` — that is what a
|
|
72
|
+
* drill's `SIG` is, and why one is a contract decision and the other is not.
|
|
73
|
+
*/
|
|
74
|
+
export declare class IncompletePartError extends VendorResponseError {
|
|
75
|
+
}
|
|
46
76
|
export {};
|
package/dist/errors.js
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* cannot read.** The variants endpoint changed shape, a scrape's row count
|
|
11
11
|
* disagrees with the declared one, a DIN 4000 code that should be pinned is
|
|
12
12
|
* absent. The catalog is fine; the world moved.
|
|
13
|
+
* - {@link IncompletePartError} — **one part is missing a measurement it
|
|
14
|
+
* cannot be a part without.** A narrower case of the second, and the only
|
|
15
|
+
* one `registry.toRecords` may skip past: see below.
|
|
13
16
|
*
|
|
14
17
|
* They throw rather than exiting the process: a Node backend imports this, and
|
|
15
18
|
* a mapped column that moved must not take down somebody's request handler.
|
|
@@ -51,3 +54,30 @@ export class ScraperConfigError extends ScraperError {
|
|
|
51
54
|
*/
|
|
52
55
|
export class VendorResponseError extends ScraperError {
|
|
53
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* One part does not publish a dimension its kind requires.
|
|
59
|
+
*
|
|
60
|
+
* **The only failure a whole family survives.** `registry.toRecords` maps a
|
|
61
|
+
* family's rows together, so before this type existed every refusal was
|
|
62
|
+
* equally fatal: one part with an unpublished cell ended the conversion and
|
|
63
|
+
* took every other row with it. EMUGE-FRANKEN omits `overall length l₁` on
|
|
64
|
+
* roughly 175 of its 7,021 end mill variants, and both end mill families
|
|
65
|
+
* produced nothing at all because of them.
|
|
66
|
+
*
|
|
67
|
+
* It is a distinct type rather than a flag on {@link VendorResponseError}
|
|
68
|
+
* because the two must not be skipped alike. A cutting material this package
|
|
69
|
+
* has no word for, a point-angle column a family stopped mapping, a variants
|
|
70
|
+
* table that changed shape — those are the vendor's vocabulary or this
|
|
71
|
+
* package's map having moved, and skipping past them quietly is how a scraper
|
|
72
|
+
* starts publishing a catalog nobody checked. Only `columns.required` raises
|
|
73
|
+
* this one, and only for a cell the vendor left unpublished.
|
|
74
|
+
*
|
|
75
|
+
* **It is not a licence to relax a kind's contract.** `records.RECORD_GEOMETRY`
|
|
76
|
+
* still says an end mill always has an `OAL`, and that stays true of every
|
|
77
|
+
* record this package emits: a part without one does not become a record with
|
|
78
|
+
* a hole in it, it becomes no record and a warning. Where a *vendor* genuinely
|
|
79
|
+
* never publishes a field, the answer is still `sometimes` — that is what a
|
|
80
|
+
* drill's `SIG` is, and why one is a contract decision and the other is not.
|
|
81
|
+
*/
|
|
82
|
+
export class IncompletePartError extends VendorResponseError {
|
|
83
|
+
}
|
package/dist/families/emuge.d.ts
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* coating and the coolant supply **per part**, in columns, and a scraped column
|
|
9
9
|
* beats a family constant. Splitting by product line would turn four counted
|
|
10
10
|
* row totals into thirty and buy nothing a `product line` column does not
|
|
11
|
-
* already carry.
|
|
11
|
+
* already carry — and it is carried: `vendors/emuge/records.ts`'s
|
|
12
|
+
* `PRODUCT_LINE_COLUMNS` reads one per part onto `ToolRecord.productLine`, from
|
|
13
|
+
* a column every scrape already writes.
|
|
12
14
|
*
|
|
13
15
|
* So the only fact three of these four state is `unit`, and the fourth adds the
|
|
14
16
|
* two a drill record cannot be built without.
|
package/dist/families/emuge.js
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* coating and the coolant supply **per part**, in columns, and a scraped column
|
|
9
9
|
* beats a family constant. Splitting by product line would turn four counted
|
|
10
10
|
* row totals into thirty and buy nothing a `product line` column does not
|
|
11
|
-
* already carry.
|
|
11
|
+
* already carry — and it is carried: `vendors/emuge/records.ts`'s
|
|
12
|
+
* `PRODUCT_LINE_COLUMNS` reads one per part onto `ToolRecord.productLine`, from
|
|
13
|
+
* a column every scrape already writes.
|
|
12
14
|
*
|
|
13
15
|
* So the only fact three of these four state is `unit`, and the fourth adds the
|
|
14
16
|
* two a drill record cannot be built without.
|
|
@@ -100,9 +102,11 @@ export const FAMILIES = {
|
|
|
100
102
|
kind: 'drill',
|
|
101
103
|
familyCode: 'FB01',
|
|
102
104
|
// `SIG` is a mapped column and not a fact, which no other drill family in
|
|
103
|
-
// this package manages: EMUGE states a point angle on
|
|
104
|
-
//
|
|
105
|
-
//
|
|
105
|
+
// this package manages: EMUGE states a point angle on the detail record of
|
|
106
|
+
// 2,669 of these 2,670 parts, and leaves the cell empty on the last, so the
|
|
107
|
+
// record may carry no `SIG` — see `vendors/emuge/records.ts`'s `angle`.
|
|
108
|
+
// Kennametal's two drill lines assume theirs or derive them from a point
|
|
109
|
+
// length, and both say so at length in `families/kennametal.ts`.
|
|
106
110
|
columns: {
|
|
107
111
|
DC: 'nominal diameter d₁',
|
|
108
112
|
SFDM: 'Shank diameter d₂',
|
package/dist/node/cli.js
CHANGED
|
@@ -257,7 +257,12 @@ async function kennametal(argv, io, fetcher) {
|
|
|
257
257
|
const at = a.indexOf('=');
|
|
258
258
|
return [a.slice(0, at), a.slice(at + 1)];
|
|
259
259
|
});
|
|
260
|
-
|
|
260
|
+
// The family page carries the product line, which the variants table does
|
|
261
|
+
// not state anywhere. One extra request per family — see
|
|
262
|
+
// `vendors/kennametal/family.ts`.
|
|
263
|
+
const scrape = await scrapeFamily(fetcher, code, brand, tags, {
|
|
264
|
+
familyTitle: true,
|
|
265
|
+
});
|
|
261
266
|
wrote(out, brand, scrape, io);
|
|
262
267
|
return 0;
|
|
263
268
|
}
|
package/dist/records.d.ts
CHANGED
|
@@ -236,17 +236,31 @@ export declare const DIMENSIONAL_COLUMNS: ReadonlySet<GeometryName>;
|
|
|
236
236
|
* a key in `sometimes` may be missing and its absence is the vendor's silence;
|
|
237
237
|
* a key in neither list is not part of that kind's record at all.
|
|
238
238
|
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* -
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
239
|
+
* Every `sometimes` entry today is one vendor publishing nothing where another
|
|
240
|
+
* publishes a number. `sometimes` permits the key, it does not forbid it — the
|
|
241
|
+
* vendors that state these keep filling them.
|
|
242
|
+
*
|
|
243
|
+
* - the **end mill's `NOF`**, for Harvey's two deburring families — they
|
|
244
|
+
* publish right- and left-hand tooth counts and no flute count, so there is
|
|
245
|
+
* nothing to read and 0 is not a substitute;
|
|
246
|
+
* - the **tap's `NOF`**, for EMUGE-FRANKEN, which states no flute count
|
|
247
|
+
* anywhere a scrape can reach — not on the grouped product, the variant
|
|
248
|
+
* listing, the per-part detail record or any facet — while its own tap
|
|
249
|
+
* families run 2, 3 and 4 flutes across their size range, so no per-family
|
|
250
|
+
* constant could be true of every row. Kennametal's taps publish a `Z`
|
|
251
|
+
* column and keep filling it;
|
|
252
|
+
* - the **drill's `SIG`**, for the one EMUGE-FRANKEN drill whose point-angle
|
|
253
|
+
* cell is empty. This one is a hole in a column the vendor otherwise fills,
|
|
254
|
+
* not a column it never had, and it is the reason the key had to move: the
|
|
255
|
+
* EMUGE drill family reads `SIG` from a column rather than from a fact, so
|
|
256
|
+
* a single blank cell refused the row, and `registry.toRecords` maps a
|
|
257
|
+
* family's rows together — 2,669 drills were lost to the 2,670th. Every
|
|
258
|
+
* Kennametal drill supplies `SIG` from a fact and none can be absent.
|
|
259
|
+
*
|
|
260
|
+
* A point angle is not a field to guess at. It sets the drill tip's length, so
|
|
261
|
+
* a CAM system that assumed 140 deg for a part ground at 118 would cut a hole
|
|
262
|
+
* to the wrong depth — an absence a consumer must check for is recoverable and
|
|
263
|
+
* a plausible wrong number is not.
|
|
250
264
|
*/
|
|
251
265
|
export declare const RECORD_GEOMETRY: Record<ToolKind, {
|
|
252
266
|
readonly always: readonly GeometryName[];
|
|
@@ -325,6 +339,37 @@ export interface ToolRecord {
|
|
|
325
339
|
* consumer that needs a per-part string has `catalogNumber`.
|
|
326
340
|
*/
|
|
327
341
|
readonly description: string;
|
|
342
|
+
/**
|
|
343
|
+
* The vendor's own name for the product line this part belongs to —
|
|
344
|
+
* `FRANKEN TOP-Cut`, `MultiDRILL`, `KenCut™ FF`, `Viper` — or `null` where
|
|
345
|
+
* the vendor names none.
|
|
346
|
+
*
|
|
347
|
+
* **Null is the vendor's silence, not an empty name**, the same three-state
|
|
348
|
+
* reasoning {@link ToolRecord.materialGroups} makes with {@link UNSPECIFIED}
|
|
349
|
+
* and for the same reason: a consumer faceting a catalog by product line has
|
|
350
|
+
* to be able to tell "EMUGE calls this Alu-Cut" from "nobody has decided what
|
|
351
|
+
* Harvey's line is called", and `''` collapses the two.
|
|
352
|
+
*
|
|
353
|
+
* **Verbatim, and never inferred** — the {@link ToolRecord.coating} rule.
|
|
354
|
+
* EMUGE's own index says `FRANKEN Expert` for the eight end mills its
|
|
355
|
+
* marketing calls "Cut & Form", and Destiny Tool ships `viper-mini` and
|
|
356
|
+
* `python` beside `Viper` and `Raptor`; case-folding either here would be
|
|
357
|
+
* this package authoring a vendor's catalog. The one thing an adapter may do
|
|
358
|
+
* is map a vendor's *code* onto that same vendor's *own* published name for
|
|
359
|
+
* it — see `vendors/emuge/records.ts`'s `PRODUCT_LINES`, where both sides of
|
|
360
|
+
* every entry are EMUGE's and the article page each name came from is cited.
|
|
361
|
+
*
|
|
362
|
+
* **Where it comes from is a fact about the vendor's data, not a convention.**
|
|
363
|
+
* Three sources are in use and each is the only one its vendor offers: a
|
|
364
|
+
* scraped column read per part (EMUGE, Destiny Tool), a page title fetched
|
|
365
|
+
* per family (Kennametal, WIDIA), and nothing at all (Harvey Tool, whose
|
|
366
|
+
* product-line title is already this record's `description` — a second copy
|
|
367
|
+
* of one string is the thing that field's own docstring refuses).
|
|
368
|
+
*
|
|
369
|
+
* **Never a copy of another field on this record**, for the reason
|
|
370
|
+
* {@link ToolRecord.description} states it.
|
|
371
|
+
*/
|
|
372
|
+
readonly productLine: string | null;
|
|
328
373
|
readonly kind: ToolKind;
|
|
329
374
|
readonly unit: UnitSystem;
|
|
330
375
|
readonly substrate: string;
|
|
@@ -388,7 +433,7 @@ export interface ToolRecord {
|
|
|
388
433
|
* interchange value, and a mapper that mutated one would be reaching back
|
|
389
434
|
* across the seam this type exists to draw.
|
|
390
435
|
*/
|
|
391
|
-
export declare function toolRecord(fields: Omit<ToolRecord, 'guid' | 'materialGroups' | 'materialGroupsSource' | 'nonFerrous'> & Partial<Pick<ToolRecord, 'materialGroups' | 'materialGroupsSource' | 'nonFerrous'>>): ToolRecord;
|
|
436
|
+
export declare function toolRecord(fields: Omit<ToolRecord, 'guid' | 'materialGroups' | 'materialGroupsSource' | 'nonFerrous' | 'productLine'> & Partial<Pick<ToolRecord, 'materialGroups' | 'materialGroupsSource' | 'nonFerrous' | 'productLine'>>): ToolRecord;
|
|
392
437
|
/**
|
|
393
438
|
* A family's canonical-field → CSV-column-label mapping, validated.
|
|
394
439
|
*
|
package/dist/records.js
CHANGED
|
@@ -194,22 +194,36 @@ export const DIMENSIONAL_COLUMNS = new Set([...DIMENSIONAL].filter((name) => nam
|
|
|
194
194
|
* a key in `sometimes` may be missing and its absence is the vendor's silence;
|
|
195
195
|
* a key in neither list is not part of that kind's record at all.
|
|
196
196
|
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* -
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
197
|
+
* Every `sometimes` entry today is one vendor publishing nothing where another
|
|
198
|
+
* publishes a number. `sometimes` permits the key, it does not forbid it — the
|
|
199
|
+
* vendors that state these keep filling them.
|
|
200
|
+
*
|
|
201
|
+
* - the **end mill's `NOF`**, for Harvey's two deburring families — they
|
|
202
|
+
* publish right- and left-hand tooth counts and no flute count, so there is
|
|
203
|
+
* nothing to read and 0 is not a substitute;
|
|
204
|
+
* - the **tap's `NOF`**, for EMUGE-FRANKEN, which states no flute count
|
|
205
|
+
* anywhere a scrape can reach — not on the grouped product, the variant
|
|
206
|
+
* listing, the per-part detail record or any facet — while its own tap
|
|
207
|
+
* families run 2, 3 and 4 flutes across their size range, so no per-family
|
|
208
|
+
* constant could be true of every row. Kennametal's taps publish a `Z`
|
|
209
|
+
* column and keep filling it;
|
|
210
|
+
* - the **drill's `SIG`**, for the one EMUGE-FRANKEN drill whose point-angle
|
|
211
|
+
* cell is empty. This one is a hole in a column the vendor otherwise fills,
|
|
212
|
+
* not a column it never had, and it is the reason the key had to move: the
|
|
213
|
+
* EMUGE drill family reads `SIG` from a column rather than from a fact, so
|
|
214
|
+
* a single blank cell refused the row, and `registry.toRecords` maps a
|
|
215
|
+
* family's rows together — 2,669 drills were lost to the 2,670th. Every
|
|
216
|
+
* Kennametal drill supplies `SIG` from a fact and none can be absent.
|
|
217
|
+
*
|
|
218
|
+
* A point angle is not a field to guess at. It sets the drill tip's length, so
|
|
219
|
+
* a CAM system that assumed 140 deg for a part ground at 118 would cut a hole
|
|
220
|
+
* to the wrong depth — an absence a consumer must check for is recoverable and
|
|
221
|
+
* a plausible wrong number is not.
|
|
208
222
|
*/
|
|
209
223
|
export const RECORD_GEOMETRY = {
|
|
210
224
|
drill: {
|
|
211
|
-
always: ['DC', 'SFDM', 'OAL', 'LCF', 'NOF'
|
|
212
|
-
sometimes: [],
|
|
225
|
+
always: ['DC', 'SFDM', 'OAL', 'LCF', 'NOF'],
|
|
226
|
+
sometimes: ['SIG'],
|
|
213
227
|
},
|
|
214
228
|
tap: {
|
|
215
229
|
always: ['DC', 'TP', 'SFDM', 'OAL', 'LCF'],
|
|
@@ -271,6 +285,15 @@ function checkGeometry(kind, what, geometry) {
|
|
|
271
285
|
export function toolRecord(fields) {
|
|
272
286
|
const groups = fields.materialGroups ?? null;
|
|
273
287
|
const source = fields.materialGroupsSource ?? UNSPECIFIED;
|
|
288
|
+
// An adapter that has nothing to say about the product line says nothing;
|
|
289
|
+
// `''` would be a name and there is no nameless line. Optional here and
|
|
290
|
+
// required on the type for the reason stated above — a consumer reading a
|
|
291
|
+
// record never handles `undefined`.
|
|
292
|
+
const line = fields.productLine ?? null;
|
|
293
|
+
if (line === '') {
|
|
294
|
+
throw new ScraperConfigError(fields.materialNumber, `productLine is the empty string — a vendor that names no product line ` +
|
|
295
|
+
`is null, which is the state a consumer can tell from a name`);
|
|
296
|
+
}
|
|
274
297
|
// The invariant is what keeps the three states three: groups labelled
|
|
275
298
|
// `unspecified` are groups nobody stated, and an attributed source with no
|
|
276
299
|
// groups attributes nothing. Either would land as a record whose material
|
|
@@ -288,6 +311,7 @@ export function toolRecord(fields) {
|
|
|
288
311
|
materialGroups: groups === null ? null : Object.freeze([...groups]),
|
|
289
312
|
materialGroupsSource: source,
|
|
290
313
|
nonFerrous: fields.nonFerrous ?? null,
|
|
314
|
+
productLine: line,
|
|
291
315
|
});
|
|
292
316
|
}
|
|
293
317
|
/**
|
package/dist/registry.d.ts
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import { type BoundFamily, type BoundToolholding, type RecordMappers } from './family.js';
|
|
30
30
|
import { type ToolRecord } from './records.js';
|
|
31
|
-
import type
|
|
31
|
+
import { type MapperOptions, type ScrapeResult } from './scrape.js';
|
|
32
32
|
/**
|
|
33
33
|
* Brand -> its row-to-record mappers, by tool kind.
|
|
34
34
|
*
|
|
@@ -81,6 +81,31 @@ export declare function boundFamily(name: string): BoundFamily;
|
|
|
81
81
|
*
|
|
82
82
|
* `familyName` is the CSV filename the catalog is keyed by
|
|
83
83
|
* (`'harvey_endmill_025.csv'`), which is what `boundFamily` takes.
|
|
84
|
+
*
|
|
85
|
+
* ## One incomplete part does not end the family
|
|
86
|
+
*
|
|
87
|
+
* The rows are mapped together, so until 2026-09-01 every refusal was equally
|
|
88
|
+
* fatal — and the refusals are not equal. A part the vendor left a required
|
|
89
|
+
* cell blank on is one bad row among thousands of good ones; EMUGE-FRANKEN
|
|
90
|
+
* omits `overall length l₁` on roughly 175 of its 7,021 end mill variants, and
|
|
91
|
+
* both end mill families converted to nothing at all because of them.
|
|
92
|
+
*
|
|
93
|
+
* So an {@link IncompletePartError} is warned about and the row is dropped.
|
|
94
|
+
* **Nothing else is.** A cutting material with no mapping, a column a family
|
|
95
|
+
* stopped mapping, a response that changed shape — those say the vendor's
|
|
96
|
+
* vocabulary or this package's catalog has moved, and a scraper that skipped
|
|
97
|
+
* quietly past them would publish a catalog nobody checked. `columns.required`
|
|
98
|
+
* is the only place that raises the skippable one.
|
|
99
|
+
*
|
|
100
|
+
* A dropped row is **not** a relaxed contract. `records.RECORD_GEOMETRY` still
|
|
101
|
+
* says an end mill always has an `OAL`, and every record returned here still
|
|
102
|
+
* has one: the part without it becomes no record rather than a record with a
|
|
103
|
+
* hole. Where a vendor genuinely never publishes a field, `sometimes` is still
|
|
104
|
+
* the answer — a drill's `SIG` is that, and it stays that.
|
|
105
|
+
*
|
|
106
|
+
* The count of what was dropped is not returned. A caller that needs it has
|
|
107
|
+
* the row count it passed in and the length it got back, and the warnings name
|
|
108
|
+
* every part by number.
|
|
84
109
|
*/
|
|
85
110
|
export declare function toRecords(familyName: string, scrape: ScrapeResult, options?: MapperOptions): ToolRecord[];
|
|
86
111
|
/**
|
package/dist/registry.js
CHANGED
|
@@ -29,9 +29,10 @@
|
|
|
29
29
|
import { checkIdentityColumns } from './conventions.js';
|
|
30
30
|
import { familyBrand, } from './family.js';
|
|
31
31
|
import { COLLET_FAMILIES, FAMILIES, HOLDER_FAMILIES } from './families/index.js';
|
|
32
|
-
import { ScraperConfigError } from './errors.js';
|
|
32
|
+
import { IncompletePartError, ScraperConfigError } from './errors.js';
|
|
33
33
|
import { checkFact } from './provenance.js';
|
|
34
34
|
import { checkColumnMap, checkColumnsExist } from './records.js';
|
|
35
|
+
import { consoleWarn } from './scrape.js';
|
|
35
36
|
import { RECORD_MAPPERS as DESTINYTOOL } from './vendors/destinytool/records.js';
|
|
36
37
|
import { RECORD_MAPPERS as EMUGE } from './vendors/emuge/records.js';
|
|
37
38
|
import { RECORD_MAPPERS as HARVEY } from './vendors/harvey/records.js';
|
|
@@ -164,12 +165,49 @@ export function boundFamily(name) {
|
|
|
164
165
|
*
|
|
165
166
|
* `familyName` is the CSV filename the catalog is keyed by
|
|
166
167
|
* (`'harvey_endmill_025.csv'`), which is what `boundFamily` takes.
|
|
168
|
+
*
|
|
169
|
+
* ## One incomplete part does not end the family
|
|
170
|
+
*
|
|
171
|
+
* The rows are mapped together, so until 2026-09-01 every refusal was equally
|
|
172
|
+
* fatal — and the refusals are not equal. A part the vendor left a required
|
|
173
|
+
* cell blank on is one bad row among thousands of good ones; EMUGE-FRANKEN
|
|
174
|
+
* omits `overall length l₁` on roughly 175 of its 7,021 end mill variants, and
|
|
175
|
+
* both end mill families converted to nothing at all because of them.
|
|
176
|
+
*
|
|
177
|
+
* So an {@link IncompletePartError} is warned about and the row is dropped.
|
|
178
|
+
* **Nothing else is.** A cutting material with no mapping, a column a family
|
|
179
|
+
* stopped mapping, a response that changed shape — those say the vendor's
|
|
180
|
+
* vocabulary or this package's catalog has moved, and a scraper that skipped
|
|
181
|
+
* quietly past them would publish a catalog nobody checked. `columns.required`
|
|
182
|
+
* is the only place that raises the skippable one.
|
|
183
|
+
*
|
|
184
|
+
* A dropped row is **not** a relaxed contract. `records.RECORD_GEOMETRY` still
|
|
185
|
+
* says an end mill always has an `OAL`, and every record returned here still
|
|
186
|
+
* has one: the part without it becomes no record rather than a record with a
|
|
187
|
+
* hole. Where a vendor genuinely never publishes a field, `sometimes` is still
|
|
188
|
+
* the answer — a drill's `SIG` is that, and it stays that.
|
|
189
|
+
*
|
|
190
|
+
* The count of what was dropped is not returned. A caller that needs it has
|
|
191
|
+
* the row count it passed in and the length it got back, and the warnings name
|
|
192
|
+
* every part by number.
|
|
167
193
|
*/
|
|
168
194
|
export function toRecords(familyName, scrape, options) {
|
|
169
195
|
const cfg = boundFamily(familyName);
|
|
196
|
+
const warn = options?.warn ?? consoleWarn;
|
|
170
197
|
checkIdentityColumns(familyBrand(cfg), scrape.header);
|
|
171
198
|
checkColumnsExist(familyName, cfg, scrape.header);
|
|
172
|
-
|
|
199
|
+
const records = [];
|
|
200
|
+
for (const row of scrape.rows) {
|
|
201
|
+
try {
|
|
202
|
+
records.push(cfg.records(row, cfg, cfg.columns, options));
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (!(error instanceof IncompletePartError))
|
|
206
|
+
throw error;
|
|
207
|
+
warn(` WARNING: ${error.message} — no record written for it`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return records;
|
|
173
211
|
}
|
|
174
212
|
/**
|
|
175
213
|
* Forget what has been bound.
|
|
@@ -247,6 +247,14 @@ export function endmillRecord(row, family, columns, options = {}) {
|
|
|
247
247
|
description,
|
|
248
248
|
kind: 'endmill',
|
|
249
249
|
unit,
|
|
250
|
+
// `series` is the vendor's own product-line name and the one column here
|
|
251
|
+
// that names one — `Viper`, `Raptor`, `DiamondBack`, and `viper-mini` and
|
|
252
|
+
// `python` in the vendor's own lower case, which stays exactly as
|
|
253
|
+
// published for the reason `records.ToolRecord.productLine` gives. It has
|
|
254
|
+
// been scraped since the adapter was written and read by nothing until
|
|
255
|
+
// now; the description cannot stand in for it, because Raptor calls itself
|
|
256
|
+
// `DVH` there and DiamondBack calls itself `DBACK RGHR`.
|
|
257
|
+
productLine: row['series']?.trim() || null,
|
|
250
258
|
substrate: (row['material'] || fact(family, 'bmc', family.bmc)).toLowerCase(),
|
|
251
259
|
// No carbide grade is published; the coating id is what there is.
|
|
252
260
|
coating: row['coatingId'] ?? '',
|
|
@@ -14,7 +14,12 @@
|
|
|
14
14
|
* article code, per part, so there is no `conventions.IDENTITY_DEVIATIONS`
|
|
15
15
|
* entry — the first vendor since Kennametal that needs none.
|
|
16
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.
|
|
17
|
+
* detail record states it, so `SIG` is a mapped column here and no fact. On
|
|
18
|
+
* all but one part: 2,669 of 2,670 drill variants state one, and part
|
|
19
|
+
* `000000000010727800` publishes a single classification feature and no
|
|
20
|
+
* dimensional properties at all — so its row carries no `SIG` **key**, not
|
|
21
|
+
* an empty one. That is why `records.RECORD_GEOMETRY.drill` lists `SIG`
|
|
22
|
+
* under `sometimes`, and why {@link angle} reads the row rather than `cell`.
|
|
18
23
|
* - **A per-part ISO 513 index.** `applicationMaterials` returns the vendor's
|
|
19
24
|
* own P/M/K/N/S/H rating for each part, which fills `materialGroups` as
|
|
20
25
|
* `vendor-stated`.
|
|
@@ -97,6 +102,79 @@ export declare const COOLANT_COLUMNS: Readonly<Record<string, Readonly<Record<st
|
|
|
97
102
|
* facet across all three categories (2026-09-01).
|
|
98
103
|
*/
|
|
99
104
|
export declare const SUBSTRATES: Readonly<Record<string, string>>;
|
|
105
|
+
/**
|
|
106
|
+
* The column each EMUGE-FRANKEN category states its product line in, keyed by
|
|
107
|
+
* the vendor's own category code.
|
|
108
|
+
*
|
|
109
|
+
* **One column per category, and each is a facet that partitions its category
|
|
110
|
+
* exactly.** Checked against the vendor's own index on 2026-09-01, at group
|
|
111
|
+
* level rather than by summing counts:
|
|
112
|
+
*
|
|
113
|
+
* | Category | Facet | Values | Groups it covers | Groups in two values |
|
|
114
|
+
* | -------- | -------------------- | ------ | ---------------- | -------------------- |
|
|
115
|
+
* | `FF01` | `AMM_PROG_LINIE` | 15 | 554 of 554 | 2 |
|
|
116
|
+
* | `FB01` | `HYB_BAM_SB_GT` | 4 | 17 of 17 | 0 |
|
|
117
|
+
* | `FG01` | `HYB_BAM_SB_GT` | 17 | 414 of 414 | 0 |
|
|
118
|
+
*
|
|
119
|
+
* That is what makes a product line here a **read and not an arbitration**.
|
|
120
|
+
* EMUGE's marketing publishes 43 overlapping product-family pages — a tap is
|
|
121
|
+
* simultaneously "Rekord B-Z Taps", "Enorm Z Taps" and "Left Hand Taps" — and
|
|
122
|
+
* choosing between those would be this package inventing a rule the vendor
|
|
123
|
+
* never stated. The facets above are the vendor's own partition of the same
|
|
124
|
+
* catalog, so there is nothing to choose.
|
|
125
|
+
*
|
|
126
|
+
* **The two milling groups that fall in two values cost nothing**, because the
|
|
127
|
+
* milling column is read per part: `H300024` and `H300025` each hold both
|
|
128
|
+
* `FRANKEN TiNox-Cut` and `FRANKEN TiNox-Cut VAR` variants, and the per-part
|
|
129
|
+
* detail record states which is which. A rule that tagged a whole group would
|
|
130
|
+
* have had to pick.
|
|
131
|
+
*
|
|
132
|
+
* **No request is added for any of this.** `Geometry` is on the grouped product
|
|
133
|
+
* and `product line` on the per-part detail, so `scrape.ts` already writes both
|
|
134
|
+
* into every row — see its three-call note.
|
|
135
|
+
*/
|
|
136
|
+
export declare const PRODUCT_LINE_COLUMNS: Readonly<Record<string, string>>;
|
|
137
|
+
/**
|
|
138
|
+
* A category's product-line codes onto the vendor's own name for each.
|
|
139
|
+
*
|
|
140
|
+
* **Both sides are EMUGE's.** The key is the value its `HYB_BAM_SB_GT` facet
|
|
141
|
+
* publishes and the name is the title of the vendor's own product-family
|
|
142
|
+
* article page for it, read on 2026-09-01 and cited per entry. Nothing here is
|
|
143
|
+
* this package's wording, which is the whole condition on a table like this —
|
|
144
|
+
* the same rule {@link SUBSTRATES} keeps, one level less strict because that
|
|
145
|
+
* one maps onto a vocabulary this package owns and this one does not.
|
|
146
|
+
*
|
|
147
|
+
* **A category whose facet already names the line has no entry.** `FF01`'s
|
|
148
|
+
* values are `FRANKEN TOP-Cut`, `FRANKEN Hard-Cut`, `FRANKEN Alu-Cut` — the
|
|
149
|
+
* vendor's marketing names already — so milling passes through verbatim and
|
|
150
|
+
* appears nowhere below. Drilling and tapping index by a geometry code
|
|
151
|
+
* (`MULTI`, `Z`, `VA`) that matches nothing EMUGE sells under that name, which
|
|
152
|
+
* is the only reason this table exists.
|
|
153
|
+
*
|
|
154
|
+
* **A code with no article page keeps the code**, and that is a gap in the
|
|
155
|
+
* vendor's marketing rather than a hole here: `SPEED`, `FK`, `GAL`, `GG` and
|
|
156
|
+
* `TILEG` are real lines with no `/a/` page on the US storefront, so the
|
|
157
|
+
* honest answer is the vendor's own code until one appears.
|
|
158
|
+
*/
|
|
159
|
+
export declare const PRODUCT_LINES: Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
160
|
+
/**
|
|
161
|
+
* The product line this row states, or null where the vendor states none.
|
|
162
|
+
*
|
|
163
|
+
* Three answers and they are different things. A family whose category has no
|
|
164
|
+
* entry in {@link PRODUCT_LINE_COLUMNS} is a category this adapter has not
|
|
165
|
+
* looked at, and it answers null rather than guessing at which of the row's
|
|
166
|
+
* columns is the line. A column the vendor left empty is null too — the same
|
|
167
|
+
* silence `angle` treats as an omission rather than a fault. A *value* is
|
|
168
|
+
* mapped through {@link PRODUCT_LINES} where its category has a table and
|
|
169
|
+
* passed through verbatim where it does not, which is the milling case.
|
|
170
|
+
*
|
|
171
|
+
* It does not throw on an unknown code, and that is the difference between
|
|
172
|
+
* this and `substrate`: a cutting material this package cannot map would put a
|
|
173
|
+
* wrong word in `ToolRecord.substrate`, where an unmapped product-line code is
|
|
174
|
+
* the vendor's own code and readable as one. A new EMUGE geometry showing up
|
|
175
|
+
* as `SPEED` is a name to improve, not a record to refuse.
|
|
176
|
+
*/
|
|
177
|
+
export declare function productLine(row: ScrapedRow, family: BoundFamily): string | null;
|
|
100
178
|
/**
|
|
101
179
|
* The flute count EMUGE publishes where it has none: 64 end mill variants, on
|
|
102
180
|
* 2026-09-01. A sentinel and not a number, so it is refused rather than read.
|
|
@@ -123,8 +201,10 @@ export declare function endmillRecord(row: ScrapedRow, family: BoundFamily, colu
|
|
|
123
201
|
* `SIG` is a **mapped column**, which no other drill family in this package
|
|
124
202
|
* manages: the per-part detail record states the point angle outright, so
|
|
125
203
|
* nothing here is derived from a point length or assumed from a product line.
|
|
126
|
-
*
|
|
127
|
-
*
|
|
204
|
+
* It is also the one geometry key this record may not carry — one variant's
|
|
205
|
+
* cell is empty, and {@link angle} says what that costs. `NOF` is the one
|
|
206
|
+
* thing that is a fact, and `nonFerrous` with it — neither has a default
|
|
207
|
+
* anywhere, by design.
|
|
128
208
|
*/
|
|
129
209
|
export declare function drillRecord(row: ScrapedRow, family: BoundFamily, columns: ColumnMap, options?: MapperOptions): ToolRecord;
|
|
130
210
|
/**
|