@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.
- package/README.md +41 -11
- package/dist/columns.d.ts +62 -0
- package/dist/columns.js +62 -0
- package/dist/conventions.d.ts +89 -15
- package/dist/conventions.js +108 -15
- package/dist/families/emuge.d.ts +183 -0
- package/dist/families/emuge.js +159 -0
- package/dist/families/harvey.d.ts +1782 -0
- package/dist/families/harvey.js +1328 -0
- package/dist/families/index.js +5 -2
- package/dist/families/kennametal.d.ts +21 -0
- package/dist/families/kennametal.js +10 -0
- package/dist/families/maritool.d.ts +120 -0
- package/dist/families/maritool.js +175 -0
- package/dist/family.d.ts +35 -1
- package/dist/family.js +28 -0
- package/dist/identity.d.ts +18 -0
- package/dist/identity.js +46 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/measure.d.ts +112 -0
- package/dist/measure.js +130 -0
- package/dist/node/cli.d.ts +3 -0
- package/dist/node/cli.js +152 -2
- package/dist/node/main.js +0 -0
- package/dist/records.d.ts +169 -12
- package/dist/records.js +116 -5
- package/dist/registry.d.ts +31 -1
- package/dist/registry.js +40 -1
- package/dist/scrape.d.ts +15 -0
- package/dist/scrape.js +24 -0
- package/dist/vendors/destinytool/records.d.ts +13 -3
- package/dist/vendors/destinytool/records.js +41 -37
- package/dist/vendors/emuge/index.d.ts +17 -0
- package/dist/vendors/emuge/index.js +17 -0
- package/dist/vendors/emuge/records.d.ts +150 -0
- package/dist/vendors/emuge/records.js +375 -0
- package/dist/vendors/emuge/scrape.d.ts +227 -0
- package/dist/vendors/emuge/scrape.js +358 -0
- package/dist/vendors/emuge/value.d.ts +75 -0
- package/dist/vendors/emuge/value.js +116 -0
- package/dist/vendors/harvey/catalog.d.ts +53 -0
- package/dist/vendors/harvey/catalog.js +120 -0
- package/dist/vendors/harvey/header.d.ts +89 -0
- package/dist/vendors/harvey/header.js +185 -0
- package/dist/vendors/harvey/index.d.ts +21 -0
- package/dist/vendors/harvey/index.js +21 -0
- package/dist/vendors/harvey/lexicon.d.ts +73 -0
- package/dist/vendors/harvey/lexicon.js +126 -0
- package/dist/vendors/harvey/literal.d.ts +68 -0
- package/dist/vendors/harvey/literal.js +214 -0
- package/dist/vendors/harvey/records.d.ts +79 -0
- package/dist/vendors/harvey/records.js +163 -0
- package/dist/vendors/harvey/scrape.d.ts +187 -0
- package/dist/vendors/harvey/scrape.js +483 -0
- package/dist/vendors/harvey/value.d.ts +74 -0
- package/dist/vendors/harvey/value.js +119 -0
- package/dist/vendors/kennametal/records.d.ts +13 -4
- package/dist/vendors/kennametal/records.js +61 -26
- package/dist/vendors/maritool/catalog.d.ts +81 -0
- package/dist/vendors/maritool/catalog.js +132 -0
- package/dist/vendors/maritool/index.d.ts +10 -0
- package/dist/vendors/maritool/index.js +10 -0
- package/dist/vendors/maritool/scrape.d.ts +297 -0
- package/dist/vendors/maritool/scrape.js +593 -0
- package/dist/vendors/regofix/scrape.d.ts +8 -11
- package/dist/vendors/regofix/scrape.js +21 -36
- package/package.json +31 -7
package/dist/records.d.ts
CHANGED
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
* separately could supply a tool that claims a holder it does not have.
|
|
44
44
|
*/
|
|
45
45
|
import { type UnitSystem } from './conventions.js';
|
|
46
|
+
import { type BrandName } from './identity.js';
|
|
47
|
+
import type { FactSource } from './provenance.js';
|
|
46
48
|
/** The kinds of cutting tool this package maps. */
|
|
47
49
|
export type ToolKind = 'drill' | 'tap' | 'endmill';
|
|
48
50
|
/**
|
|
@@ -56,6 +58,35 @@ export type ToolKind = 'drill' | 'tap' | 'endmill';
|
|
|
56
58
|
* and a tool's own list from another has no way to notice the two disagree.
|
|
57
59
|
*/
|
|
58
60
|
export declare const ISO_MATERIAL_GROUPS: readonly ["P", "M", "K", "N", "S", "H", "C"];
|
|
61
|
+
/**
|
|
62
|
+
* The material-groups label for **we do not know what this tool is for**.
|
|
63
|
+
*
|
|
64
|
+
* Not an ISO 513 group and deliberately not one of {@link ISO_MATERIAL_GROUPS}:
|
|
65
|
+
* it is a statement about this package's evidence, not about the tool. It sits
|
|
66
|
+
* in {@link ToolRecord.materialGroupsSource} rather than in the group list,
|
|
67
|
+
* because a consumer filtering a catalog down to "cuts steel" must not have to
|
|
68
|
+
* know that one of the letters is not a letter.
|
|
69
|
+
*
|
|
70
|
+
* **Why a label and not a bare `null`.** The absence was expressible before —
|
|
71
|
+
* `materialGroups === null` — and an absence is the one thing a reader has to
|
|
72
|
+
* interpret. Every Harvey part is here, and so is every Kennametal family whose
|
|
73
|
+
* material sweep has not been run: two different reasons for the same honest
|
|
74
|
+
* answer, and neither is "rated for nothing". A named value says that out loud
|
|
75
|
+
* in a UI, a log line and a filter, which `null` never does.
|
|
76
|
+
*/
|
|
77
|
+
export declare const UNSPECIFIED = "unspecified";
|
|
78
|
+
/**
|
|
79
|
+
* How a record's material groups were arrived at.
|
|
80
|
+
*
|
|
81
|
+
* A fact's own three sources plus {@link UNSPECIFIED}, which provenance has no
|
|
82
|
+
* word for because a `Fact` only exists once somebody has stated something. A
|
|
83
|
+
* record exists either way, so it needs the fourth.
|
|
84
|
+
*
|
|
85
|
+
* Only `vendor-stated`, `derived` and `unspecified` occur today; `assumed` is
|
|
86
|
+
* reachable the moment a family fact supplies the groups, and is kept in the
|
|
87
|
+
* type so that the vocabularies cannot drift apart.
|
|
88
|
+
*/
|
|
89
|
+
export type MaterialGroupsSource = FactSource | typeof UNSPECIFIED;
|
|
59
90
|
/** One canonical geometry name: what it measures, and whose name it is. */
|
|
60
91
|
export interface GeometryField {
|
|
61
92
|
/**
|
|
@@ -186,6 +217,41 @@ export declare const DIMENSIONAL: ReadonlySet<GeometryName>;
|
|
|
186
217
|
* suffix would look for a column that was never scraped.
|
|
187
218
|
*/
|
|
188
219
|
export declare const DIMENSIONAL_COLUMNS: ReadonlySet<GeometryName>;
|
|
220
|
+
/**
|
|
221
|
+
* What geometry a **record** of each kind carries, as against what a family
|
|
222
|
+
* must map.
|
|
223
|
+
*
|
|
224
|
+
* {@link REQUIRED_GEOMETRY} is about columns: it refuses a family whose config
|
|
225
|
+
* maps no `LCF`. This is about the record that comes out the other end, and the
|
|
226
|
+
* two genuinely differ — a Kennametal drill's `NOF` and `SIG` come from facts
|
|
227
|
+
* and not from any column, so they can never appear in a column map and are
|
|
228
|
+
* always on the record.
|
|
229
|
+
*
|
|
230
|
+
* **`sometimes` is the point of the table.** An absent key was the one thing in
|
|
231
|
+
* a record a reader had to interpret: `geometry.NOF === undefined` means "Harvey
|
|
232
|
+
* publishes no flute count for this family" on an end mill and "not part of the
|
|
233
|
+
* contract" on a drill, and nothing said which. That is exactly the ambiguity
|
|
234
|
+
* {@link UNSPECIFIED} exists to remove from {@link ToolRecord.materialGroups},
|
|
235
|
+
* and the record shipped both encodings at once. Now the absence is declared:
|
|
236
|
+
* a key in `sometimes` may be missing and its absence is the vendor's silence;
|
|
237
|
+
* a key in neither list is not part of that kind's record at all.
|
|
238
|
+
*
|
|
239
|
+
* Both `sometimes` entries today are a flute count nobody publishes:
|
|
240
|
+
*
|
|
241
|
+
* - the **end mill's**, for Harvey's two deburring families — they publish
|
|
242
|
+
* right- and left-hand tooth counts and no flute count, so there is nothing
|
|
243
|
+
* to read and 0 is not a substitute;
|
|
244
|
+
* - the **tap's**, for EMUGE-FRANKEN, which states no flute count anywhere a
|
|
245
|
+
* scrape can reach — not on the grouped product, the variant listing, the
|
|
246
|
+
* per-part detail record or any facet — while its own tap families run 2, 3
|
|
247
|
+
* and 4 flutes across their size range, so no per-family constant could be
|
|
248
|
+
* true of every row. Kennametal's taps publish a `Z` column and keep filling
|
|
249
|
+
* it: `sometimes` permits the key, it does not forbid it.
|
|
250
|
+
*/
|
|
251
|
+
export declare const RECORD_GEOMETRY: Record<ToolKind, {
|
|
252
|
+
readonly always: readonly GeometryName[];
|
|
253
|
+
readonly sometimes: readonly GeometryName[];
|
|
254
|
+
}>;
|
|
189
255
|
/**
|
|
190
256
|
* One orderable cutting tool, in canonical fields, ready for the core.
|
|
191
257
|
*
|
|
@@ -193,23 +259,103 @@ export declare const DIMENSIONAL_COLUMNS: ReadonlySet<GeometryName>;
|
|
|
193
259
|
* it and hands it over, and a mapper that mutated one would be reaching back
|
|
194
260
|
* across the seam this type exists to draw.
|
|
195
261
|
*
|
|
196
|
-
* `geometry` holds {@link GEOMETRY_FIELDS} names in `unit`.
|
|
197
|
-
*
|
|
198
|
-
* material
|
|
199
|
-
*
|
|
262
|
+
* `geometry` holds {@link GEOMETRY_FIELDS} names in `unit`.
|
|
263
|
+
*
|
|
264
|
+
* ## The workpiece-material groups have three states, not two
|
|
265
|
+
*
|
|
266
|
+
* They were `string[]` until this contract, and an empty array carried two
|
|
267
|
+
* incompatible claims at once. Kennametal's 129 taps are empty because the
|
|
268
|
+
* vendor's own index rates them for nothing; every Harvey part was empty
|
|
269
|
+
* because Harvey publishes no index a scrape can reach, and a Kennametal family
|
|
270
|
+
* whose material sweep was never run was empty for a third reason. A consumer
|
|
271
|
+
* reading the first as "not rated for steel" is right and reading the second
|
|
272
|
+
* the same way is wrong, and nothing in the record told the two apart.
|
|
273
|
+
*
|
|
274
|
+
* So:
|
|
275
|
+
*
|
|
276
|
+
* - **`null`, labelled {@link UNSPECIFIED}** — we do not know what this tool is
|
|
277
|
+
* for. Not indexed, not published, or not swept. Says nothing about what it
|
|
278
|
+
* cuts, and is not a claim that it cuts nothing.
|
|
279
|
+
* - **`[]`** — the vendor's index exists and rates this part for nothing.
|
|
280
|
+
* - **non-empty** — rated, in {@link ISO_MATERIAL_GROUPS} order.
|
|
281
|
+
*
|
|
282
|
+
* {@link ToolRecord.materialGroupsSource} is never absent: it is `unspecified`
|
|
283
|
+
* in the first case and, in the other two, how the answer was arrived at in the
|
|
284
|
+
* same vocabulary a family fact uses — `vendor-stated` where the vendor's own
|
|
285
|
+
* index said so, `derived` where this package computed it. A consumer that will
|
|
286
|
+
* not act on a guess filters on it, and one that will not show an unknown as an
|
|
287
|
+
* empty facet filters on it too.
|
|
200
288
|
*/
|
|
201
289
|
export interface ToolRecord {
|
|
290
|
+
/**
|
|
291
|
+
* The brand key the record was minted under — `identity.BRANDS`'s own key,
|
|
292
|
+
* not the display name.
|
|
293
|
+
*
|
|
294
|
+
* Here because {@link ToolRecord.guid} is the join key for every downstream
|
|
295
|
+
* consumer and it is minted in this brand's namespace: without the key on
|
|
296
|
+
* the record, the guid is underivable from the record and `vendor` is a
|
|
297
|
+
* display string nothing can look a namespace up by.
|
|
298
|
+
*/
|
|
299
|
+
readonly brand: BrandName;
|
|
300
|
+
/** What this brand's records call the vendor — `identity.BRANDS[brand].vendor`. */
|
|
202
301
|
readonly vendor: string;
|
|
302
|
+
/**
|
|
303
|
+
* `identity.recordGuid(brand, materialNumber)`, minted by {@link toolRecord}
|
|
304
|
+
* rather than by an adapter: one mint path is what makes a guid collision
|
|
305
|
+
* across brands structurally impossible instead of merely unlikely.
|
|
306
|
+
*/
|
|
307
|
+
readonly guid: string;
|
|
203
308
|
readonly materialNumber: string;
|
|
204
309
|
readonly catalogNumber: string;
|
|
310
|
+
/**
|
|
311
|
+
* The vendor's own free text about this part, verbatim — `''` where the
|
|
312
|
+
* vendor publishes none.
|
|
313
|
+
*
|
|
314
|
+
* **Never a copy of another field on this record.** It was
|
|
315
|
+
* `row['ISO Catalog Number']` on every Kennametal drill and end mill until
|
|
316
|
+
* 2026-08-29, which put the catalog number in two fields and told a consumer
|
|
317
|
+
* nothing it did not already have: a search index built on it matched a part
|
|
318
|
+
* number and no words. Kennametal publishes no description column, so the
|
|
319
|
+
* honest answer is the empty string — the same rule {@link ToolRecord.coating}
|
|
320
|
+
* already states for a table that publishes no coating.
|
|
321
|
+
*
|
|
322
|
+
* **It may be per product line rather than per part.** Harvey states one
|
|
323
|
+
* title for a whole page and no per-part text, so every record of a Harvey
|
|
324
|
+
* family carries that family's title. That is what the vendor published; a
|
|
325
|
+
* consumer that needs a per-part string has `catalogNumber`.
|
|
326
|
+
*/
|
|
205
327
|
readonly description: string;
|
|
206
328
|
readonly kind: ToolKind;
|
|
207
329
|
readonly unit: UnitSystem;
|
|
208
330
|
readonly substrate: string;
|
|
209
|
-
|
|
210
|
-
|
|
331
|
+
/**
|
|
332
|
+
* The vendor's own coating string, `''` where the table publishes none.
|
|
333
|
+
*
|
|
334
|
+
* Raw, and never inferred from: `AlTiN COATED`, `TiN` and a Destiny Tool
|
|
335
|
+
* coating id are three vendors' vocabularies, and mapping them onto a shared
|
|
336
|
+
* one here would be this package inventing a standard rather than recording
|
|
337
|
+
* what was published.
|
|
338
|
+
*/
|
|
339
|
+
readonly coating: string;
|
|
340
|
+
/**
|
|
341
|
+
* The canonical geometry, in {@link ToolRecord.unit}.
|
|
342
|
+
*
|
|
343
|
+
* Numbers only. It was `number | boolean` until 2026-08-29 and no adapter has
|
|
344
|
+
* ever put a boolean in one — {@link GEOMETRY_FIELDS} defines no boolean
|
|
345
|
+
* field — so the width bought nothing and cost every consumer a narrowing
|
|
346
|
+
* before it could do arithmetic on `geometry.DC`.
|
|
347
|
+
*
|
|
348
|
+
* **Which keys are present is stated per kind, not left to the mapper**: see
|
|
349
|
+
* {@link RECORD_GEOMETRY}. An absent key is a claim, and it is the vendor's
|
|
350
|
+
* silence rather than a gap — the same distinction
|
|
351
|
+
* {@link ToolRecord.materialGroups} draws with {@link UNSPECIFIED}.
|
|
352
|
+
*/
|
|
353
|
+
readonly geometry: Readonly<Partial<Record<GeometryName, number>>>;
|
|
211
354
|
readonly coolantThrough: boolean;
|
|
212
|
-
|
|
355
|
+
/** ISO 513 main groups in {@link ISO_MATERIAL_GROUPS} order — see above. */
|
|
356
|
+
readonly materialGroups: readonly string[] | null;
|
|
357
|
+
/** How the groups were arrived at. {@link UNSPECIFIED} exactly when they are null. */
|
|
358
|
+
readonly materialGroupsSource: MaterialGroupsSource;
|
|
213
359
|
/**
|
|
214
360
|
* Drills only, and deliberately `null` elsewhere rather than `false`: it
|
|
215
361
|
* drops the two ferrous presets downstream, and a default would ship them on
|
|
@@ -218,20 +364,31 @@ export interface ToolRecord {
|
|
|
218
364
|
readonly nonFerrous: boolean | null;
|
|
219
365
|
}
|
|
220
366
|
/**
|
|
221
|
-
* Build a {@link ToolRecord},
|
|
222
|
-
* absence.
|
|
367
|
+
* Build a {@link ToolRecord}: mint its guid, default the fields that have a
|
|
368
|
+
* meaningful absence, and refuse the one state that cannot be true.
|
|
223
369
|
*
|
|
224
370
|
* An interface cannot carry a default, and requiring every adapter to write
|
|
225
|
-
* `materialGroups:
|
|
371
|
+
* `materialGroups: null` would put the decision back in the three places least
|
|
226
372
|
* able to notice it was wrong. They stay **required on the type** so a
|
|
227
373
|
* consumer reading a record never handles `undefined` — only the construction
|
|
228
|
-
* is optional.
|
|
374
|
+
* is optional. The default is `null`/{@link UNSPECIFIED} and not `[]`, because
|
|
375
|
+
* an adapter that says nothing about workpiece materials has produced no
|
|
376
|
+
* evidence, which is the one thing the three-state rule exists to distinguish.
|
|
377
|
+
*
|
|
378
|
+
* `guid` is not an input at all. Every adapter minting it would be three
|
|
379
|
+
* copies of `recordGuid(brand, materialNumber)` to drift, on the value that is
|
|
380
|
+
* the join key for every downstream consumer.
|
|
381
|
+
*
|
|
382
|
+
* The geometry is checked against {@link RECORD_GEOMETRY} before anything is
|
|
383
|
+
* built, so a kind's shape is one table rather than a convention three mappers
|
|
384
|
+
* each keep separately. That is the same move the material-groups invariant
|
|
385
|
+
* below makes: the factory is where a record's shape can be refused once.
|
|
229
386
|
*
|
|
230
387
|
* The result is frozen, geometry and material groups included: a record is an
|
|
231
388
|
* interchange value, and a mapper that mutated one would be reaching back
|
|
232
389
|
* across the seam this type exists to draw.
|
|
233
390
|
*/
|
|
234
|
-
export declare function toolRecord(fields: Omit<ToolRecord, 'materialGroups' | 'nonFerrous'> & Partial<Pick<ToolRecord, 'materialGroups' | 'nonFerrous'>>): ToolRecord;
|
|
391
|
+
export declare function toolRecord(fields: Omit<ToolRecord, 'guid' | 'materialGroups' | 'materialGroupsSource' | 'nonFerrous'> & Partial<Pick<ToolRecord, 'materialGroups' | 'materialGroupsSource' | 'nonFerrous'>>): ToolRecord;
|
|
235
392
|
/**
|
|
236
393
|
* A family's canonical-field → CSV-column-label mapping, validated.
|
|
237
394
|
*
|
package/dist/records.js
CHANGED
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
*/
|
|
45
45
|
import { dimensionalColumn } from './conventions.js';
|
|
46
46
|
import { ScraperConfigError } from './errors.js';
|
|
47
|
+
import { recordGuid } from './identity.js';
|
|
47
48
|
/**
|
|
48
49
|
* ISO 513's main workpiece-material groups, in the order every vendor's
|
|
49
50
|
* material groups must agree on — **core, not a Kennametal fact**, the same
|
|
@@ -55,6 +56,23 @@ import { ScraperConfigError } from './errors.js';
|
|
|
55
56
|
* and a tool's own list from another has no way to notice the two disagree.
|
|
56
57
|
*/
|
|
57
58
|
export const ISO_MATERIAL_GROUPS = ['P', 'M', 'K', 'N', 'S', 'H', 'C'];
|
|
59
|
+
/**
|
|
60
|
+
* The material-groups label for **we do not know what this tool is for**.
|
|
61
|
+
*
|
|
62
|
+
* Not an ISO 513 group and deliberately not one of {@link ISO_MATERIAL_GROUPS}:
|
|
63
|
+
* it is a statement about this package's evidence, not about the tool. It sits
|
|
64
|
+
* in {@link ToolRecord.materialGroupsSource} rather than in the group list,
|
|
65
|
+
* because a consumer filtering a catalog down to "cuts steel" must not have to
|
|
66
|
+
* know that one of the letters is not a letter.
|
|
67
|
+
*
|
|
68
|
+
* **Why a label and not a bare `null`.** The absence was expressible before —
|
|
69
|
+
* `materialGroups === null` — and an absence is the one thing a reader has to
|
|
70
|
+
* interpret. Every Harvey part is here, and so is every Kennametal family whose
|
|
71
|
+
* material sweep has not been run: two different reasons for the same honest
|
|
72
|
+
* answer, and neither is "rated for nothing". A named value says that out loud
|
|
73
|
+
* in a UI, a log line and a filter, which `null` never does.
|
|
74
|
+
*/
|
|
75
|
+
export const UNSPECIFIED = 'unspecified';
|
|
58
76
|
/**
|
|
59
77
|
* Canonical geometry fields an adapter may supply, and what each means.
|
|
60
78
|
*
|
|
@@ -158,24 +176,117 @@ export const DIMENSIONAL = new Set([
|
|
|
158
176
|
*/
|
|
159
177
|
export const DIMENSIONAL_COLUMNS = new Set([...DIMENSIONAL].filter((name) => name !== 'TP'));
|
|
160
178
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
179
|
+
* What geometry a **record** of each kind carries, as against what a family
|
|
180
|
+
* must map.
|
|
181
|
+
*
|
|
182
|
+
* {@link REQUIRED_GEOMETRY} is about columns: it refuses a family whose config
|
|
183
|
+
* maps no `LCF`. This is about the record that comes out the other end, and the
|
|
184
|
+
* two genuinely differ — a Kennametal drill's `NOF` and `SIG` come from facts
|
|
185
|
+
* and not from any column, so they can never appear in a column map and are
|
|
186
|
+
* always on the record.
|
|
187
|
+
*
|
|
188
|
+
* **`sometimes` is the point of the table.** An absent key was the one thing in
|
|
189
|
+
* a record a reader had to interpret: `geometry.NOF === undefined` means "Harvey
|
|
190
|
+
* publishes no flute count for this family" on an end mill and "not part of the
|
|
191
|
+
* contract" on a drill, and nothing said which. That is exactly the ambiguity
|
|
192
|
+
* {@link UNSPECIFIED} exists to remove from {@link ToolRecord.materialGroups},
|
|
193
|
+
* and the record shipped both encodings at once. Now the absence is declared:
|
|
194
|
+
* a key in `sometimes` may be missing and its absence is the vendor's silence;
|
|
195
|
+
* a key in neither list is not part of that kind's record at all.
|
|
196
|
+
*
|
|
197
|
+
* Both `sometimes` entries today are a flute count nobody publishes:
|
|
198
|
+
*
|
|
199
|
+
* - the **end mill's**, for Harvey's two deburring families — they publish
|
|
200
|
+
* right- and left-hand tooth counts and no flute count, so there is nothing
|
|
201
|
+
* to read and 0 is not a substitute;
|
|
202
|
+
* - the **tap's**, for EMUGE-FRANKEN, which states no flute count anywhere a
|
|
203
|
+
* scrape can reach — not on the grouped product, the variant listing, the
|
|
204
|
+
* per-part detail record or any facet — while its own tap families run 2, 3
|
|
205
|
+
* and 4 flutes across their size range, so no per-family constant could be
|
|
206
|
+
* true of every row. Kennametal's taps publish a `Z` column and keep filling
|
|
207
|
+
* it: `sometimes` permits the key, it does not forbid it.
|
|
208
|
+
*/
|
|
209
|
+
export const RECORD_GEOMETRY = {
|
|
210
|
+
drill: {
|
|
211
|
+
always: ['DC', 'SFDM', 'OAL', 'LCF', 'NOF', 'SIG'],
|
|
212
|
+
sometimes: [],
|
|
213
|
+
},
|
|
214
|
+
tap: {
|
|
215
|
+
always: ['DC', 'TP', 'SFDM', 'OAL', 'LCF'],
|
|
216
|
+
sometimes: ['NOF'],
|
|
217
|
+
},
|
|
218
|
+
endmill: {
|
|
219
|
+
always: ['DC', 'RE', 'SFDM', 'OAL', 'LCF', 'shoulder-length', 'shoulder-diameter'],
|
|
220
|
+
sometimes: ['NOF'],
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
/**
|
|
224
|
+
* Refuse a record whose geometry does not match its kind's declared shape.
|
|
225
|
+
*
|
|
226
|
+
* Two failures, and the second is the one worth having: a key the kind does not
|
|
227
|
+
* declare at all means a mapper is writing a measurement into a record nothing
|
|
228
|
+
* downstream expects to find there, which is invisible until a consumer does
|
|
229
|
+
* not read it.
|
|
230
|
+
*/
|
|
231
|
+
function checkGeometry(kind, what, geometry) {
|
|
232
|
+
const { always, sometimes } = RECORD_GEOMETRY[kind];
|
|
233
|
+
const present = new Set(Object.keys(geometry));
|
|
234
|
+
const missing = always.filter((name) => !present.has(name));
|
|
235
|
+
if (missing.length > 0) {
|
|
236
|
+
throw new ScraperConfigError(what, `a ${kind} record carries ${missing.join(', ')} and this one does not — ` +
|
|
237
|
+
`a field a kind always has is not a field a mapper may skip`);
|
|
238
|
+
}
|
|
239
|
+
const declared = new Set([...always, ...sometimes]);
|
|
240
|
+
const extra = [...present].filter((name) => !declared.has(name)).sort();
|
|
241
|
+
if (extra.length > 0) {
|
|
242
|
+
throw new ScraperConfigError(what, `a ${kind} record does not carry ${extra.join(', ')} — ` +
|
|
243
|
+
`add it to RECORD_GEOMETRY before a mapper writes one`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Build a {@link ToolRecord}: mint its guid, default the fields that have a
|
|
248
|
+
* meaningful absence, and refuse the one state that cannot be true.
|
|
163
249
|
*
|
|
164
250
|
* An interface cannot carry a default, and requiring every adapter to write
|
|
165
|
-
* `materialGroups:
|
|
251
|
+
* `materialGroups: null` would put the decision back in the three places least
|
|
166
252
|
* able to notice it was wrong. They stay **required on the type** so a
|
|
167
253
|
* consumer reading a record never handles `undefined` — only the construction
|
|
168
|
-
* is optional.
|
|
254
|
+
* is optional. The default is `null`/{@link UNSPECIFIED} and not `[]`, because
|
|
255
|
+
* an adapter that says nothing about workpiece materials has produced no
|
|
256
|
+
* evidence, which is the one thing the three-state rule exists to distinguish.
|
|
257
|
+
*
|
|
258
|
+
* `guid` is not an input at all. Every adapter minting it would be three
|
|
259
|
+
* copies of `recordGuid(brand, materialNumber)` to drift, on the value that is
|
|
260
|
+
* the join key for every downstream consumer.
|
|
261
|
+
*
|
|
262
|
+
* The geometry is checked against {@link RECORD_GEOMETRY} before anything is
|
|
263
|
+
* built, so a kind's shape is one table rather than a convention three mappers
|
|
264
|
+
* each keep separately. That is the same move the material-groups invariant
|
|
265
|
+
* below makes: the factory is where a record's shape can be refused once.
|
|
169
266
|
*
|
|
170
267
|
* The result is frozen, geometry and material groups included: a record is an
|
|
171
268
|
* interchange value, and a mapper that mutated one would be reaching back
|
|
172
269
|
* across the seam this type exists to draw.
|
|
173
270
|
*/
|
|
174
271
|
export function toolRecord(fields) {
|
|
272
|
+
const groups = fields.materialGroups ?? null;
|
|
273
|
+
const source = fields.materialGroupsSource ?? UNSPECIFIED;
|
|
274
|
+
// The invariant is what keeps the three states three: groups labelled
|
|
275
|
+
// `unspecified` are groups nobody stated, and an attributed source with no
|
|
276
|
+
// groups attributes nothing. Either would land as a record whose material
|
|
277
|
+
// index reads as the opposite of what the adapter meant.
|
|
278
|
+
if ((groups === null) !== (source === UNSPECIFIED)) {
|
|
279
|
+
throw new ScraperConfigError(fields.materialNumber, `materialGroups is ${groups === null ? 'null' : JSON.stringify([...groups])} ` +
|
|
280
|
+
`and materialGroupsSource is ${JSON.stringify(source)} — ` +
|
|
281
|
+
`groups are ${JSON.stringify(UNSPECIFIED)} exactly when there are none`);
|
|
282
|
+
}
|
|
283
|
+
checkGeometry(fields.kind, fields.materialNumber, fields.geometry);
|
|
175
284
|
return Object.freeze({
|
|
176
285
|
...fields,
|
|
286
|
+
guid: recordGuid(fields.brand, fields.materialNumber),
|
|
177
287
|
geometry: Object.freeze({ ...fields.geometry }),
|
|
178
|
-
materialGroups: Object.freeze([...
|
|
288
|
+
materialGroups: groups === null ? null : Object.freeze([...groups]),
|
|
289
|
+
materialGroupsSource: source,
|
|
179
290
|
nonFerrous: fields.nonFerrous ?? null,
|
|
180
291
|
});
|
|
181
292
|
}
|
package/dist/registry.d.ts
CHANGED
|
@@ -26,7 +26,9 @@
|
|
|
26
26
|
* key. What changes is only *when* — first use rather than import — and every
|
|
27
27
|
* entry point into this package goes through here.
|
|
28
28
|
*/
|
|
29
|
-
import type
|
|
29
|
+
import { type BoundFamily, type BoundToolholding, type RecordMappers } from './family.js';
|
|
30
|
+
import { type ToolRecord } from './records.js';
|
|
31
|
+
import type { MapperOptions, ScrapeResult } from './scrape.js';
|
|
30
32
|
/**
|
|
31
33
|
* Brand -> its row-to-record mappers, by tool kind.
|
|
32
34
|
*
|
|
@@ -53,6 +55,34 @@ export declare function boundFamilies(): Map<string, BoundFamily>;
|
|
|
53
55
|
export declare function boundToolholding(): Map<string, BoundToolholding>;
|
|
54
56
|
/** One bound cutting-tool family by CSV name. */
|
|
55
57
|
export declare function boundFamily(name: string): BoundFamily;
|
|
58
|
+
/**
|
|
59
|
+
* One family's scrape, as {@link ToolRecord}s — the package's uniform output.
|
|
60
|
+
*
|
|
61
|
+
* **This is what a consumer wants and what nothing shipped until now.** Every
|
|
62
|
+
* CLI command ends at a vendor-labelled CSV, and a CSV is the receipt: four
|
|
63
|
+
* vendors, four column vocabularies, and a `D1_mm` that means one thing in
|
|
64
|
+
* Kennametal's table and another in ISO 13399's dictionary. The adapters that
|
|
65
|
+
* resolve that have been here the whole time and only the tests called them.
|
|
66
|
+
*
|
|
67
|
+
* It lives in the registry rather than beside `toolRecord` because it needs
|
|
68
|
+
* both halves — the config table and the vendor mappers — and the main entry
|
|
69
|
+
* point deliberately imports no vendor. Reach it through the `./registry`
|
|
70
|
+
* subpath.
|
|
71
|
+
*
|
|
72
|
+
* The two checks run **before the first row**, in the order a failure is
|
|
73
|
+
* cheapest to read:
|
|
74
|
+
*
|
|
75
|
+
* 1. {@link checkIdentityColumns} — a re-scrape whose part-number column was
|
|
76
|
+
* renamed still parses, still has the right row count, and mints every guid
|
|
77
|
+
* off an empty string.
|
|
78
|
+
* 2. {@link checkColumnsExist} — a mapped column the CSV does not carry names
|
|
79
|
+
* the family and the field here, instead of naming one row out of ninety-three
|
|
80
|
+
* from inside a mapper.
|
|
81
|
+
*
|
|
82
|
+
* `familyName` is the CSV filename the catalog is keyed by
|
|
83
|
+
* (`'harvey_endmill_025.csv'`), which is what `boundFamily` takes.
|
|
84
|
+
*/
|
|
85
|
+
export declare function toRecords(familyName: string, scrape: ScrapeResult, options?: MapperOptions): ToolRecord[];
|
|
56
86
|
/**
|
|
57
87
|
* Forget what has been bound.
|
|
58
88
|
*
|
package/dist/registry.js
CHANGED
|
@@ -26,11 +26,15 @@
|
|
|
26
26
|
* key. What changes is only *when* — first use rather than import — and every
|
|
27
27
|
* entry point into this package goes through here.
|
|
28
28
|
*/
|
|
29
|
+
import { checkIdentityColumns } from './conventions.js';
|
|
30
|
+
import { familyBrand, } from './family.js';
|
|
29
31
|
import { COLLET_FAMILIES, FAMILIES, HOLDER_FAMILIES } from './families/index.js';
|
|
30
32
|
import { ScraperConfigError } from './errors.js';
|
|
31
33
|
import { checkFact } from './provenance.js';
|
|
32
|
-
import { checkColumnMap } from './records.js';
|
|
34
|
+
import { checkColumnMap, checkColumnsExist } from './records.js';
|
|
33
35
|
import { RECORD_MAPPERS as DESTINYTOOL } from './vendors/destinytool/records.js';
|
|
36
|
+
import { RECORD_MAPPERS as EMUGE } from './vendors/emuge/records.js';
|
|
37
|
+
import { RECORD_MAPPERS as HARVEY } from './vendors/harvey/records.js';
|
|
34
38
|
import { RECORD_MAPPERS as KENNAMETAL } from './vendors/kennametal/records.js';
|
|
35
39
|
/**
|
|
36
40
|
* Brand -> its row-to-record mappers, by tool kind.
|
|
@@ -45,6 +49,8 @@ export const ADAPTERS = {
|
|
|
45
49
|
kennametal: KENNAMETAL,
|
|
46
50
|
widia: KENNAMETAL,
|
|
47
51
|
destinytool: DESTINYTOOL,
|
|
52
|
+
harvey: HARVEY,
|
|
53
|
+
emuge: EMUGE,
|
|
48
54
|
};
|
|
49
55
|
/**
|
|
50
56
|
* Check every fact of one family and project its value onto the config under
|
|
@@ -132,6 +138,39 @@ export function boundFamily(name) {
|
|
|
132
138
|
}
|
|
133
139
|
return cfg;
|
|
134
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* One family's scrape, as {@link ToolRecord}s — the package's uniform output.
|
|
143
|
+
*
|
|
144
|
+
* **This is what a consumer wants and what nothing shipped until now.** Every
|
|
145
|
+
* CLI command ends at a vendor-labelled CSV, and a CSV is the receipt: four
|
|
146
|
+
* vendors, four column vocabularies, and a `D1_mm` that means one thing in
|
|
147
|
+
* Kennametal's table and another in ISO 13399's dictionary. The adapters that
|
|
148
|
+
* resolve that have been here the whole time and only the tests called them.
|
|
149
|
+
*
|
|
150
|
+
* It lives in the registry rather than beside `toolRecord` because it needs
|
|
151
|
+
* both halves — the config table and the vendor mappers — and the main entry
|
|
152
|
+
* point deliberately imports no vendor. Reach it through the `./registry`
|
|
153
|
+
* subpath.
|
|
154
|
+
*
|
|
155
|
+
* The two checks run **before the first row**, in the order a failure is
|
|
156
|
+
* cheapest to read:
|
|
157
|
+
*
|
|
158
|
+
* 1. {@link checkIdentityColumns} — a re-scrape whose part-number column was
|
|
159
|
+
* renamed still parses, still has the right row count, and mints every guid
|
|
160
|
+
* off an empty string.
|
|
161
|
+
* 2. {@link checkColumnsExist} — a mapped column the CSV does not carry names
|
|
162
|
+
* the family and the field here, instead of naming one row out of ninety-three
|
|
163
|
+
* from inside a mapper.
|
|
164
|
+
*
|
|
165
|
+
* `familyName` is the CSV filename the catalog is keyed by
|
|
166
|
+
* (`'harvey_endmill_025.csv'`), which is what `boundFamily` takes.
|
|
167
|
+
*/
|
|
168
|
+
export function toRecords(familyName, scrape, options) {
|
|
169
|
+
const cfg = boundFamily(familyName);
|
|
170
|
+
checkIdentityColumns(familyBrand(cfg), scrape.header);
|
|
171
|
+
checkColumnsExist(familyName, cfg, scrape.header);
|
|
172
|
+
return scrape.rows.map((row) => cfg.records(row, cfg, cfg.columns, options));
|
|
173
|
+
}
|
|
135
174
|
/**
|
|
136
175
|
* Forget what has been bound.
|
|
137
176
|
*
|
package/dist/scrape.d.ts
CHANGED
|
@@ -49,6 +49,21 @@ export interface MapperOptions {
|
|
|
49
49
|
}
|
|
50
50
|
/** The default: stderr, where a message nobody injected a sink for belongs. */
|
|
51
51
|
export declare const consoleWarn: Warn;
|
|
52
|
+
/**
|
|
53
|
+
* Rows to the header they imply: the union of their keys, in first-seen order.
|
|
54
|
+
*
|
|
55
|
+
* A union rather than the first row's keys, and the reason is the same for both
|
|
56
|
+
* vendors that build a `ScrapeResult` this way. A REGO-FIX collet family is
|
|
57
|
+
* mixed-unit — `D1_mm` on its metric rows and `D1_in` on its inch ones — and a
|
|
58
|
+
* MariTool CSV mixes three holder styles, so which spec keys a part publishes is
|
|
59
|
+
* a function of its style. Keying off row one drops whichever came second.
|
|
60
|
+
*
|
|
61
|
+
* It lived in both adapters, byte for byte, until 2026-08-29. Building the
|
|
62
|
+
* header of a {@link ScrapeResult} is this module's business rather than any
|
|
63
|
+
* manufacturer's — the same call `pause` and `conventions.CAD_COLUMN` already
|
|
64
|
+
* got, and the one `tests/vendor-boundary.test.ts` now makes automatically.
|
|
65
|
+
*/
|
|
66
|
+
export declare function unionHeader(rows: readonly ScrapedRow[]): string[];
|
|
52
67
|
/**
|
|
53
68
|
* Milliseconds between requests, wherever a scrape or a mirror loops.
|
|
54
69
|
*
|
package/dist/scrape.js
CHANGED
|
@@ -13,6 +13,30 @@
|
|
|
13
13
|
export const consoleWarn = (message) => {
|
|
14
14
|
console.warn(message);
|
|
15
15
|
};
|
|
16
|
+
/**
|
|
17
|
+
* Rows to the header they imply: the union of their keys, in first-seen order.
|
|
18
|
+
*
|
|
19
|
+
* A union rather than the first row's keys, and the reason is the same for both
|
|
20
|
+
* vendors that build a `ScrapeResult` this way. A REGO-FIX collet family is
|
|
21
|
+
* mixed-unit — `D1_mm` on its metric rows and `D1_in` on its inch ones — and a
|
|
22
|
+
* MariTool CSV mixes three holder styles, so which spec keys a part publishes is
|
|
23
|
+
* a function of its style. Keying off row one drops whichever came second.
|
|
24
|
+
*
|
|
25
|
+
* It lived in both adapters, byte for byte, until 2026-08-29. Building the
|
|
26
|
+
* header of a {@link ScrapeResult} is this module's business rather than any
|
|
27
|
+
* manufacturer's — the same call `pause` and `conventions.CAD_COLUMN` already
|
|
28
|
+
* got, and the one `tests/vendor-boundary.test.ts` now makes automatically.
|
|
29
|
+
*/
|
|
30
|
+
export function unionHeader(rows) {
|
|
31
|
+
const header = [];
|
|
32
|
+
for (const row of rows) {
|
|
33
|
+
for (const key of Object.keys(row)) {
|
|
34
|
+
if (!header.includes(key))
|
|
35
|
+
header.push(key);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return header;
|
|
39
|
+
}
|
|
16
40
|
/**
|
|
17
41
|
* Milliseconds between requests, wherever a scrape or a mirror loops.
|
|
18
42
|
*
|
|
@@ -94,6 +94,13 @@ export declare function shoulderDiameter(description: string, dc: number): numbe
|
|
|
94
94
|
* column when populated, or a fallback keyed on flute count when it is not
|
|
95
95
|
* (blank on 423 of 3,898 rows, 2026-08-19).
|
|
96
96
|
*
|
|
97
|
+
* **The two answers are labelled, not blended.** A populated cell is
|
|
98
|
+
* `vendor-stated` and the flute-count fallback is `derived`, so a consumer that
|
|
99
|
+
* will not route a cut off this package's arithmetic can filter on the source
|
|
100
|
+
* rather than having to know which rows Destiny Tool left blank. Neither is
|
|
101
|
+
* ever `null`: the fallback covers every blank cell, so this vendor always has
|
|
102
|
+
* an answer.
|
|
103
|
+
*
|
|
97
104
|
* The fallback is not a new rule invented for this vendor — it is the split
|
|
98
105
|
* cutting-data presets are routed by downstream (≤3 flutes non-ferrous, >3
|
|
99
106
|
* ferrous), applied here to the material-groups facet instead. Real vendor
|
|
@@ -109,10 +116,13 @@ export declare function shoulderDiameter(description: string, dc: number): numbe
|
|
|
109
116
|
* array and a tool's own list from another has no way to notice the two
|
|
110
117
|
* disagree.
|
|
111
118
|
*/
|
|
112
|
-
export declare function materialGroups(row: ScrapedRow, flutes: number):
|
|
119
|
+
export declare function materialGroups(row: ScrapedRow, flutes: number): Pick<ToolRecord, 'materialGroups' | 'materialGroupsSource'>;
|
|
113
120
|
/**
|
|
114
|
-
* A solid end mill,
|
|
115
|
-
*
|
|
121
|
+
* A solid end mill, in the family's declared unit.
|
|
122
|
+
*
|
|
123
|
+
* Which is inches on the one family there is — Destiny Tool publishes no metric
|
|
124
|
+
* line — but read from the `unit` fact rather than hardcoded, so the fact stays
|
|
125
|
+
* the single authored copy the way it is for every other vendor here.
|
|
116
126
|
*/
|
|
117
127
|
export declare function endmillRecord(row: ScrapedRow, family: BoundFamily, columns: ColumnMap, options?: MapperOptions): ToolRecord;
|
|
118
128
|
export declare const RECORD_MAPPERS: RecordMappers;
|