@toolpath/tool-scraper 0.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/LICENSE +21 -0
- package/README.md +98 -0
- package/dist/conventions.d.ts +124 -0
- package/dist/conventions.js +143 -0
- package/dist/errors.d.ts +46 -0
- package/dist/errors.js +53 -0
- package/dist/families/destinytool.d.ts +49 -0
- package/dist/families/destinytool.js +55 -0
- package/dist/families/index.d.ts +59 -0
- package/dist/families/index.js +91 -0
- package/dist/families/kennametal.d.ts +757 -0
- package/dist/families/kennametal.js +660 -0
- package/dist/families/regofix.d.ts +185 -0
- package/dist/families/regofix.js +250 -0
- package/dist/family.d.ts +130 -0
- package/dist/family.js +38 -0
- package/dist/fetch.d.ts +98 -0
- package/dist/fetch.js +116 -0
- package/dist/identity.d.ts +133 -0
- package/dist/identity.js +118 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +31 -0
- package/dist/node/cad-mirror.d.ts +55 -0
- package/dist/node/cad-mirror.js +89 -0
- package/dist/node/cli.d.ts +35 -0
- package/dist/node/cli.js +340 -0
- package/dist/node/csv.d.ts +47 -0
- package/dist/node/csv.js +123 -0
- package/dist/node/index.d.ts +16 -0
- package/dist/node/index.js +16 -0
- package/dist/node/main.d.ts +13 -0
- package/dist/node/main.js +14 -0
- package/dist/node/paths.d.ts +60 -0
- package/dist/node/paths.js +80 -0
- package/dist/node/receipts.d.ts +100 -0
- package/dist/node/receipts.js +107 -0
- package/dist/order.d.ts +10 -0
- package/dist/order.js +12 -0
- package/dist/provenance.d.ts +125 -0
- package/dist/provenance.js +133 -0
- package/dist/records.d.ts +305 -0
- package/dist/records.js +297 -0
- package/dist/registry.d.ts +63 -0
- package/dist/registry.js +145 -0
- package/dist/scrape.d.ts +70 -0
- package/dist/scrape.js +37 -0
- package/dist/thread.d.ts +48 -0
- package/dist/thread.js +98 -0
- package/dist/uuid5.d.ts +31 -0
- package/dist/uuid5.js +64 -0
- package/dist/vendors/destinytool/index.d.ts +11 -0
- package/dist/vendors/destinytool/index.js +11 -0
- package/dist/vendors/destinytool/records.d.ts +118 -0
- package/dist/vendors/destinytool/records.js +266 -0
- package/dist/vendors/destinytool/scrape.d.ts +108 -0
- package/dist/vendors/destinytool/scrape.js +192 -0
- package/dist/vendors/kennametal/cad.d.ts +87 -0
- package/dist/vendors/kennametal/cad.js +119 -0
- package/dist/vendors/kennametal/index.d.ts +21 -0
- package/dist/vendors/kennametal/index.js +21 -0
- package/dist/vendors/kennametal/materials.d.ts +143 -0
- package/dist/vendors/kennametal/materials.js +200 -0
- package/dist/vendors/kennametal/records.d.ts +88 -0
- package/dist/vendors/kennametal/records.js +241 -0
- package/dist/vendors/kennametal/scrape.d.ts +111 -0
- package/dist/vendors/kennametal/scrape.js +226 -0
- package/dist/vendors/kennametal/thread-column.d.ts +28 -0
- package/dist/vendors/kennametal/thread-column.js +41 -0
- package/dist/vendors/regofix/index.d.ts +8 -0
- package/dist/vendors/regofix/index.js +8 -0
- package/dist/vendors/regofix/scrape.d.ts +237 -0
- package/dist/vendors/regofix/scrape.js +521 -0
- package/package.json +76 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The interchange contract: what an adapter hands the core, and the canonical
|
|
3
|
+
* names it hands it in.
|
|
4
|
+
*
|
|
5
|
+
* Before this seam existed, the mapper read `row['AP1MAX_mm']`, `row['D3_mm']`,
|
|
6
|
+
* `row['Re_mm']` and `row['Z']` — Kennametal's own attribute codes, in the
|
|
7
|
+
* module that also held the reduced-shank rule and the bare-tool length
|
|
8
|
+
* convention. A second vendor had two options and both were bad: relabel its
|
|
9
|
+
* columns into Kennametal's on the way into the CSV, which puts a lie in the
|
|
10
|
+
* file whose whole job is to record what the vendor published; or fork the
|
|
11
|
+
* mapper, which copies the domain into every adapter to drift independently.
|
|
12
|
+
*
|
|
13
|
+
* So the mapper stopped reading vendor column names. **An adapter owns CSV →
|
|
14
|
+
* record; a record is where this package hands off.** The CSV keeps the
|
|
15
|
+
* vendor's own column labels, because it is the receipt — see `conventions`.
|
|
16
|
+
*
|
|
17
|
+
* ## The canonical names are ISO 13399
|
|
18
|
+
*
|
|
19
|
+
* `DC`, `OAL`, `LCF`, `RE`, `NOF`, `SIG`, `TP` are codes from **ISO 13399**,
|
|
20
|
+
* *Cutting tool data representation and exchange* — the machine-tool
|
|
21
|
+
* industry's own interchange dictionary. CAM vendors implement subsets of it;
|
|
22
|
+
* Autodesk Fusion implements one, which is why these names also appear in
|
|
23
|
+
* Fusion's tool JSON. This package uses the standard directly, so nothing here
|
|
24
|
+
* depends on any CAM vendor's choices, and the places where a CAM vendor
|
|
25
|
+
* departed from the standard are recorded as departures rather than adopted
|
|
26
|
+
* silently: see {@link GEOMETRY_FIELDS}, where three of the ten names are
|
|
27
|
+
* Autodesk's own and say so.
|
|
28
|
+
*
|
|
29
|
+
* The standard is paid and split across parts, so the working reference is a
|
|
30
|
+
* manufacturer's published table — Sandvik Coromant's and Dormer Pramet's are
|
|
31
|
+
* both complete.
|
|
32
|
+
*
|
|
33
|
+
* **A canonical name says nothing about units.** An adapter declares
|
|
34
|
+
* `DC: 'D1'` and the core appends `_mm`/`_in` from the family's declared unit;
|
|
35
|
+
* that rule lives in `conventions.UNIT_SUFFIX`, in one place, because choosing
|
|
36
|
+
* the suffix inside an adapter is exactly the mistake `unit` exists to prevent.
|
|
37
|
+
*
|
|
38
|
+
* ## What is deliberately *not* here
|
|
39
|
+
*
|
|
40
|
+
* `LB` and `assemblyGaugeLength` are not canonical inputs — they are `OAL`
|
|
41
|
+
* under another name on a bare tool, ISO code or not, and a field that is
|
|
42
|
+
* always a copy is not a second measurement. An adapter that could supply them
|
|
43
|
+
* separately could supply a tool that claims a holder it does not have.
|
|
44
|
+
*/
|
|
45
|
+
import { type UnitSystem } from './conventions.js';
|
|
46
|
+
/** The kinds of cutting tool this package maps. */
|
|
47
|
+
export type ToolKind = 'drill' | 'tap' | 'endmill';
|
|
48
|
+
/**
|
|
49
|
+
* ISO 513's main workpiece-material groups, in the order every vendor's
|
|
50
|
+
* material groups must agree on — **core, not a Kennametal fact**, the same
|
|
51
|
+
* distinction that puts thread-designation parsing in `thread.ts`: this is a
|
|
52
|
+
* standard, not a table. A vendor whose own column publishes groups in a
|
|
53
|
+
* different order — Destiny Tool's `isoMaterialGroups` does, e.g.
|
|
54
|
+
* `['M', 'P', 'S']` — must reorder onto this sequence rather than passing its
|
|
55
|
+
* raw order through, because a consumer that renders a facet from one array
|
|
56
|
+
* and a tool's own list from another has no way to notice the two disagree.
|
|
57
|
+
*/
|
|
58
|
+
export declare const ISO_MATERIAL_GROUPS: readonly ["P", "M", "K", "N", "S", "H", "C"];
|
|
59
|
+
/** One canonical geometry name: what it measures, and whose name it is. */
|
|
60
|
+
export interface GeometryField {
|
|
61
|
+
/**
|
|
62
|
+
* What the field measures, phrased so it can be quoted back at whoever
|
|
63
|
+
* mapped a column to the wrong one.
|
|
64
|
+
*/
|
|
65
|
+
definition: string;
|
|
66
|
+
/**
|
|
67
|
+
* The ISO 13399 code for this measurement, or `null` where the standard's
|
|
68
|
+
* counterpart has not been pinned against the dictionary. Equal to the
|
|
69
|
+
* canonical name itself on every field that *is* the standard's code.
|
|
70
|
+
*/
|
|
71
|
+
iso: string | null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Canonical geometry fields an adapter may supply, and what each means.
|
|
75
|
+
*
|
|
76
|
+
* Keyed by name so the load-time check can quote a definition back at whoever
|
|
77
|
+
* mapped a column to the wrong one, and each entry carries its ISO 13399 code
|
|
78
|
+
* so the vocabulary's source is readable from the code rather than from a plan
|
|
79
|
+
* document.
|
|
80
|
+
*
|
|
81
|
+
* **Seven of the ten are the standard's codes with the standard's meanings.**
|
|
82
|
+
* The three that are not are Autodesk's, and each has an ISO counterpart
|
|
83
|
+
* Autodesk did not use:
|
|
84
|
+
*
|
|
85
|
+
* - `SFDM` is Autodesk's "Shaft Diameter"; ISO's shank diameter is `DMM`.
|
|
86
|
+
* - `shoulder-length` and `shoulder-diameter` are Autodesk's hyphenated
|
|
87
|
+
* lowercase keys. ISO's nearest are `LS` and `DN`, and they are recorded as
|
|
88
|
+
* unpinned rather than mapped, because "nearest" is not "the same" and the
|
|
89
|
+
* standing rule here is to leave a code unlabelled rather than guess at what
|
|
90
|
+
* it measures.
|
|
91
|
+
*
|
|
92
|
+
* They are kept under Autodesk's names anyway: renaming them would buy
|
|
93
|
+
* correctness in a document nothing reads yet and cost the one property that
|
|
94
|
+
* makes a canonical name useful, which is that a downstream consumer
|
|
95
|
+
* recognises it.
|
|
96
|
+
*/
|
|
97
|
+
export declare const GEOMETRY_FIELDS: {
|
|
98
|
+
readonly DC: {
|
|
99
|
+
readonly definition: "cutting diameter";
|
|
100
|
+
readonly iso: "DC";
|
|
101
|
+
};
|
|
102
|
+
readonly SFDM: {
|
|
103
|
+
readonly definition: "shank diameter";
|
|
104
|
+
readonly iso: "DMM";
|
|
105
|
+
};
|
|
106
|
+
readonly OAL: {
|
|
107
|
+
readonly definition: "overall length";
|
|
108
|
+
readonly iso: "OAL";
|
|
109
|
+
};
|
|
110
|
+
readonly LCF: {
|
|
111
|
+
readonly definition: "flute length — the length of the cutting edge";
|
|
112
|
+
readonly iso: "LCF";
|
|
113
|
+
};
|
|
114
|
+
readonly RE: {
|
|
115
|
+
readonly definition: "corner radius; 0 on a square-end tool";
|
|
116
|
+
readonly iso: "RE";
|
|
117
|
+
};
|
|
118
|
+
readonly TP: {
|
|
119
|
+
readonly definition: "thread pitch, in the tool’s own unit system";
|
|
120
|
+
readonly iso: "TP";
|
|
121
|
+
};
|
|
122
|
+
readonly NOF: {
|
|
123
|
+
readonly definition: "number of flutes";
|
|
124
|
+
readonly iso: "NOF";
|
|
125
|
+
};
|
|
126
|
+
readonly SIG: {
|
|
127
|
+
readonly definition: "point angle, degrees included";
|
|
128
|
+
readonly iso: "SIG";
|
|
129
|
+
};
|
|
130
|
+
readonly 'shoulder-length': {
|
|
131
|
+
readonly definition: "usable length below the full shank";
|
|
132
|
+
readonly iso: null;
|
|
133
|
+
};
|
|
134
|
+
readonly 'shoulder-diameter': {
|
|
135
|
+
readonly definition: "diameter at the shoulder — the neck, where necked";
|
|
136
|
+
readonly iso: null;
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* The canonical names an adapter may map, as a type.
|
|
141
|
+
*
|
|
142
|
+
* Derived from {@link GEOMETRY_FIELDS}, so a column map naming `LFC` is a
|
|
143
|
+
* compile error rather than a key that sits there doing nothing while the
|
|
144
|
+
* geometry it was meant to fill is silently absent from every tool.
|
|
145
|
+
*/
|
|
146
|
+
export type GeometryName = keyof typeof GEOMETRY_FIELDS;
|
|
147
|
+
/**
|
|
148
|
+
* The canonical names that are **not** ISO 13399's own codes, derived rather
|
|
149
|
+
* than listed so the two cannot disagree.
|
|
150
|
+
*
|
|
151
|
+
* Asserted by the tests as exactly these three, which is what makes adding a
|
|
152
|
+
* fourth a deliberate act: a canonical name that is one CAM vendor's invention
|
|
153
|
+
* is a departure from the standard, and this package's claim to be using the
|
|
154
|
+
* standard is only as good as the departures being counted.
|
|
155
|
+
*/
|
|
156
|
+
export declare const NON_ISO_NAMES: readonly GeometryName[];
|
|
157
|
+
/**
|
|
158
|
+
* What each tool kind must map before a single row is read.
|
|
159
|
+
*
|
|
160
|
+
* The point of stating it per kind rather than per family: core can refuse
|
|
161
|
+
* "endmill family X maps no LCF" at config load, naming the family, instead of
|
|
162
|
+
* failing on a missing key from inside a mapper on row 1 of a scrape that
|
|
163
|
+
* already ran. A field's *absence* from this list is a claim too — `RE` is
|
|
164
|
+
* optional because a square-end family publishes no corner-radius column and 0
|
|
165
|
+
* is the right answer, not a missing one.
|
|
166
|
+
*/
|
|
167
|
+
export declare const REQUIRED_GEOMETRY: Record<ToolKind, readonly GeometryName[]>;
|
|
168
|
+
/**
|
|
169
|
+
* Fields that carry a unit and therefore get a `_mm`/`_in` suffix appended to
|
|
170
|
+
* the vendor's column label. The rest are counts, angles and flags, published
|
|
171
|
+
* in one column whatever the family's unit system.
|
|
172
|
+
*
|
|
173
|
+
* `TP` is here and it is the interesting one: a thread pitch is a length, and
|
|
174
|
+
* on an inch tap it is `1/TPI` **inches** while on a metric tap it is
|
|
175
|
+
* millimetres. It is nonetheless read from a single column, because the
|
|
176
|
+
* scraper derives it in the family's native unit already — so it is listed as
|
|
177
|
+
* dimensional for documentation and excluded from suffixing by
|
|
178
|
+
* {@link DIMENSIONAL_COLUMNS}.
|
|
179
|
+
*/
|
|
180
|
+
export declare const DIMENSIONAL: ReadonlySet<GeometryName>;
|
|
181
|
+
/**
|
|
182
|
+
* The subset of {@link DIMENSIONAL} whose CSV column is a unit *pair*.
|
|
183
|
+
*
|
|
184
|
+
* `TP` is dimensional but not paired: the Kennametal thread-pitch step derives
|
|
185
|
+
* one `Thread Pitch` column already in the tap's native system, so appending a
|
|
186
|
+
* suffix would look for a column that was never scraped.
|
|
187
|
+
*/
|
|
188
|
+
export declare const DIMENSIONAL_COLUMNS: ReadonlySet<GeometryName>;
|
|
189
|
+
/**
|
|
190
|
+
* One orderable cutting tool, in canonical fields, ready for the core.
|
|
191
|
+
*
|
|
192
|
+
* `readonly` throughout because it is an interchange value: an adapter builds
|
|
193
|
+
* it and hands it over, and a mapper that mutated one would be reaching back
|
|
194
|
+
* across the seam this type exists to draw.
|
|
195
|
+
*
|
|
196
|
+
* `geometry` holds {@link GEOMETRY_FIELDS} names in `unit`. An **empty**
|
|
197
|
+
* `materialGroups` is a real answer — Kennametal indexes no tap by workpiece
|
|
198
|
+
* material, so all 129 carry none, and reading empty as "unconstrained" would
|
|
199
|
+
* put every tap under every material on no evidence.
|
|
200
|
+
*/
|
|
201
|
+
export interface ToolRecord {
|
|
202
|
+
readonly vendor: string;
|
|
203
|
+
readonly materialNumber: string;
|
|
204
|
+
readonly catalogNumber: string;
|
|
205
|
+
readonly description: string;
|
|
206
|
+
readonly kind: ToolKind;
|
|
207
|
+
readonly unit: UnitSystem;
|
|
208
|
+
readonly substrate: string;
|
|
209
|
+
readonly grade: string;
|
|
210
|
+
readonly geometry: Readonly<Partial<Record<GeometryName, number | boolean>>>;
|
|
211
|
+
readonly coolantThrough: boolean;
|
|
212
|
+
readonly materialGroups: readonly string[];
|
|
213
|
+
/**
|
|
214
|
+
* Drills only, and deliberately `null` elsewhere rather than `false`: it
|
|
215
|
+
* drops the two ferrous presets downstream, and a default would ship them on
|
|
216
|
+
* a PCD tool.
|
|
217
|
+
*/
|
|
218
|
+
readonly nonFerrous: boolean | null;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Build a {@link ToolRecord}, defaulting the two fields that have a meaningful
|
|
222
|
+
* absence.
|
|
223
|
+
*
|
|
224
|
+
* An interface cannot carry a default, and requiring every adapter to write
|
|
225
|
+
* `materialGroups: []` would put the decision back in the three places least
|
|
226
|
+
* able to notice it was wrong. They stay **required on the type** so a
|
|
227
|
+
* consumer reading a record never handles `undefined` — only the construction
|
|
228
|
+
* is optional.
|
|
229
|
+
*
|
|
230
|
+
* The result is frozen, geometry and material groups included: a record is an
|
|
231
|
+
* interchange value, and a mapper that mutated one would be reaching back
|
|
232
|
+
* across the seam this type exists to draw.
|
|
233
|
+
*/
|
|
234
|
+
export declare function toolRecord(fields: Omit<ToolRecord, 'materialGroups' | 'nonFerrous'> & Partial<Pick<ToolRecord, 'materialGroups' | 'nonFerrous'>>): ToolRecord;
|
|
235
|
+
/**
|
|
236
|
+
* A family's canonical-field → CSV-column-label mapping, validated.
|
|
237
|
+
*
|
|
238
|
+
* Built once when the registry binds. `labels` are the vendor's own column
|
|
239
|
+
* labels **without** a unit suffix — `'D1'`, not `'D1_mm'` — because which
|
|
240
|
+
* suffix to read is the core's business, derived from the family's unit
|
|
241
|
+
* system. A vendor that wrote `'D1_mm'` here would be hardcoding the answer to
|
|
242
|
+
* the question `unit` exists to ask.
|
|
243
|
+
*
|
|
244
|
+
* A class rather than a plain object so that {@link checkColumnMap} is the
|
|
245
|
+
* only way to get one: a caller cannot accidentally use an unvalidated map.
|
|
246
|
+
*/
|
|
247
|
+
export declare class ColumnMap {
|
|
248
|
+
readonly kind: ToolKind;
|
|
249
|
+
readonly labels: Readonly<Partial<Record<GeometryName, string>>>;
|
|
250
|
+
/** @internal Use {@link checkColumnMap}. */
|
|
251
|
+
constructor(kind: ToolKind, labels: Readonly<Partial<Record<GeometryName, string>>>);
|
|
252
|
+
/** The CSV column to read for `canonical`, or null when unmapped. */
|
|
253
|
+
column(canonical: GeometryName, unit: UnitSystem): string | null;
|
|
254
|
+
/** Every canonical name this family maps, in declaration order. */
|
|
255
|
+
mapped(): GeometryName[];
|
|
256
|
+
}
|
|
257
|
+
/** A family config, as far as this module is concerned. */
|
|
258
|
+
export interface ColumnBearing {
|
|
259
|
+
unit?: UnitSystem;
|
|
260
|
+
columns: ColumnMap;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* The unit systems a family's rows can be in.
|
|
264
|
+
*
|
|
265
|
+
* Usually one, declared. **A tap family declares none**, and that asymmetry is
|
|
266
|
+
* real rather than an omission: a tap's system comes from its own
|
|
267
|
+
* `Thread System` column, so one family can hold metric and inch taps and both
|
|
268
|
+
* column sets must exist. Anything checking a family's columns has to check
|
|
269
|
+
* both for those.
|
|
270
|
+
*/
|
|
271
|
+
export declare function familyUnits(cfg: {
|
|
272
|
+
unit?: UnitSystem;
|
|
273
|
+
}): UnitSystem[];
|
|
274
|
+
/**
|
|
275
|
+
* Every mapped column is really in the CSV — before a single row is read.
|
|
276
|
+
*
|
|
277
|
+
* {@link checkColumnMap} sees only the map, so it cannot catch a family that
|
|
278
|
+
* maps `LCF: 'AP1MAX'` against a table publishing `AP1MAX_in` alone while
|
|
279
|
+
* tagged metric. That resolves to a column which is not there, and the per-row
|
|
280
|
+
* failure names one row out of ninety-three instead of naming the family and
|
|
281
|
+
* the field.
|
|
282
|
+
*
|
|
283
|
+
* It is separate from binding because binding deliberately does no I/O — an
|
|
284
|
+
* installed package with no scraped data must still import.
|
|
285
|
+
*/
|
|
286
|
+
export declare function checkColumnsExist(family: string, cfg: ColumnBearing, header: Iterable<string>): void;
|
|
287
|
+
/**
|
|
288
|
+
* Validate a family's column map, or refuse it by name.
|
|
289
|
+
*
|
|
290
|
+
* Three failures, and each is one that used to surface far from its cause:
|
|
291
|
+
*
|
|
292
|
+
* 1. **An unknown canonical field.** A typo like `LFC` would otherwise sit in
|
|
293
|
+
* the map doing nothing, and the geometry it was meant to fill would
|
|
294
|
+
* silently be absent from every tool in the family. {@link GeometryName}
|
|
295
|
+
* catches this at compile time for a map written in TypeScript; the check
|
|
296
|
+
* stays for one that arrives as data.
|
|
297
|
+
* 2. **A missing required field.** A missing-key fault from inside a mapper
|
|
298
|
+
* names the *column*, which is the one piece of information the person who
|
|
299
|
+
* wrote the map already had.
|
|
300
|
+
* 3. **An unknown kind**, which would otherwise skip the required-field check
|
|
301
|
+
* entirely by looking up an empty set.
|
|
302
|
+
*
|
|
303
|
+
* Returns the validated map so a caller cannot accidentally use the raw object.
|
|
304
|
+
*/
|
|
305
|
+
export declare function checkColumnMap(family: string, kind: string, labels: Readonly<Record<string, string>>): ColumnMap;
|
package/dist/records.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The interchange contract: what an adapter hands the core, and the canonical
|
|
3
|
+
* names it hands it in.
|
|
4
|
+
*
|
|
5
|
+
* Before this seam existed, the mapper read `row['AP1MAX_mm']`, `row['D3_mm']`,
|
|
6
|
+
* `row['Re_mm']` and `row['Z']` — Kennametal's own attribute codes, in the
|
|
7
|
+
* module that also held the reduced-shank rule and the bare-tool length
|
|
8
|
+
* convention. A second vendor had two options and both were bad: relabel its
|
|
9
|
+
* columns into Kennametal's on the way into the CSV, which puts a lie in the
|
|
10
|
+
* file whose whole job is to record what the vendor published; or fork the
|
|
11
|
+
* mapper, which copies the domain into every adapter to drift independently.
|
|
12
|
+
*
|
|
13
|
+
* So the mapper stopped reading vendor column names. **An adapter owns CSV →
|
|
14
|
+
* record; a record is where this package hands off.** The CSV keeps the
|
|
15
|
+
* vendor's own column labels, because it is the receipt — see `conventions`.
|
|
16
|
+
*
|
|
17
|
+
* ## The canonical names are ISO 13399
|
|
18
|
+
*
|
|
19
|
+
* `DC`, `OAL`, `LCF`, `RE`, `NOF`, `SIG`, `TP` are codes from **ISO 13399**,
|
|
20
|
+
* *Cutting tool data representation and exchange* — the machine-tool
|
|
21
|
+
* industry's own interchange dictionary. CAM vendors implement subsets of it;
|
|
22
|
+
* Autodesk Fusion implements one, which is why these names also appear in
|
|
23
|
+
* Fusion's tool JSON. This package uses the standard directly, so nothing here
|
|
24
|
+
* depends on any CAM vendor's choices, and the places where a CAM vendor
|
|
25
|
+
* departed from the standard are recorded as departures rather than adopted
|
|
26
|
+
* silently: see {@link GEOMETRY_FIELDS}, where three of the ten names are
|
|
27
|
+
* Autodesk's own and say so.
|
|
28
|
+
*
|
|
29
|
+
* The standard is paid and split across parts, so the working reference is a
|
|
30
|
+
* manufacturer's published table — Sandvik Coromant's and Dormer Pramet's are
|
|
31
|
+
* both complete.
|
|
32
|
+
*
|
|
33
|
+
* **A canonical name says nothing about units.** An adapter declares
|
|
34
|
+
* `DC: 'D1'` and the core appends `_mm`/`_in` from the family's declared unit;
|
|
35
|
+
* that rule lives in `conventions.UNIT_SUFFIX`, in one place, because choosing
|
|
36
|
+
* the suffix inside an adapter is exactly the mistake `unit` exists to prevent.
|
|
37
|
+
*
|
|
38
|
+
* ## What is deliberately *not* here
|
|
39
|
+
*
|
|
40
|
+
* `LB` and `assemblyGaugeLength` are not canonical inputs — they are `OAL`
|
|
41
|
+
* under another name on a bare tool, ISO code or not, and a field that is
|
|
42
|
+
* always a copy is not a second measurement. An adapter that could supply them
|
|
43
|
+
* separately could supply a tool that claims a holder it does not have.
|
|
44
|
+
*/
|
|
45
|
+
import { dimensionalColumn } from './conventions.js';
|
|
46
|
+
import { ScraperConfigError } from './errors.js';
|
|
47
|
+
/**
|
|
48
|
+
* ISO 513's main workpiece-material groups, in the order every vendor's
|
|
49
|
+
* material groups must agree on — **core, not a Kennametal fact**, the same
|
|
50
|
+
* distinction that puts thread-designation parsing in `thread.ts`: this is a
|
|
51
|
+
* standard, not a table. A vendor whose own column publishes groups in a
|
|
52
|
+
* different order — Destiny Tool's `isoMaterialGroups` does, e.g.
|
|
53
|
+
* `['M', 'P', 'S']` — must reorder onto this sequence rather than passing its
|
|
54
|
+
* raw order through, because a consumer that renders a facet from one array
|
|
55
|
+
* and a tool's own list from another has no way to notice the two disagree.
|
|
56
|
+
*/
|
|
57
|
+
export const ISO_MATERIAL_GROUPS = ['P', 'M', 'K', 'N', 'S', 'H', 'C'];
|
|
58
|
+
/**
|
|
59
|
+
* Canonical geometry fields an adapter may supply, and what each means.
|
|
60
|
+
*
|
|
61
|
+
* Keyed by name so the load-time check can quote a definition back at whoever
|
|
62
|
+
* mapped a column to the wrong one, and each entry carries its ISO 13399 code
|
|
63
|
+
* so the vocabulary's source is readable from the code rather than from a plan
|
|
64
|
+
* document.
|
|
65
|
+
*
|
|
66
|
+
* **Seven of the ten are the standard's codes with the standard's meanings.**
|
|
67
|
+
* The three that are not are Autodesk's, and each has an ISO counterpart
|
|
68
|
+
* Autodesk did not use:
|
|
69
|
+
*
|
|
70
|
+
* - `SFDM` is Autodesk's "Shaft Diameter"; ISO's shank diameter is `DMM`.
|
|
71
|
+
* - `shoulder-length` and `shoulder-diameter` are Autodesk's hyphenated
|
|
72
|
+
* lowercase keys. ISO's nearest are `LS` and `DN`, and they are recorded as
|
|
73
|
+
* unpinned rather than mapped, because "nearest" is not "the same" and the
|
|
74
|
+
* standing rule here is to leave a code unlabelled rather than guess at what
|
|
75
|
+
* it measures.
|
|
76
|
+
*
|
|
77
|
+
* They are kept under Autodesk's names anyway: renaming them would buy
|
|
78
|
+
* correctness in a document nothing reads yet and cost the one property that
|
|
79
|
+
* makes a canonical name useful, which is that a downstream consumer
|
|
80
|
+
* recognises it.
|
|
81
|
+
*/
|
|
82
|
+
export const GEOMETRY_FIELDS = {
|
|
83
|
+
DC: { definition: 'cutting diameter', iso: 'DC' },
|
|
84
|
+
SFDM: { definition: 'shank diameter', iso: 'DMM' },
|
|
85
|
+
OAL: { definition: 'overall length', iso: 'OAL' },
|
|
86
|
+
LCF: {
|
|
87
|
+
definition: 'flute length — the length of the cutting edge',
|
|
88
|
+
iso: 'LCF',
|
|
89
|
+
},
|
|
90
|
+
RE: { definition: 'corner radius; 0 on a square-end tool', iso: 'RE' },
|
|
91
|
+
TP: { definition: 'thread pitch, in the tool’s own unit system', iso: 'TP' },
|
|
92
|
+
NOF: { definition: 'number of flutes', iso: 'NOF' },
|
|
93
|
+
SIG: { definition: 'point angle, degrees included', iso: 'SIG' },
|
|
94
|
+
'shoulder-length': {
|
|
95
|
+
definition: 'usable length below the full shank',
|
|
96
|
+
iso: null,
|
|
97
|
+
},
|
|
98
|
+
'shoulder-diameter': {
|
|
99
|
+
definition: 'diameter at the shoulder — the neck, where necked',
|
|
100
|
+
iso: null,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* The canonical names that are **not** ISO 13399's own codes, derived rather
|
|
105
|
+
* than listed so the two cannot disagree.
|
|
106
|
+
*
|
|
107
|
+
* Asserted by the tests as exactly these three, which is what makes adding a
|
|
108
|
+
* fourth a deliberate act: a canonical name that is one CAM vendor's invention
|
|
109
|
+
* is a departure from the standard, and this package's claim to be using the
|
|
110
|
+
* standard is only as good as the departures being counted.
|
|
111
|
+
*/
|
|
112
|
+
export const NON_ISO_NAMES = Object.entries(GEOMETRY_FIELDS)
|
|
113
|
+
.filter(([name, field]) => field.iso !== name)
|
|
114
|
+
.map(([name]) => name);
|
|
115
|
+
/**
|
|
116
|
+
* What each tool kind must map before a single row is read.
|
|
117
|
+
*
|
|
118
|
+
* The point of stating it per kind rather than per family: core can refuse
|
|
119
|
+
* "endmill family X maps no LCF" at config load, naming the family, instead of
|
|
120
|
+
* failing on a missing key from inside a mapper on row 1 of a scrape that
|
|
121
|
+
* already ran. A field's *absence* from this list is a claim too — `RE` is
|
|
122
|
+
* optional because a square-end family publishes no corner-radius column and 0
|
|
123
|
+
* is the right answer, not a missing one.
|
|
124
|
+
*/
|
|
125
|
+
export const REQUIRED_GEOMETRY = {
|
|
126
|
+
drill: ['DC', 'SFDM', 'OAL', 'LCF'],
|
|
127
|
+
tap: ['SFDM', 'OAL', 'LCF'],
|
|
128
|
+
endmill: ['DC', 'SFDM', 'OAL', 'LCF'],
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Fields that carry a unit and therefore get a `_mm`/`_in` suffix appended to
|
|
132
|
+
* the vendor's column label. The rest are counts, angles and flags, published
|
|
133
|
+
* in one column whatever the family's unit system.
|
|
134
|
+
*
|
|
135
|
+
* `TP` is here and it is the interesting one: a thread pitch is a length, and
|
|
136
|
+
* on an inch tap it is `1/TPI` **inches** while on a metric tap it is
|
|
137
|
+
* millimetres. It is nonetheless read from a single column, because the
|
|
138
|
+
* scraper derives it in the family's native unit already — so it is listed as
|
|
139
|
+
* dimensional for documentation and excluded from suffixing by
|
|
140
|
+
* {@link DIMENSIONAL_COLUMNS}.
|
|
141
|
+
*/
|
|
142
|
+
export const DIMENSIONAL = new Set([
|
|
143
|
+
'DC',
|
|
144
|
+
'SFDM',
|
|
145
|
+
'OAL',
|
|
146
|
+
'LCF',
|
|
147
|
+
'RE',
|
|
148
|
+
'TP',
|
|
149
|
+
'shoulder-length',
|
|
150
|
+
'shoulder-diameter',
|
|
151
|
+
]);
|
|
152
|
+
/**
|
|
153
|
+
* The subset of {@link DIMENSIONAL} whose CSV column is a unit *pair*.
|
|
154
|
+
*
|
|
155
|
+
* `TP` is dimensional but not paired: the Kennametal thread-pitch step derives
|
|
156
|
+
* one `Thread Pitch` column already in the tap's native system, so appending a
|
|
157
|
+
* suffix would look for a column that was never scraped.
|
|
158
|
+
*/
|
|
159
|
+
export const DIMENSIONAL_COLUMNS = new Set([...DIMENSIONAL].filter((name) => name !== 'TP'));
|
|
160
|
+
/**
|
|
161
|
+
* Build a {@link ToolRecord}, defaulting the two fields that have a meaningful
|
|
162
|
+
* absence.
|
|
163
|
+
*
|
|
164
|
+
* An interface cannot carry a default, and requiring every adapter to write
|
|
165
|
+
* `materialGroups: []` would put the decision back in the three places least
|
|
166
|
+
* able to notice it was wrong. They stay **required on the type** so a
|
|
167
|
+
* consumer reading a record never handles `undefined` — only the construction
|
|
168
|
+
* is optional.
|
|
169
|
+
*
|
|
170
|
+
* The result is frozen, geometry and material groups included: a record is an
|
|
171
|
+
* interchange value, and a mapper that mutated one would be reaching back
|
|
172
|
+
* across the seam this type exists to draw.
|
|
173
|
+
*/
|
|
174
|
+
export function toolRecord(fields) {
|
|
175
|
+
return Object.freeze({
|
|
176
|
+
...fields,
|
|
177
|
+
geometry: Object.freeze({ ...fields.geometry }),
|
|
178
|
+
materialGroups: Object.freeze([...(fields.materialGroups ?? [])]),
|
|
179
|
+
nonFerrous: fields.nonFerrous ?? null,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* A family's canonical-field → CSV-column-label mapping, validated.
|
|
184
|
+
*
|
|
185
|
+
* Built once when the registry binds. `labels` are the vendor's own column
|
|
186
|
+
* labels **without** a unit suffix — `'D1'`, not `'D1_mm'` — because which
|
|
187
|
+
* suffix to read is the core's business, derived from the family's unit
|
|
188
|
+
* system. A vendor that wrote `'D1_mm'` here would be hardcoding the answer to
|
|
189
|
+
* the question `unit` exists to ask.
|
|
190
|
+
*
|
|
191
|
+
* A class rather than a plain object so that {@link checkColumnMap} is the
|
|
192
|
+
* only way to get one: a caller cannot accidentally use an unvalidated map.
|
|
193
|
+
*/
|
|
194
|
+
export class ColumnMap {
|
|
195
|
+
kind;
|
|
196
|
+
labels;
|
|
197
|
+
/** @internal Use {@link checkColumnMap}. */
|
|
198
|
+
constructor(kind, labels) {
|
|
199
|
+
this.kind = kind;
|
|
200
|
+
this.labels = { ...labels };
|
|
201
|
+
}
|
|
202
|
+
/** The CSV column to read for `canonical`, or null when unmapped. */
|
|
203
|
+
column(canonical, unit) {
|
|
204
|
+
const label = this.labels[canonical];
|
|
205
|
+
if (label === undefined)
|
|
206
|
+
return null;
|
|
207
|
+
if (!DIMENSIONAL_COLUMNS.has(canonical))
|
|
208
|
+
return label;
|
|
209
|
+
return dimensionalColumn(label, unit);
|
|
210
|
+
}
|
|
211
|
+
/** Every canonical name this family maps, in declaration order. */
|
|
212
|
+
mapped() {
|
|
213
|
+
return Object.keys(this.labels);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The unit systems a family's rows can be in.
|
|
218
|
+
*
|
|
219
|
+
* Usually one, declared. **A tap family declares none**, and that asymmetry is
|
|
220
|
+
* real rather than an omission: a tap's system comes from its own
|
|
221
|
+
* `Thread System` column, so one family can hold metric and inch taps and both
|
|
222
|
+
* column sets must exist. Anything checking a family's columns has to check
|
|
223
|
+
* both for those.
|
|
224
|
+
*/
|
|
225
|
+
export function familyUnits(cfg) {
|
|
226
|
+
return cfg.unit === undefined ? ['millimeters', 'inches'] : [cfg.unit];
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Every mapped column is really in the CSV — before a single row is read.
|
|
230
|
+
*
|
|
231
|
+
* {@link checkColumnMap} sees only the map, so it cannot catch a family that
|
|
232
|
+
* maps `LCF: 'AP1MAX'` against a table publishing `AP1MAX_in` alone while
|
|
233
|
+
* tagged metric. That resolves to a column which is not there, and the per-row
|
|
234
|
+
* failure names one row out of ninety-three instead of naming the family and
|
|
235
|
+
* the field.
|
|
236
|
+
*
|
|
237
|
+
* It is separate from binding because binding deliberately does no I/O — an
|
|
238
|
+
* installed package with no scraped data must still import.
|
|
239
|
+
*/
|
|
240
|
+
export function checkColumnsExist(family, cfg, header) {
|
|
241
|
+
const present = new Set(header);
|
|
242
|
+
const missing = [];
|
|
243
|
+
for (const unit of familyUnits(cfg)) {
|
|
244
|
+
for (const canonical of cfg.columns.mapped()) {
|
|
245
|
+
// Never null: `mapped()` yields only the names that carry a label, which
|
|
246
|
+
// is the one case `column()` has nothing to return.
|
|
247
|
+
const column = cfg.columns.column(canonical, unit);
|
|
248
|
+
if (!present.has(column)) {
|
|
249
|
+
missing.push(`${canonical} -> ${column}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (missing.length > 0) {
|
|
254
|
+
throw new ScraperConfigError(family, `mapped column(s) absent from the CSV: ${[...new Set(missing)].sort().join(', ')}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Validate a family's column map, or refuse it by name.
|
|
259
|
+
*
|
|
260
|
+
* Three failures, and each is one that used to surface far from its cause:
|
|
261
|
+
*
|
|
262
|
+
* 1. **An unknown canonical field.** A typo like `LFC` would otherwise sit in
|
|
263
|
+
* the map doing nothing, and the geometry it was meant to fill would
|
|
264
|
+
* silently be absent from every tool in the family. {@link GeometryName}
|
|
265
|
+
* catches this at compile time for a map written in TypeScript; the check
|
|
266
|
+
* stays for one that arrives as data.
|
|
267
|
+
* 2. **A missing required field.** A missing-key fault from inside a mapper
|
|
268
|
+
* names the *column*, which is the one piece of information the person who
|
|
269
|
+
* wrote the map already had.
|
|
270
|
+
* 3. **An unknown kind**, which would otherwise skip the required-field check
|
|
271
|
+
* entirely by looking up an empty set.
|
|
272
|
+
*
|
|
273
|
+
* Returns the validated map so a caller cannot accidentally use the raw object.
|
|
274
|
+
*/
|
|
275
|
+
export function checkColumnMap(family, kind, labels) {
|
|
276
|
+
if (!Object.hasOwn(REQUIRED_GEOMETRY, kind)) {
|
|
277
|
+
throw new ScraperConfigError(family, `unknown tool kind ${JSON.stringify(kind)} ` +
|
|
278
|
+
`(known: ${Object.keys(REQUIRED_GEOMETRY).sort().join(', ')})`);
|
|
279
|
+
}
|
|
280
|
+
const unknown = Object.keys(labels)
|
|
281
|
+
.filter((name) => !Object.hasOwn(GEOMETRY_FIELDS, name))
|
|
282
|
+
.sort();
|
|
283
|
+
if (unknown.length > 0) {
|
|
284
|
+
throw new ScraperConfigError(family, `maps ${unknown.join(', ')} which are not canonical geometry fields ` +
|
|
285
|
+
`(known: ${Object.keys(GEOMETRY_FIELDS).sort().join(', ')})`);
|
|
286
|
+
}
|
|
287
|
+
const missing = REQUIRED_GEOMETRY[kind]
|
|
288
|
+
.filter((name) => !Object.hasOwn(labels, name))
|
|
289
|
+
.sort();
|
|
290
|
+
if (missing.length > 0) {
|
|
291
|
+
const described = missing
|
|
292
|
+
.map((name) => `${name} (${GEOMETRY_FIELDS[name].definition})`)
|
|
293
|
+
.join(', ');
|
|
294
|
+
throw new ScraperConfigError(family, `a ${kind} family must map ${described}`);
|
|
295
|
+
}
|
|
296
|
+
return new ColumnMap(kind, labels);
|
|
297
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which adapter serves which family — the composition root.
|
|
3
|
+
*
|
|
4
|
+
* `families/` is the config table and `vendors/` are the adapters; this is the
|
|
5
|
+
* one module that knows both, and it exists so that neither has to. Putting
|
|
6
|
+
* the binding in the config table made the table import a manufacturer, which
|
|
7
|
+
* `tests/vendor-boundary.test.ts` refuses for good reason: the table is read
|
|
8
|
+
* by every test, and none of those should drag a vendor's scraper in behind it.
|
|
9
|
+
*
|
|
10
|
+
* **This is the seam a per-vendor distribution would replace with an
|
|
11
|
+
* entry-point registry.** When a vendor package can be installed from outside
|
|
12
|
+
* the tree, the {@link ADAPTERS} table below becomes a lookup over those and
|
|
13
|
+
* nothing else here changes shape. Until then one repository holds every
|
|
14
|
+
* adapter, which is what lets one commit change the record contract and every
|
|
15
|
+
* adapter together.
|
|
16
|
+
*
|
|
17
|
+
* ## Binding is lazy, not an import side effect
|
|
18
|
+
*
|
|
19
|
+
* A mapping fault has to surface as an error naming the family, not as a
|
|
20
|
+
* missing-key fault from inside a mapper on row 1 of a scrape that already
|
|
21
|
+
* ran. Binding at import would buy that and charge for it: a consumer that
|
|
22
|
+
* imports one helper should not pay to validate a catalog it never reads.
|
|
23
|
+
*
|
|
24
|
+
* So it is memoised instead: the first call to {@link boundFamilies} validates
|
|
25
|
+
* every family and every fact, and the failure still names the family and the
|
|
26
|
+
* key. What changes is only *when* — first use rather than import — and every
|
|
27
|
+
* entry point into this package goes through here.
|
|
28
|
+
*/
|
|
29
|
+
import type { BoundFamily, BoundToolholding, RecordMappers } from './family.js';
|
|
30
|
+
/**
|
|
31
|
+
* Brand -> its row-to-record mappers, by tool kind.
|
|
32
|
+
*
|
|
33
|
+
* One entry serves two brands: Kennametal and WIDIA are the same AEM platform
|
|
34
|
+
* and the same table vocabulary, so one adapter covers both, exactly as one
|
|
35
|
+
* scraper does. A brand absent from here can still be scraped — REGO-FIX ships
|
|
36
|
+
* toolholding and no cutting tools, and only cutting tools go through a column
|
|
37
|
+
* map.
|
|
38
|
+
*/
|
|
39
|
+
export declare const ADAPTERS: Record<string, RecordMappers>;
|
|
40
|
+
/**
|
|
41
|
+
* Every cutting-tool family, validated and bound to its record mapper.
|
|
42
|
+
*
|
|
43
|
+
* Memoised, so calling it twice is free and a fault is reported once.
|
|
44
|
+
*/
|
|
45
|
+
export declare function boundFamilies(): Map<string, BoundFamily>;
|
|
46
|
+
/**
|
|
47
|
+
* Every holder and collet family, with its facts checked and projected.
|
|
48
|
+
*
|
|
49
|
+
* They bind no adapter — only cutting tools go through a column map — but
|
|
50
|
+
* their facts pass the same gate: a taper or a clamping mode is a per-family
|
|
51
|
+
* constant no variant table states, exactly like a drill's flute count.
|
|
52
|
+
*/
|
|
53
|
+
export declare function boundToolholding(): Map<string, BoundToolholding>;
|
|
54
|
+
/** One bound cutting-tool family by CSV name. */
|
|
55
|
+
export declare function boundFamily(name: string): BoundFamily;
|
|
56
|
+
/**
|
|
57
|
+
* Forget what has been bound.
|
|
58
|
+
*
|
|
59
|
+
* For tests that put a family in front of the gate. Nothing in a running
|
|
60
|
+
* scrape should need it — the tables are module constants, so a second bind
|
|
61
|
+
* would produce the same answer.
|
|
62
|
+
*/
|
|
63
|
+
export declare function resetBindings(): void;
|