@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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/dist/conventions.d.ts +124 -0
  4. package/dist/conventions.js +143 -0
  5. package/dist/errors.d.ts +46 -0
  6. package/dist/errors.js +53 -0
  7. package/dist/families/destinytool.d.ts +49 -0
  8. package/dist/families/destinytool.js +55 -0
  9. package/dist/families/index.d.ts +59 -0
  10. package/dist/families/index.js +91 -0
  11. package/dist/families/kennametal.d.ts +757 -0
  12. package/dist/families/kennametal.js +660 -0
  13. package/dist/families/regofix.d.ts +185 -0
  14. package/dist/families/regofix.js +250 -0
  15. package/dist/family.d.ts +130 -0
  16. package/dist/family.js +38 -0
  17. package/dist/fetch.d.ts +98 -0
  18. package/dist/fetch.js +116 -0
  19. package/dist/identity.d.ts +133 -0
  20. package/dist/identity.js +118 -0
  21. package/dist/index.d.ts +31 -0
  22. package/dist/index.js +31 -0
  23. package/dist/node/cad-mirror.d.ts +55 -0
  24. package/dist/node/cad-mirror.js +89 -0
  25. package/dist/node/cli.d.ts +35 -0
  26. package/dist/node/cli.js +340 -0
  27. package/dist/node/csv.d.ts +47 -0
  28. package/dist/node/csv.js +123 -0
  29. package/dist/node/index.d.ts +16 -0
  30. package/dist/node/index.js +16 -0
  31. package/dist/node/main.d.ts +13 -0
  32. package/dist/node/main.js +14 -0
  33. package/dist/node/paths.d.ts +60 -0
  34. package/dist/node/paths.js +80 -0
  35. package/dist/node/receipts.d.ts +100 -0
  36. package/dist/node/receipts.js +107 -0
  37. package/dist/order.d.ts +10 -0
  38. package/dist/order.js +12 -0
  39. package/dist/provenance.d.ts +125 -0
  40. package/dist/provenance.js +133 -0
  41. package/dist/records.d.ts +305 -0
  42. package/dist/records.js +297 -0
  43. package/dist/registry.d.ts +63 -0
  44. package/dist/registry.js +145 -0
  45. package/dist/scrape.d.ts +70 -0
  46. package/dist/scrape.js +37 -0
  47. package/dist/thread.d.ts +48 -0
  48. package/dist/thread.js +98 -0
  49. package/dist/uuid5.d.ts +31 -0
  50. package/dist/uuid5.js +64 -0
  51. package/dist/vendors/destinytool/index.d.ts +11 -0
  52. package/dist/vendors/destinytool/index.js +11 -0
  53. package/dist/vendors/destinytool/records.d.ts +118 -0
  54. package/dist/vendors/destinytool/records.js +266 -0
  55. package/dist/vendors/destinytool/scrape.d.ts +108 -0
  56. package/dist/vendors/destinytool/scrape.js +192 -0
  57. package/dist/vendors/kennametal/cad.d.ts +87 -0
  58. package/dist/vendors/kennametal/cad.js +119 -0
  59. package/dist/vendors/kennametal/index.d.ts +21 -0
  60. package/dist/vendors/kennametal/index.js +21 -0
  61. package/dist/vendors/kennametal/materials.d.ts +143 -0
  62. package/dist/vendors/kennametal/materials.js +200 -0
  63. package/dist/vendors/kennametal/records.d.ts +88 -0
  64. package/dist/vendors/kennametal/records.js +241 -0
  65. package/dist/vendors/kennametal/scrape.d.ts +111 -0
  66. package/dist/vendors/kennametal/scrape.js +226 -0
  67. package/dist/vendors/kennametal/thread-column.d.ts +28 -0
  68. package/dist/vendors/kennametal/thread-column.js +41 -0
  69. package/dist/vendors/regofix/index.d.ts +8 -0
  70. package/dist/vendors/regofix/index.js +8 -0
  71. package/dist/vendors/regofix/scrape.d.ts +237 -0
  72. package/dist/vendors/regofix/scrape.js +521 -0
  73. package/package.json +76 -0
