@toolpath/tool-scraper 2.0.0 → 2.2.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 +54 -0
- package/dist/conventions.js +59 -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/family.d.ts +16 -2
- package/dist/holding.d.ts +396 -0
- package/dist/holding.js +360 -0
- package/dist/index.d.ts +23 -13
- package/dist/index.js +23 -13
- package/dist/node/cad-mirror.d.ts +58 -1
- package/dist/node/cad-mirror.js +56 -8
- package/dist/node/cli.d.ts +4 -1
- package/dist/node/cli.js +198 -19
- package/dist/node/holder-import.d.ts +223 -0
- package/dist/node/holder-import.js +379 -0
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.js +1 -0
- package/dist/node/paths.d.ts +16 -0
- package/dist/node/paths.js +20 -0
- package/dist/profiles.d.ts +265 -0
- package/dist/profiles.js +295 -0
- package/dist/records.d.ts +57 -12
- package/dist/records.js +37 -13
- package/dist/registry.d.ts +86 -5
- package/dist/registry.js +149 -8
- 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/holding.d.ts +35 -0
- package/dist/vendors/kennametal/holding.js +112 -0
- package/dist/vendors/kennametal/index.d.ts +2 -0
- package/dist/vendors/kennametal/index.js +2 -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/dist/vendors/maritool/holding.d.ts +79 -0
- package/dist/vendors/maritool/holding.js +164 -0
- package/dist/vendors/maritool/index.d.ts +1 -0
- package/dist/vendors/maritool/index.js +1 -0
- package/dist/vendors/maritool/scrape.d.ts +37 -13
- package/dist/vendors/maritool/scrape.js +53 -14
- package/dist/vendors/regofix/holding.d.ts +35 -0
- package/dist/vendors/regofix/holding.js +108 -0
- package/dist/vendors/regofix/index.d.ts +1 -0
- package/dist/vendors/regofix/index.js +1 -0
- package/dist/vendors/regofix/scrape.js +2 -2
- package/package.json +1 -1
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
|
@@ -27,8 +27,9 @@
|
|
|
27
27
|
* entry point into this package goes through here.
|
|
28
28
|
*/
|
|
29
29
|
import { type BoundFamily, type BoundToolholding, type RecordMappers } from './family.js';
|
|
30
|
+
import type { HoldingMappers, HoldingRecord } from './holding.js';
|
|
30
31
|
import { type ToolRecord } from './records.js';
|
|
31
|
-
import type
|
|
32
|
+
import { type MapperOptions, type ScrapeResult } from './scrape.js';
|
|
32
33
|
/**
|
|
33
34
|
* Brand -> its row-to-record mappers, by tool kind.
|
|
34
35
|
*
|
|
@@ -39,6 +40,26 @@ import type { MapperOptions, ScrapeResult } from './scrape.js';
|
|
|
39
40
|
* map.
|
|
40
41
|
*/
|
|
41
42
|
export declare const ADAPTERS: Record<string, RecordMappers>;
|
|
43
|
+
/**
|
|
44
|
+
* Brand -> its toolholding mappers, by the kind of thing they build.
|
|
45
|
+
*
|
|
46
|
+
* The toolholding counterpart of {@link ADAPTERS}, and **partial in both
|
|
47
|
+
* directions on purpose**. A brand absent from here can still be scraped: its
|
|
48
|
+
* families bind, its CSVs are written, and its receipt is checked, exactly as
|
|
49
|
+
* before — what it cannot do is mint records. A brand present with a mapper for
|
|
50
|
+
* only one kind is the same statement one level down; MariTool publishes ER
|
|
51
|
+
* collets that this package does not scrape, so it maps holders and nothing
|
|
52
|
+
* else.
|
|
53
|
+
*
|
|
54
|
+
* That is what makes minting records additive rather than a break. Nothing here
|
|
55
|
+
* changes what a vendor with no entry does today, and the refusal only happens
|
|
56
|
+
* where a caller explicitly asks for records from a family whose brand maps
|
|
57
|
+
* none — {@link toHolding}, which names the brand and what it does map.
|
|
58
|
+
*
|
|
59
|
+
* One entry serves two brands for the reason {@link ADAPTERS} states: Kennametal
|
|
60
|
+
* and WIDIA are the same platform and the same table vocabulary.
|
|
61
|
+
*/
|
|
62
|
+
export declare const HOLDING_ADAPTERS: Record<string, HoldingMappers>;
|
|
42
63
|
/**
|
|
43
64
|
* Every cutting-tool family, validated and bound to its record mapper.
|
|
44
65
|
*
|
|
@@ -46,13 +67,25 @@ export declare const ADAPTERS: Record<string, RecordMappers>;
|
|
|
46
67
|
*/
|
|
47
68
|
export declare function boundFamilies(): Map<string, BoundFamily>;
|
|
48
69
|
/**
|
|
49
|
-
* Every holder and collet family, with its facts checked and projected
|
|
70
|
+
* Every holder and collet family, with its facts checked and projected, and
|
|
71
|
+
* bound to the mapper its brand supplies for its kind.
|
|
72
|
+
*
|
|
73
|
+
* Their facts pass the same gate cutting-tool families' do: a taper or a
|
|
74
|
+
* clamping mode is a per-family constant no variant table states, exactly like
|
|
75
|
+
* a drill's flute count.
|
|
50
76
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
77
|
+
* **A family whose brand maps nothing binds `undefined` rather than throwing**,
|
|
78
|
+
* which is where this differs from {@link boundFamilies}. A cutting-tool family
|
|
79
|
+
* with no mapper is a catalog fault — nothing can be done with it — but a
|
|
80
|
+
* toolholding family with no mapper is the state every one of them was in until
|
|
81
|
+
* records existed, and it still scrapes, writes a CSV and checks a receipt.
|
|
82
|
+
* Refusing at bind time would take that away from every consumer that never
|
|
83
|
+
* asked for a record. {@link toHolding} is where the absence is reported, at
|
|
84
|
+
* the one call that cannot proceed without it.
|
|
54
85
|
*/
|
|
55
86
|
export declare function boundToolholding(): Map<string, BoundToolholding>;
|
|
87
|
+
/** One bound toolholding family by CSV name. */
|
|
88
|
+
export declare function boundHolding(name: string): BoundToolholding;
|
|
56
89
|
/** One bound cutting-tool family by CSV name. */
|
|
57
90
|
export declare function boundFamily(name: string): BoundFamily;
|
|
58
91
|
/**
|
|
@@ -81,8 +114,56 @@ export declare function boundFamily(name: string): BoundFamily;
|
|
|
81
114
|
*
|
|
82
115
|
* `familyName` is the CSV filename the catalog is keyed by
|
|
83
116
|
* (`'harvey_endmill_025.csv'`), which is what `boundFamily` takes.
|
|
117
|
+
*
|
|
118
|
+
* ## One incomplete part does not end the family
|
|
119
|
+
*
|
|
120
|
+
* The rows are mapped together, so until 2026-09-01 every refusal was equally
|
|
121
|
+
* fatal — and the refusals are not equal. A part the vendor left a required
|
|
122
|
+
* cell blank on is one bad row among thousands of good ones; EMUGE-FRANKEN
|
|
123
|
+
* omits `overall length l₁` on roughly 175 of its 7,021 end mill variants, and
|
|
124
|
+
* both end mill families converted to nothing at all because of them.
|
|
125
|
+
*
|
|
126
|
+
* So an {@link IncompletePartError} is warned about and the row is dropped.
|
|
127
|
+
* **Nothing else is.** A cutting material with no mapping, a column a family
|
|
128
|
+
* stopped mapping, a response that changed shape — those say the vendor's
|
|
129
|
+
* vocabulary or this package's catalog has moved, and a scraper that skipped
|
|
130
|
+
* quietly past them would publish a catalog nobody checked. `columns.required`
|
|
131
|
+
* is the only place that raises the skippable one.
|
|
132
|
+
*
|
|
133
|
+
* A dropped row is **not** a relaxed contract. `records.RECORD_GEOMETRY` still
|
|
134
|
+
* says an end mill always has an `OAL`, and every record returned here still
|
|
135
|
+
* has one: the part without it becomes no record rather than a record with a
|
|
136
|
+
* hole. Where a vendor genuinely never publishes a field, `sometimes` is still
|
|
137
|
+
* the answer — a drill's `SIG` is that, and it stays that.
|
|
138
|
+
*
|
|
139
|
+
* The count of what was dropped is not returned. A caller that needs it has
|
|
140
|
+
* the row count it passed in and the length it got back, and the warnings name
|
|
141
|
+
* every part by number.
|
|
84
142
|
*/
|
|
85
143
|
export declare function toRecords(familyName: string, scrape: ScrapeResult, options?: MapperOptions): ToolRecord[];
|
|
144
|
+
/**
|
|
145
|
+
* One toolholding family's scrape, as {@link HoldingRecord}s.
|
|
146
|
+
*
|
|
147
|
+
* {@link toRecords}'s counterpart, and deliberately the same shape: the two
|
|
148
|
+
* checks run before the first row, one incomplete part does not end the family,
|
|
149
|
+
* and the count of what was dropped is not returned because the caller has the
|
|
150
|
+
* row count it passed in and the length it got back.
|
|
151
|
+
*
|
|
152
|
+
* **It refuses only where a caller asked for something this package cannot
|
|
153
|
+
* give.** A toolholding family whose brand maps no mapper binds one anyway
|
|
154
|
+
* (see {@link boundToolholding}) and scrapes exactly as it did before; this is
|
|
155
|
+
* the one call that cannot proceed without one, so this is where the absence is
|
|
156
|
+
* named — with the brand and with what that brand does map, the way
|
|
157
|
+
* {@link boundFamilies} names a missing tool mapper.
|
|
158
|
+
*
|
|
159
|
+
* `checkColumnsExist` has no counterpart here: a holder family carries no
|
|
160
|
+
* `ColumnMap`, because the columns a holder publishes are the vendor's own and
|
|
161
|
+
* are read by that vendor's mapper rather than through a canonical name. What
|
|
162
|
+
* does still run is {@link checkIdentityColumns}, which catches the failure that
|
|
163
|
+
* matters most — a re-scrape whose part-number column was renamed still parses,
|
|
164
|
+
* still has the right row count, and mints every guid off an empty string.
|
|
165
|
+
*/
|
|
166
|
+
export declare function toHolding(familyName: string, scrape: ScrapeResult, options?: MapperOptions): HoldingRecord[];
|
|
86
167
|
/**
|
|
87
168
|
* Forget what has been bound.
|
|
88
169
|
*
|
package/dist/registry.js
CHANGED
|
@@ -29,13 +29,17 @@
|
|
|
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';
|
|
39
|
+
import { HOLDING_MAPPERS as KM_HOLDING } from './vendors/kennametal/holding.js';
|
|
38
40
|
import { RECORD_MAPPERS as KENNAMETAL } from './vendors/kennametal/records.js';
|
|
41
|
+
import { HOLDING_MAPPERS as MARITOOL_HOLDING } from './vendors/maritool/holding.js';
|
|
42
|
+
import { HOLDING_MAPPERS as REGOFIX_HOLDING } from './vendors/regofix/holding.js';
|
|
39
43
|
/**
|
|
40
44
|
* Brand -> its row-to-record mappers, by tool kind.
|
|
41
45
|
*
|
|
@@ -52,6 +56,31 @@ export const ADAPTERS = {
|
|
|
52
56
|
harvey: HARVEY,
|
|
53
57
|
emuge: EMUGE,
|
|
54
58
|
};
|
|
59
|
+
/**
|
|
60
|
+
* Brand -> its toolholding mappers, by the kind of thing they build.
|
|
61
|
+
*
|
|
62
|
+
* The toolholding counterpart of {@link ADAPTERS}, and **partial in both
|
|
63
|
+
* directions on purpose**. A brand absent from here can still be scraped: its
|
|
64
|
+
* families bind, its CSVs are written, and its receipt is checked, exactly as
|
|
65
|
+
* before — what it cannot do is mint records. A brand present with a mapper for
|
|
66
|
+
* only one kind is the same statement one level down; MariTool publishes ER
|
|
67
|
+
* collets that this package does not scrape, so it maps holders and nothing
|
|
68
|
+
* else.
|
|
69
|
+
*
|
|
70
|
+
* That is what makes minting records additive rather than a break. Nothing here
|
|
71
|
+
* changes what a vendor with no entry does today, and the refusal only happens
|
|
72
|
+
* where a caller explicitly asks for records from a family whose brand maps
|
|
73
|
+
* none — {@link toHolding}, which names the brand and what it does map.
|
|
74
|
+
*
|
|
75
|
+
* One entry serves two brands for the reason {@link ADAPTERS} states: Kennametal
|
|
76
|
+
* and WIDIA are the same platform and the same table vocabulary.
|
|
77
|
+
*/
|
|
78
|
+
export const HOLDING_ADAPTERS = {
|
|
79
|
+
kennametal: KM_HOLDING,
|
|
80
|
+
widia: KM_HOLDING,
|
|
81
|
+
regofix: REGOFIX_HOLDING,
|
|
82
|
+
maritool: MARITOOL_HOLDING,
|
|
83
|
+
};
|
|
55
84
|
/**
|
|
56
85
|
* Check every fact of one family and project its value onto the config under
|
|
57
86
|
* its own key.
|
|
@@ -109,27 +138,50 @@ export function boundFamilies() {
|
|
|
109
138
|
return bound;
|
|
110
139
|
}
|
|
111
140
|
/**
|
|
112
|
-
* Every holder and collet family, with its facts checked and projected
|
|
141
|
+
* Every holder and collet family, with its facts checked and projected, and
|
|
142
|
+
* bound to the mapper its brand supplies for its kind.
|
|
113
143
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
144
|
+
* Their facts pass the same gate cutting-tool families' do: a taper or a
|
|
145
|
+
* clamping mode is a per-family constant no variant table states, exactly like
|
|
146
|
+
* a drill's flute count.
|
|
147
|
+
*
|
|
148
|
+
* **A family whose brand maps nothing binds `undefined` rather than throwing**,
|
|
149
|
+
* which is where this differs from {@link boundFamilies}. A cutting-tool family
|
|
150
|
+
* with no mapper is a catalog fault — nothing can be done with it — but a
|
|
151
|
+
* toolholding family with no mapper is the state every one of them was in until
|
|
152
|
+
* records existed, and it still scrapes, writes a CSV and checks a receipt.
|
|
153
|
+
* Refusing at bind time would take that away from every consumer that never
|
|
154
|
+
* asked for a record. {@link toHolding} is where the absence is reported, at
|
|
155
|
+
* the one call that cannot proceed without it.
|
|
117
156
|
*/
|
|
118
157
|
export function boundToolholding() {
|
|
119
158
|
if (toolholding !== null)
|
|
120
159
|
return toolholding;
|
|
121
160
|
const bound = new Map();
|
|
122
|
-
for (const [
|
|
161
|
+
for (const [kind, families_] of [
|
|
123
162
|
['holder', HOLDER_FAMILIES],
|
|
124
163
|
['collet', COLLET_FAMILIES],
|
|
125
164
|
]) {
|
|
126
165
|
for (const [name, cfg] of Object.entries(families_)) {
|
|
127
|
-
|
|
166
|
+
const mappers = HOLDING_ADAPTERS[familyBrand(cfg)];
|
|
167
|
+
bound.set(name, {
|
|
168
|
+
...project(kind, name, cfg),
|
|
169
|
+
kind,
|
|
170
|
+
records: mappers?.[kind],
|
|
171
|
+
});
|
|
128
172
|
}
|
|
129
173
|
}
|
|
130
174
|
toolholding = bound;
|
|
131
175
|
return bound;
|
|
132
176
|
}
|
|
177
|
+
/** One bound toolholding family by CSV name. */
|
|
178
|
+
export function boundHolding(name) {
|
|
179
|
+
const cfg = boundToolholding().get(name);
|
|
180
|
+
if (cfg === undefined) {
|
|
181
|
+
throw new ScraperConfigError(name, `unknown toolholding family (known: ${[...boundToolholding().keys()].sort().join(', ')})`);
|
|
182
|
+
}
|
|
183
|
+
return cfg;
|
|
184
|
+
}
|
|
133
185
|
/** One bound cutting-tool family by CSV name. */
|
|
134
186
|
export function boundFamily(name) {
|
|
135
187
|
const cfg = boundFamilies().get(name);
|
|
@@ -164,12 +216,101 @@ export function boundFamily(name) {
|
|
|
164
216
|
*
|
|
165
217
|
* `familyName` is the CSV filename the catalog is keyed by
|
|
166
218
|
* (`'harvey_endmill_025.csv'`), which is what `boundFamily` takes.
|
|
219
|
+
*
|
|
220
|
+
* ## One incomplete part does not end the family
|
|
221
|
+
*
|
|
222
|
+
* The rows are mapped together, so until 2026-09-01 every refusal was equally
|
|
223
|
+
* fatal — and the refusals are not equal. A part the vendor left a required
|
|
224
|
+
* cell blank on is one bad row among thousands of good ones; EMUGE-FRANKEN
|
|
225
|
+
* omits `overall length l₁` on roughly 175 of its 7,021 end mill variants, and
|
|
226
|
+
* both end mill families converted to nothing at all because of them.
|
|
227
|
+
*
|
|
228
|
+
* So an {@link IncompletePartError} is warned about and the row is dropped.
|
|
229
|
+
* **Nothing else is.** A cutting material with no mapping, a column a family
|
|
230
|
+
* stopped mapping, a response that changed shape — those say the vendor's
|
|
231
|
+
* vocabulary or this package's catalog has moved, and a scraper that skipped
|
|
232
|
+
* quietly past them would publish a catalog nobody checked. `columns.required`
|
|
233
|
+
* is the only place that raises the skippable one.
|
|
234
|
+
*
|
|
235
|
+
* A dropped row is **not** a relaxed contract. `records.RECORD_GEOMETRY` still
|
|
236
|
+
* says an end mill always has an `OAL`, and every record returned here still
|
|
237
|
+
* has one: the part without it becomes no record rather than a record with a
|
|
238
|
+
* hole. Where a vendor genuinely never publishes a field, `sometimes` is still
|
|
239
|
+
* the answer — a drill's `SIG` is that, and it stays that.
|
|
240
|
+
*
|
|
241
|
+
* The count of what was dropped is not returned. A caller that needs it has
|
|
242
|
+
* the row count it passed in and the length it got back, and the warnings name
|
|
243
|
+
* every part by number.
|
|
167
244
|
*/
|
|
168
245
|
export function toRecords(familyName, scrape, options) {
|
|
169
246
|
const cfg = boundFamily(familyName);
|
|
247
|
+
const warn = options?.warn ?? consoleWarn;
|
|
170
248
|
checkIdentityColumns(familyBrand(cfg), scrape.header);
|
|
171
249
|
checkColumnsExist(familyName, cfg, scrape.header);
|
|
172
|
-
|
|
250
|
+
const records = [];
|
|
251
|
+
for (const row of scrape.rows) {
|
|
252
|
+
try {
|
|
253
|
+
records.push(cfg.records(row, cfg, cfg.columns, options));
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
if (!(error instanceof IncompletePartError))
|
|
257
|
+
throw error;
|
|
258
|
+
warn(` WARNING: ${error.message} — no record written for it`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return records;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* One toolholding family's scrape, as {@link HoldingRecord}s.
|
|
265
|
+
*
|
|
266
|
+
* {@link toRecords}'s counterpart, and deliberately the same shape: the two
|
|
267
|
+
* checks run before the first row, one incomplete part does not end the family,
|
|
268
|
+
* and the count of what was dropped is not returned because the caller has the
|
|
269
|
+
* row count it passed in and the length it got back.
|
|
270
|
+
*
|
|
271
|
+
* **It refuses only where a caller asked for something this package cannot
|
|
272
|
+
* give.** A toolholding family whose brand maps no mapper binds one anyway
|
|
273
|
+
* (see {@link boundToolholding}) and scrapes exactly as it did before; this is
|
|
274
|
+
* the one call that cannot proceed without one, so this is where the absence is
|
|
275
|
+
* named — with the brand and with what that brand does map, the way
|
|
276
|
+
* {@link boundFamilies} names a missing tool mapper.
|
|
277
|
+
*
|
|
278
|
+
* `checkColumnsExist` has no counterpart here: a holder family carries no
|
|
279
|
+
* `ColumnMap`, because the columns a holder publishes are the vendor's own and
|
|
280
|
+
* are read by that vendor's mapper rather than through a canonical name. What
|
|
281
|
+
* does still run is {@link checkIdentityColumns}, which catches the failure that
|
|
282
|
+
* matters most — a re-scrape whose part-number column was renamed still parses,
|
|
283
|
+
* still has the right row count, and mints every guid off an empty string.
|
|
284
|
+
*/
|
|
285
|
+
export function toHolding(familyName, scrape, options) {
|
|
286
|
+
const cfg = boundHolding(familyName);
|
|
287
|
+
const warn = options?.warn ?? consoleWarn;
|
|
288
|
+
const brand = familyBrand(cfg);
|
|
289
|
+
const mapper = cfg.records;
|
|
290
|
+
if (mapper === undefined) {
|
|
291
|
+
const mappers = HOLDING_ADAPTERS[brand];
|
|
292
|
+
throw new ScraperConfigError(familyName, `brand ${JSON.stringify(brand)} has no ${cfg.kind} mapper ` +
|
|
293
|
+
(mappers === undefined
|
|
294
|
+
? `— it maps no toolholding at all, so this family ends at rows and ` +
|
|
295
|
+
`a receipt (brands that map some: ` +
|
|
296
|
+
`${Object.keys(HOLDING_ADAPTERS).sort().join(', ')})`
|
|
297
|
+
: `(it maps: ${Object.keys(mappers).sort().join(', ')})`));
|
|
298
|
+
}
|
|
299
|
+
checkIdentityColumns(brand, scrape.header);
|
|
300
|
+
const records = [];
|
|
301
|
+
for (const row of scrape.rows) {
|
|
302
|
+
try {
|
|
303
|
+
records.push(cfg.kind === 'holder'
|
|
304
|
+
? mapper(row, cfg, options)
|
|
305
|
+
: mapper(row, cfg, options));
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
if (!(error instanceof IncompletePartError))
|
|
309
|
+
throw error;
|
|
310
|
+
warn(` WARNING: ${error.message} — no record written for it`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return records;
|
|
173
314
|
}
|
|
174
315
|
/**
|
|
175
316
|
* 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
|
/**
|