@@ -0,0 +1,185 @@
1
+ /**
2
+ * REGO-FIX's families — toolholding only.
3
+ *
4
+ * There is no `FAMILIES` table here: REGO-FIX publishes no cutting tools, so this
5
+ * vendor never goes through a column map. What it publishes is powRgrip holders
6
+ * and the PG collets that press into them, and both are scraped from the
7
+ * ProductFinder's Elasticsearch index rather than from a family page — which is
8
+ * why there is no `familyCode` either. The scrape target is a set of index
9
+ * filters, and it lives with the scraper that posts them.
10
+ */
11
+ export declare const HOLDER_FAMILIES: {
12
+ readonly 'regofix_bt30_pg_holders.csv': {
13
+ readonly catalogName: "REGO-FIX powRgrip BT30 Toolholders";
14
+ readonly rows: 21;
15
+ readonly brand: "regofix";
16
+ readonly facts: {
17
+ readonly taper: {
18
+ readonly value: "BT30";
19
+ readonly source: "vendor-stated";
20
+ readonly cite: "every row's DIN 4000 J1 property is DINISO7388-2 — the same 7/24 cone whether or not the flange face seats";
21
+ };
22
+ readonly clamping: {
23
+ readonly value: "collet";
24
+ readonly source: "vendor-stated";
25
+ readonly cite: "the holder publishes a CST collet series, so it grips through a collet";
26
+ };
27
+ readonly style: {
28
+ readonly value: "pg-collet-chuck";
29
+ readonly source: "vendor-stated";
30
+ readonly cite: "REGO-FIX's own system name, /products/system/powrgrip";
31
+ };
32
+ readonly unit: {
33
+ readonly value: "millimeters";
34
+ readonly source: "vendor-stated";
35
+ readonly cite: "the family is titled and catalogued in this system; both unit columns are usually published, so this decides which is displayed";
36
+ };
37
+ };
38
+ };
39
+ };
40
+ export declare const COLLET_FAMILIES: {
41
+ readonly 'regofix_pg_collets_standard.csv': {
42
+ readonly catalogName: "REGO-FIX powRgrip PG Standard Collets";
43
+ readonly rows: 71;
44
+ readonly brand: "regofix";
45
+ readonly facts: {
46
+ readonly style: {
47
+ readonly value: "pg-standard";
48
+ readonly source: "vendor-stated";
49
+ readonly cite: "the ProductFinder index groups these under product_group_name 'Standard'";
50
+ };
51
+ };
52
+ };
53
+ readonly 'regofix_pg_collets_coolant_flush.csv': {
54
+ readonly catalogName: "REGO-FIX powRgrip PG Coolant Flush Collets";
55
+ readonly rows: 51;
56
+ readonly brand: "regofix";
57
+ readonly facts: {
58
+ readonly style: {
59
+ readonly value: "pg-coolant-flush";
60
+ readonly source: "vendor-stated";
61
+ readonly cite: "the ProductFinder index groups these under product_group_name 'Coolant flush'";
62
+ };
63
+ };
64
+ };
65
+ readonly 'regofix_pg_collets_short.csv': {
66
+ readonly catalogName: "REGO-FIX powRgrip PG Short Collets";
67
+ readonly rows: 34;
68
+ readonly brand: "regofix";
69
+ readonly facts: {
70
+ readonly style: {
71
+ readonly value: "pg-short";
72
+ readonly source: "vendor-stated";
73
+ readonly cite: "the ProductFinder index groups these under product_group_name 'Short'";
74
+ };
75
+ };
76
+ };
77
+ readonly 'regofix_pg_collets_cool_bore.csv': {
78
+ readonly catalogName: "REGO-FIX powRgrip PG Cool Bore Collets";
79
+ readonly rows: 30;
80
+ readonly brand: "regofix";
81
+ readonly facts: {
82
+ readonly style: {
83
+ readonly value: "pg-cool-bore";
84
+ readonly source: "vendor-stated";
85
+ readonly cite: "the ProductFinder index groups these under product_group_name 'Cool bore'";
86
+ };
87
+ };
88
+ };
89
+ readonly 'regofix_pgst_collets.csv': {
90
+ readonly catalogName: "REGO-FIX powRgrip PGST Short Tail Collets";
91
+ readonly rows: 28;
92
+ readonly brand: "regofix";
93
+ readonly facts: {
94
+ readonly style: {
95
+ readonly value: "pgst-short-tail";
96
+ readonly source: "vendor-stated";
97
+ readonly cite: "the ProductFinder index groups these under product_group_name 'PGST'";
98
+ };
99
+ };
100
+ };
101
+ readonly 'regofix_pg_collets_tap.csv': {
102
+ readonly catalogName: "REGO-FIX powRgrip PG Tapping Collets";
103
+ readonly rows: 26;
104
+ readonly brand: "regofix";
105
+ readonly facts: {
106
+ readonly style: {
107
+ readonly value: "pg-tap";
108
+ readonly source: "vendor-stated";
109
+ readonly cite: "the ProductFinder index groups these under product_group_name 'Tapping collet TAP'";
110
+ };
111
+ };
112
+ };
113
+ readonly 'regofix_pg_collets_long.csv': {
114
+ readonly catalogName: "REGO-FIX powRgrip PG Long Collets";
115
+ readonly rows: 21;
116
+ readonly brand: "regofix";
117
+ readonly facts: {
118
+ readonly style: {
119
+ readonly value: "pg-long";
120
+ readonly source: "vendor-stated";
121
+ readonly cite: "the ProductFinder index groups these under product_group_name 'Long'";
122
+ };
123
+ };
124
+ };
125
+ readonly 'regofix_pg_collets_microbore.csv': {
126
+ readonly catalogName: "REGO-FIX powRgrip PG Microbore Collets";
127
+ readonly rows: 16;
128
+ readonly brand: "regofix";
129
+ readonly facts: {
130
+ readonly style: {
131
+ readonly value: "pg-microbore";
132
+ readonly source: "vendor-stated";
133
+ readonly cite: "the ProductFinder index groups these under product_group_name 'Microbore'";
134
+ };
135
+ };
136
+ };
137
+ readonly 'regofix_pg_collets_turning.csv': {
138
+ readonly catalogName: "REGO-FIX powRgrip PG Turning Collets";
139
+ readonly rows: 16;
140
+ readonly brand: "regofix";
141
+ readonly facts: {
142
+ readonly style: {
143
+ readonly value: "pg-turning";
144
+ readonly source: "vendor-stated";
145
+ readonly cite: "the ProductFinder index groups these under product_group_name 'PG-T'";
146
+ };
147
+ };
148
+ };
149
+ readonly 'regofix_pg_collets_mql.csv': {
150
+ readonly catalogName: "REGO-FIX powRgrip PG MQL Collets";
151
+ readonly rows: 11;
152
+ readonly brand: "regofix";
153
+ readonly facts: {
154
+ readonly style: {
155
+ readonly value: "pg-mql";
156
+ readonly source: "vendor-stated";
157
+ readonly cite: "the ProductFinder index groups these under product_group_name 'PG-MQL'";
158
+ };
159
+ };
160
+ };
161
+ readonly 'regofix_pg_collets_securgrip.csv': {
162
+ readonly catalogName: "REGO-FIX powRgrip PG secuRgrip Collets";
163
+ readonly rows: 10;
164
+ readonly brand: "regofix";
165
+ readonly facts: {
166
+ readonly style: {
167
+ readonly value: "pg-securgrip";
168
+ readonly source: "vendor-stated";
169
+ readonly cite: "the ProductFinder index groups these under product_group_name 'secuRgrip'";
170
+ };
171
+ };
172
+ };
173
+ readonly 'regofix_pg_collets_sealed_cap.csv': {
174
+ readonly catalogName: "REGO-FIX powRgrip PG Sealed Cap Collets";
175
+ readonly rows: 7;
176
+ readonly brand: "regofix";
177
+ readonly facts: {
178
+ readonly style: {
179
+ readonly value: "pg-sealed-cap";
180
+ readonly source: "vendor-stated";
181
+ readonly cite: "the ProductFinder index groups these under product_group_name 'PG-SC'";
182
+ };
183
+ };
184
+ };
185
+ };
@@ -0,0 +1,250 @@
1
+ /**
2
+ * REGO-FIX's families — toolholding only.
3
+ *
4
+ * There is no `FAMILIES` table here: REGO-FIX publishes no cutting tools, so this
5
+ * vendor never goes through a column map. What it publishes is powRgrip holders
6
+ * and the PG collets that press into them, and both are scraped from the
7
+ * ProductFinder's Elasticsearch index rather than from a family page — which is
8
+ * why there is no `familyCode` either. The scrape target is a set of index
9
+ * filters, and it lives with the scraper that posts them.
10
+ */
11
+ export const HOLDER_FAMILIES = {
12
+ // ── REGO-FIX powRgrip, BT 30 (JG 2026-08-07) ───────────────────────────
13
+ // The first non-Kennametal toolholding family, and the first one whose
14
+ // `contact` is **not** here. REGO-FIX publishes plain and dual-contact BT30
15
+ // in one product group — `BT 30 / PG 25 x 080 H` and `BT+ 30 / PG 25 x 080
16
+ // H` are two rows of one table — and states which is which in a `form_name`
17
+ // field. So the CSV carries a `contact` column, and the rule is that a
18
+ // scraped fact beats a family constant. There is deliberately no `contact`
19
+ // key below; supplying one would silently mask a scrape that lost the
20
+ // column.
21
+ //
22
+ // `clamping: 'collet'` and `style: 'pg-collet-chuck'`. A powRgrip holder
23
+ // takes a PG collet, which is pressed in with a hydraulic clamping unit
24
+ // rather than closed by a nut — so it grips through a collet exactly as an
25
+ // ER chuck does, and the picker asks it the same question. What differs is
26
+ // that seating one needs a PGU/PGS press on the bench, which is why it is
27
+ // its own `style` and not `er-collet-chuck` with a different series.
28
+ // REGO-FIX's own words for the line are "powRgrip" and "PG toolholders"
29
+ // (`/en/products/system/powrgrip`, `/en/products/components/toolholders/
30
+ // powRgrip`).
31
+ //
32
+ // **`taper: 'BT30'` covers BT+ 30 too**, the same call BTKV30 got and for
33
+ // the same reason: the vendor's own `J1` property is `DINISO7388-2` on
34
+ // every one of these rows, plus-form included, so the cone is identical and
35
+ // the plus is a face that also seats. Recording `BT+30` as a taper would
36
+ // hide ten holders from every BT30 filter.
37
+ //
38
+ // `rows` is 21 against 22 in the vendor's group: `4130.71506` (BT+ 30 / PG
39
+ // 15 x 075 H) has DXF and PDF drawings but no DIN 4000 document, so it has
40
+ // no published gage length and is skipped by the scraper with a message.
41
+ // The three `BT-OM 30` parts are excluded at a different level — see
42
+ // `vendors/regofix/scrape.ts`'s `SCRAPED_TAPERS` — because nothing on the
43
+ // vendor's site says what OM designates.
44
+ 'regofix_bt30_pg_holders.csv': {
45
+ catalogName: 'REGO-FIX powRgrip BT30 Toolholders',
46
+ rows: 21,
47
+ brand: 'regofix',
48
+ facts: {
49
+ taper: {
50
+ value: 'BT30',
51
+ source: 'vendor-stated',
52
+ cite: "every row's DIN 4000 J1 property is DINISO7388-2 — the same 7/24 cone whether or not the flange face seats",
53
+ },
54
+ clamping: {
55
+ value: 'collet',
56
+ source: 'vendor-stated',
57
+ cite: 'the holder publishes a CST collet series, so it grips through a collet',
58
+ },
59
+ style: {
60
+ value: 'pg-collet-chuck',
61
+ source: 'vendor-stated',
62
+ cite: "REGO-FIX's own system name, /products/system/powrgrip",
63
+ },
64
+ unit: {
65
+ value: 'millimeters',
66
+ source: 'vendor-stated',
67
+ cite: 'the family is titled and catalogued in this system; both unit columns are usually published, so this decides which is displayed',
68
+ },
69
+ },
70
+ },
71
+ };
72
+ export const COLLET_FAMILIES = {
73
+ // ── REGO-FIX powRgrip PG collets (JG 2026-08-07) ───────────────────────
74
+ // Twelve product groups, restricted to the PG series a BT30 holder takes
75
+ // (6, 10, 15, 25). PG 32 and PG 48 collets exist and no BT30 holder in this
76
+ // catalog accepts one.
77
+ //
78
+ // **None of these declares a `unit`, and that is the change this package
79
+ // promised itself.** Every group holds metric and fractional-inch collets
80
+ // side by side — `PG 25 Ø 6.0 mm` and `PG 25 Ø 1/4"` are two rows of one
81
+ // group — so there is no family-level answer to declare. The unit is a
82
+ // column. Kennametal's TT HPV family is the one place a mixed family was
83
+ // handled by splitting the CSV, and the note there says a second one means
84
+ // making the unit per-record instead of splitting twice. This is that
85
+ // second one, twelve times over.
86
+ //
87
+ // **`style` is the vendor's own product group**, not a coinage: each value
88
+ // below is a slug of `product_group_name` in the ProductFinder index, and
89
+ // a consumer's label for it comes from the matching
90
+ // `product_category_name`. Both are re-checkable with one POST to the
91
+ // endpoint in `REGOFIX_PRODUCTFINDER_API.md` — no browser, no scraper
92
+ // change. They are config rather than a scraped column for the same reason
93
+ // Kennametal's are: constant per family, and this table is where per-family
94
+ // constants live.
95
+ //
96
+ // **PGST is a separate series, not a PG collet.** Its parts are designated
97
+ // `PGST 15`, REGO-FIX sells dedicated `.../PGST Short Tail` toolholders for
98
+ // them, and nothing published says whether a PGST collet also seats in a
99
+ // plain PG holder. `collet_row` therefore writes the series exactly as the
100
+ // vendor designates it, so a PGST collet matches no PG holder. That is the
101
+ // conservative direction on purpose: hiding a collet that would have fitted
102
+ // costs an option, while offering one that does not fit costs a machinist a
103
+ // purchase. Resolve it by asking REGO-FIX, not by widening the string.
104
+ // Today no BT30 short-tail holder exists either way, so all 28 sit in the
105
+ // catalog fitting nothing.
106
+ 'regofix_pg_collets_standard.csv': {
107
+ catalogName: 'REGO-FIX powRgrip PG Standard Collets',
108
+ rows: 71,
109
+ brand: 'regofix',
110
+ facts: {
111
+ style: {
112
+ value: 'pg-standard',
113
+ source: 'vendor-stated',
114
+ cite: "the ProductFinder index groups these under product_group_name 'Standard'",
115
+ },
116
+ },
117
+ },
118
+ 'regofix_pg_collets_coolant_flush.csv': {
119
+ catalogName: 'REGO-FIX powRgrip PG Coolant Flush Collets',
120
+ rows: 51,
121
+ brand: 'regofix',
122
+ facts: {
123
+ style: {
124
+ value: 'pg-coolant-flush',
125
+ source: 'vendor-stated',
126
+ cite: "the ProductFinder index groups these under product_group_name 'Coolant flush'",
127
+ },
128
+ },
129
+ },
130
+ 'regofix_pg_collets_short.csv': {
131
+ catalogName: 'REGO-FIX powRgrip PG Short Collets',
132
+ rows: 34,
133
+ brand: 'regofix',
134
+ facts: {
135
+ style: {
136
+ value: 'pg-short',
137
+ source: 'vendor-stated',
138
+ cite: "the ProductFinder index groups these under product_group_name 'Short'",
139
+ },
140
+ },
141
+ },
142
+ 'regofix_pg_collets_cool_bore.csv': {
143
+ catalogName: 'REGO-FIX powRgrip PG Cool Bore Collets',
144
+ rows: 30,
145
+ brand: 'regofix',
146
+ facts: {
147
+ style: {
148
+ value: 'pg-cool-bore',
149
+ source: 'vendor-stated',
150
+ cite: "the ProductFinder index groups these under product_group_name 'Cool bore'",
151
+ },
152
+ },
153
+ },
154
+ 'regofix_pgst_collets.csv': {
155
+ catalogName: 'REGO-FIX powRgrip PGST Short Tail Collets',
156
+ rows: 28,
157
+ brand: 'regofix',
158
+ facts: {
159
+ style: {
160
+ value: 'pgst-short-tail',
161
+ source: 'vendor-stated',
162
+ cite: "the ProductFinder index groups these under product_group_name 'PGST'",
163
+ },
164
+ },
165
+ },
166
+ 'regofix_pg_collets_tap.csv': {
167
+ catalogName: 'REGO-FIX powRgrip PG Tapping Collets',
168
+ rows: 26,
169
+ brand: 'regofix',
170
+ facts: {
171
+ style: {
172
+ value: 'pg-tap',
173
+ source: 'vendor-stated',
174
+ cite: "the ProductFinder index groups these under product_group_name 'Tapping collet TAP'",
175
+ },
176
+ },
177
+ },
178
+ 'regofix_pg_collets_long.csv': {
179
+ catalogName: 'REGO-FIX powRgrip PG Long Collets',
180
+ rows: 21,
181
+ brand: 'regofix',
182
+ facts: {
183
+ style: {
184
+ value: 'pg-long',
185
+ source: 'vendor-stated',
186
+ cite: "the ProductFinder index groups these under product_group_name 'Long'",
187
+ },
188
+ },
189
+ },
190
+ 'regofix_pg_collets_microbore.csv': {
191
+ catalogName: 'REGO-FIX powRgrip PG Microbore Collets',
192
+ rows: 16,
193
+ brand: 'regofix',
194
+ facts: {
195
+ style: {
196
+ value: 'pg-microbore',
197
+ source: 'vendor-stated',
198
+ cite: "the ProductFinder index groups these under product_group_name 'Microbore'",
199
+ },
200
+ },
201
+ },
202
+ 'regofix_pg_collets_turning.csv': {
203
+ catalogName: 'REGO-FIX powRgrip PG Turning Collets',
204
+ rows: 16,
205
+ brand: 'regofix',
206
+ facts: {
207
+ style: {
208
+ value: 'pg-turning',
209
+ source: 'vendor-stated',
210
+ cite: "the ProductFinder index groups these under product_group_name 'PG-T'",
211
+ },
212
+ },
213
+ },
214
+ 'regofix_pg_collets_mql.csv': {
215
+ catalogName: 'REGO-FIX powRgrip PG MQL Collets',
216
+ rows: 11,
217
+ brand: 'regofix',
218
+ facts: {
219
+ style: {
220
+ value: 'pg-mql',
221
+ source: 'vendor-stated',
222
+ cite: "the ProductFinder index groups these under product_group_name 'PG-MQL'",
223
+ },
224
+ },
225
+ },
226
+ 'regofix_pg_collets_securgrip.csv': {
227
+ catalogName: 'REGO-FIX powRgrip PG secuRgrip Collets',
228
+ rows: 10,
229
+ brand: 'regofix',
230
+ facts: {
231
+ style: {
232
+ value: 'pg-securgrip',
233
+ source: 'vendor-stated',
234
+ cite: "the ProductFinder index groups these under product_group_name 'secuRgrip'",
235
+ },
236
+ },
237
+ },
238
+ 'regofix_pg_collets_sealed_cap.csv': {
239
+ catalogName: 'REGO-FIX powRgrip PG Sealed Cap Collets',
240
+ rows: 7,
241
+ brand: 'regofix',
242
+ facts: {
243
+ style: {
244
+ value: 'pg-sealed-cap',
245
+ source: 'vendor-stated',
246
+ cite: "the ProductFinder index groups these under product_group_name 'PG-SC'",
247
+ },
248
+ },
249
+ },
250
+ };
@@ -0,0 +1,130 @@
1
+ /**
2
+ * What a family declares, and what it looks like once the registry has bound it.
3
+ *
4
+ * `families/` is the config table: what to fetch, how its columns are
5
+ * labelled, and the per-family constants no vendor table states. This module
6
+ * is the *shape* of one, kept apart from the tables themselves so that
7
+ * `families/` stays data and `registry` stays the only module that knows both
8
+ * halves.
9
+ *
10
+ * ## Facts are the only place a constant is authored
11
+ *
12
+ * A constant is authored as a {@link Fact} and nowhere else:
13
+ * {@link FamilyDefinition} declares `facts` and no constant keys, so setting
14
+ * one directly does not compile. The projection still happens —
15
+ * {@link BoundFamily} carries the values under their own names, and a mapper
16
+ * says `family.pointAngle` and never learns about provenance — but there is
17
+ * only ever one authored copy to drift from.
18
+ */
19
+ import type { UnitSystem } from './conventions.js';
20
+ import type { BrandName } from './identity.js';
21
+ import type { Fact } from './provenance.js';
22
+ import type { ColumnMap, ToolKind, ToolRecord } from './records.js';
23
+ import type { MapperOptions, ScrapedRow } from './scrape.js';
24
+ /**
25
+ * The per-family constants a fact can carry, and their types.
26
+ *
27
+ * Ten keys, which is the whole vocabulary the catalog uses. Naming them rather
28
+ * than accepting any string is what lets a mapper read `family.coolantThrough`
29
+ * as a `boolean` instead of casting an `unknown` out of a bag — and what makes
30
+ * a fact whose value is the wrong type a compile error where the family is
31
+ * written.
32
+ */
33
+ export interface FamilyFacts {
34
+ /** Which unit system this family's dimensional columns are published in. */
35
+ unit?: UnitSystem;
36
+ /** Cutting-material code — `carbide`, `hss`, `diamond`. */
37
+ bmc?: string;
38
+ coolantThrough?: boolean;
39
+ flutes?: number;
40
+ /** Degrees included. */
41
+ pointAngle?: number;
42
+ nonFerrous?: boolean;
43
+ /** The holder or collet product style, as the vendor names it. */
44
+ style?: string;
45
+ /** `BT30`, `CAT40` — the spindle interface. */
46
+ taper?: string;
47
+ /** How the holder grips: `collet`, `hydraulic`, `shrink`. */
48
+ clamping?: string;
49
+ /** `taper` or `face` — whether the flange face seats as well as the cone. */
50
+ contact?: string;
51
+ }
52
+ /** A family's facts, each carrying the type its projected value will have. */
53
+ export type FactSet = {
54
+ [K in keyof FamilyFacts]?: Fact<NonNullable<FamilyFacts[K]>>;
55
+ };
56
+ /** One CSV row -> one record. An adapter supplies these, keyed by tool kind. */
57
+ export type RecordMapper = (row: ScrapedRow, family: BoundFamily, columns: ColumnMap, options?: MapperOptions) => ToolRecord;
58
+ /** A vendor adapter's mappers, by the kind of tool they build. */
59
+ export type RecordMappers = Partial<Record<ToolKind, RecordMapper>>;
60
+ /** What every family declares, cutting tool or toolholding alike. */
61
+ interface CommonDefinition {
62
+ /**
63
+ * How many rows a human counted in this CSV at scrape time.
64
+ *
65
+ * The one key here that no code needs. It is an independent restatement,
66
+ * which is the whole value: every other count is computed from the same file
67
+ * it is checking, so a scrape that silently lost rows agrees with itself. It
68
+ * is per family and not a total, because a total hides the case it exists to
69
+ * catch — one family gaining a row while another loses one sums to no change.
70
+ */
71
+ readonly rows: number;
72
+ /** Defaults to `kennametal` when a family does not name one. */
73
+ readonly brand?: BrandName;
74
+ readonly facts?: FactSet;
75
+ }
76
+ /**
77
+ * A cutting-tool family: something with a column map and a record mapper.
78
+ *
79
+ * `columns` holds the vendor's own column labels keyed by canonical ISO 13399
80
+ * name, *without* a unit suffix — `registry` runs each through
81
+ * `records.checkColumnMap`, so a typo fails when the registry binds, naming
82
+ * the family.
83
+ */
84
+ export interface FamilyDefinition extends CommonDefinition {
85
+ /**
86
+ * The vendor-local half of the family id, kebab-cased.
87
+ *
88
+ * What lets a REGO-FIX family be called whatever REGO-FIX calls it without
89
+ * checking Kennametal's list first — see {@link familyId}.
90
+ */
91
+ readonly id: string;
92
+ readonly kind: ToolKind;
93
+ /**
94
+ * The vendor's own code for the family, where the scrape target is a family
95
+ * page. Absent where it is a set of index filters instead.
96
+ */
97
+ readonly familyCode?: string;
98
+ readonly columns: Readonly<Record<string, string>>;
99
+ }
100
+ /** A holder or collet family: no column map, no record mapper, no kind. */
101
+ export interface ToolholdingDefinition extends CommonDefinition {
102
+ /** What a human calls this family. Holders and collets have no `id`. */
103
+ readonly catalogName: string;
104
+ }
105
+ /** A cutting-tool family after {@link FamilyDefinition} has been validated. */
106
+ export type BoundFamily = Omit<FamilyDefinition, 'columns'> & FamilyFacts & {
107
+ /** The validated map. Only `checkColumnMap` can produce one. */
108
+ readonly columns: ColumnMap;
109
+ /** The adapter that turns this family's rows into records. */
110
+ readonly records: RecordMapper;
111
+ };
112
+ /** A toolholding family after its facts have been checked and projected. */
113
+ export type BoundToolholding = ToolholdingDefinition & FamilyFacts;
114
+ /**
115
+ * A family's id: `<brand>:<vendor-local id>`.
116
+ *
117
+ * Bare filename stems were the id in the source package until 2026-08-08 —
118
+ * `godrill_3xd_metric` — and they are a route parameter and a join key
119
+ * downstream. With one vendor there was no collision to fix; with four there
120
+ * is a latent one the moment two ship a family called `endmills_metric`, and
121
+ * it would land as a route that resolves to whichever library was read last.
122
+ *
123
+ * The **colon** is deliberate: it is a legal `pchar` in a URL path segment
124
+ * (RFC 3986), so `/family/kennametal:godrill-3xd-metric` needs no encoding and
125
+ * stays one route parameter.
126
+ */
127
+ export declare function familyId(cfg: FamilyDefinition | BoundFamily): string;
128
+ /** The brand that published a family, defaulting as the catalog does. */
129
+ export declare function familyBrand(cfg: CommonDefinition | BoundFamily | BoundToolholding): BrandName;
130
+ export {};
package/dist/family.js ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * What a family declares, and what it looks like once the registry has bound it.
3
+ *
4
+ * `families/` is the config table: what to fetch, how its columns are
5
+ * labelled, and the per-family constants no vendor table states. This module
6
+ * is the *shape* of one, kept apart from the tables themselves so that
7
+ * `families/` stays data and `registry` stays the only module that knows both
8
+ * halves.
9
+ *
10
+ * ## Facts are the only place a constant is authored
11
+ *
12
+ * A constant is authored as a {@link Fact} and nowhere else:
13
+ * {@link FamilyDefinition} declares `facts` and no constant keys, so setting
14
+ * one directly does not compile. The projection still happens —
15
+ * {@link BoundFamily} carries the values under their own names, and a mapper
16
+ * says `family.pointAngle` and never learns about provenance — but there is
17
+ * only ever one authored copy to drift from.
18
+ */
19
+ /**
20
+ * A family's id: `<brand>:<vendor-local id>`.
21
+ *
22
+ * Bare filename stems were the id in the source package until 2026-08-08 —
23
+ * `godrill_3xd_metric` — and they are a route parameter and a join key
24
+ * downstream. With one vendor there was no collision to fix; with four there
25
+ * is a latent one the moment two ship a family called `endmills_metric`, and
26
+ * it would land as a route that resolves to whichever library was read last.
27
+ *
28
+ * The **colon** is deliberate: it is a legal `pchar` in a URL path segment
29
+ * (RFC 3986), so `/family/kennametal:godrill-3xd-metric` needs no encoding and
30
+ * stays one route parameter.
31
+ */
32
+ export function familyId(cfg) {
33
+ return `${cfg.brand ?? 'kennametal'}:${cfg.id}`;
34
+ }
35
+ /** The brand that published a family, defaulting as the catalog does. */
36
+ export function familyBrand(cfg) {
37
+ return cfg.brand ?? 'kennametal';
38
+ }