@toolpath/tool-scraper 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/conventions.d.ts +36 -0
  2. package/dist/conventions.js +41 -0
  3. package/dist/family.d.ts +16 -2
  4. package/dist/holding.d.ts +396 -0
  5. package/dist/holding.js +360 -0
  6. package/dist/index.d.ts +23 -13
  7. package/dist/index.js +23 -13
  8. package/dist/node/cad-mirror.d.ts +58 -1
  9. package/dist/node/cad-mirror.js +56 -8
  10. package/dist/node/cli.d.ts +4 -1
  11. package/dist/node/cli.js +192 -18
  12. package/dist/node/holder-import.d.ts +223 -0
  13. package/dist/node/holder-import.js +379 -0
  14. package/dist/node/index.d.ts +1 -0
  15. package/dist/node/index.js +1 -0
  16. package/dist/node/paths.d.ts +16 -0
  17. package/dist/node/paths.js +20 -0
  18. package/dist/profiles.d.ts +265 -0
  19. package/dist/profiles.js +295 -0
  20. package/dist/registry.d.ts +61 -5
  21. package/dist/registry.js +109 -6
  22. package/dist/vendors/kennametal/holding.d.ts +35 -0
  23. package/dist/vendors/kennametal/holding.js +112 -0
  24. package/dist/vendors/kennametal/index.d.ts +1 -0
  25. package/dist/vendors/kennametal/index.js +1 -0
  26. package/dist/vendors/maritool/holding.d.ts +79 -0
  27. package/dist/vendors/maritool/holding.js +164 -0
  28. package/dist/vendors/maritool/index.d.ts +1 -0
  29. package/dist/vendors/maritool/index.js +1 -0
  30. package/dist/vendors/maritool/scrape.d.ts +37 -13
  31. package/dist/vendors/maritool/scrape.js +53 -14
  32. package/dist/vendors/regofix/holding.d.ts +35 -0
  33. package/dist/vendors/regofix/holding.js +108 -0
  34. package/dist/vendors/regofix/index.d.ts +1 -0
  35. package/dist/vendors/regofix/index.js +1 -0
  36. package/dist/vendors/regofix/scrape.js +2 -2
  37. package/package.json +1 -1
@@ -22,6 +22,7 @@
22
22
  * | `Description` carries the vendor's own free text | Harvey, MariTool |
23
23
  * | `contact` says how a holder seats | REGO-FIX, MariTool |
24
24
  * | `CST` names the collet series a holder takes | REGO-FIX, MariTool |
25
+ * | `Collet Series` names the series a collet is | Kennametal, REGO-FIX |
25
26
  * | `L1_in`/`L1_mm` carry a holder's gage length | REGO-FIX, MariTool |
26
27
  * | Unmapped vendor codes keep a `DIN_` prefix | REGO-FIX; rule is general |
27
28
  * | The identity columns | **broken** — see below |
@@ -134,6 +135,21 @@ export declare const CONTACT_COLUMN = "contact";
134
135
  * the same reason.
135
136
  */
136
137
  export declare const COLLET_SERIES_COLUMN = "CST";
138
+ /**
139
+ * The CSV column naming the series a collet **is** — `ER16`, `PG25`, `PGST15`.
140
+ *
141
+ * The other half of {@link COLLET_SERIES_COLUMN}, and deliberately a second
142
+ * column rather than the same one: `CST` is the series a *holder takes* and
143
+ * this is the series a *collet belongs to*, so the join between a holder family
144
+ * and a collet family is a comparison of the two. One column carrying both
145
+ * would make that join a row's comparison with itself.
146
+ *
147
+ * Vendor-neutral and here rather than in either adapter for the reason
148
+ * {@link CAD_COLUMN} is: Kennametal's ER collet tables and REGO-FIX's PG collet
149
+ * index both write it, and neither owns it. Both close the vendor's own spacing
150
+ * before writing, exactly as they do for `CST` — two spellings join to nothing.
151
+ */
152
+ export declare const COLLET_DESIGNATION_COLUMN = "Collet Series";
137
153
  /**
138
154
  * The CSV columns carrying a holder's gage length, one per unit system.
139
155
  *
@@ -206,6 +222,26 @@ export declare function dimensionalColumn(label: string, unit: string): string;
206
222
  * indistinguishable from a scrape that lost it.
207
223
  */
208
224
  export declare function identityColumns(brand: BrandName): readonly string[];
225
+ /**
226
+ * The identity column a human reads one part by.
227
+ *
228
+ * The catalog designation where the vendor publishes one, and the sole
229
+ * identifier where it does not — the **last** of {@link identityColumns},
230
+ * because that is the order the pair is written in and a deviation lists the
231
+ * one column its vendor has.
232
+ *
233
+ * A lookup rather than a read of the row, for the reason {@link identityColumns}
234
+ * gives: a header that is missing `ISO Catalog Number` is indistinguishable
235
+ * from a scrape that lost it, so a caller that fell back column by column would
236
+ * name half a family's files after part numbers and never say why.
237
+ *
238
+ * It exists because `node/cad-mirror.ts` names a mirrored file after the part
239
+ * it holds, and hardcoded `'ISO Catalog Number'` to do it. That is Kennametal's
240
+ * pair; MariTool publishes one number per part under `Material Number` and no
241
+ * catalog designation at all, so every one of its 357 STEP models was skipped
242
+ * with a warning that the row had no catalog number to name it.
243
+ */
244
+ export declare function catalogColumn(brand: BrandName): string;
209
245
  /**
210
246
  * Every identity column `brand` claims is really in the CSV header.
211
247
  *
@@ -22,6 +22,7 @@
22
22
  * | `Description` carries the vendor's own free text | Harvey, MariTool |
23
23
  * | `contact` says how a holder seats | REGO-FIX, MariTool |
24
24
  * | `CST` names the collet series a holder takes | REGO-FIX, MariTool |
25
+ * | `Collet Series` names the series a collet is | Kennametal, REGO-FIX |
25
26
  * | `L1_in`/`L1_mm` carry a holder's gage length | REGO-FIX, MariTool |
26
27
  * | Unmapped vendor codes keep a `DIN_` prefix | REGO-FIX; rule is general |
27
28
  * | The identity columns | **broken** — see below |
@@ -135,6 +136,21 @@ export const CONTACT_COLUMN = 'contact';
135
136
  * the same reason.
136
137
  */
137
138
  export const COLLET_SERIES_COLUMN = 'CST';
139
+ /**
140
+ * The CSV column naming the series a collet **is** — `ER16`, `PG25`, `PGST15`.
141
+ *
142
+ * The other half of {@link COLLET_SERIES_COLUMN}, and deliberately a second
143
+ * column rather than the same one: `CST` is the series a *holder takes* and
144
+ * this is the series a *collet belongs to*, so the join between a holder family
145
+ * and a collet family is a comparison of the two. One column carrying both
146
+ * would make that join a row's comparison with itself.
147
+ *
148
+ * Vendor-neutral and here rather than in either adapter for the reason
149
+ * {@link CAD_COLUMN} is: Kennametal's ER collet tables and REGO-FIX's PG collet
150
+ * index both write it, and neither owns it. Both close the vendor's own spacing
151
+ * before writing, exactly as they do for `CST` — two spellings join to nothing.
152
+ */
153
+ export const COLLET_DESIGNATION_COLUMN = 'Collet Series';
138
154
  /**
139
155
  * The CSV columns carrying a holder's gage length, one per unit system.
140
156
  *
@@ -236,6 +252,31 @@ export function dimensionalColumn(label, unit) {
236
252
  export function identityColumns(brand) {
237
253
  return IDENTITY_DEVIATIONS[brand] ?? IDENTITY_COLUMNS;
238
254
  }
255
+ /**
256
+ * The identity column a human reads one part by.
257
+ *
258
+ * The catalog designation where the vendor publishes one, and the sole
259
+ * identifier where it does not — the **last** of {@link identityColumns},
260
+ * because that is the order the pair is written in and a deviation lists the
261
+ * one column its vendor has.
262
+ *
263
+ * A lookup rather than a read of the row, for the reason {@link identityColumns}
264
+ * gives: a header that is missing `ISO Catalog Number` is indistinguishable
265
+ * from a scrape that lost it, so a caller that fell back column by column would
266
+ * name half a family's files after part numbers and never say why.
267
+ *
268
+ * It exists because `node/cad-mirror.ts` names a mirrored file after the part
269
+ * it holds, and hardcoded `'ISO Catalog Number'` to do it. That is Kennametal's
270
+ * pair; MariTool publishes one number per part under `Material Number` and no
271
+ * catalog designation at all, so every one of its 357 STEP models was skipped
272
+ * with a warning that the row had no catalog number to name it.
273
+ */
274
+ export function catalogColumn(brand) {
275
+ const columns = identityColumns(brand);
276
+ // Non-null: `IDENTITY_COLUMNS` has two entries and every deviation has at
277
+ // least one, which `tests/conventions.test.ts` holds the table to.
278
+ return columns[columns.length - 1];
279
+ }
239
280
  /**
240
281
  * Every identity column `brand` claims is really in the CSV header.
241
282
  *
package/dist/family.d.ts CHANGED
@@ -18,6 +18,7 @@
18
18
  */
19
19
  import type { UnitSystem } from './conventions.js';
20
20
  import type { BrandName } from './identity.js';
21
+ import type { HoldingMapper, ToolholdingKind } from './holding.js';
21
22
  import type { Fact } from './provenance.js';
22
23
  import type { ColumnMap, ToolKind, ToolRecord } from './records.js';
23
24
  import type { MapperOptions, ScrapedRow } from './scrape.js';
@@ -121,8 +122,21 @@ export type BoundFamily = Omit<FamilyDefinition, 'columns'> & FamilyFacts & {
121
122
  /** The adapter that turns this family's rows into records. */
122
123
  readonly records: RecordMapper;
123
124
  };
124
- /** A toolholding family after its facts have been checked and projected. */
125
- export type BoundToolholding = ToolholdingDefinition & FamilyFacts;
125
+ /**
126
+ * A toolholding family after its facts have been checked and projected.
127
+ *
128
+ * `kind` is projected by the registry from **which table declared the family**,
129
+ * rather than being a key on {@link ToolholdingDefinition}: the table is the
130
+ * fact, and a `kind` beside it would be a second copy free to disagree with it.
131
+ * `records` is the mapper that brand binds for that kind, and it is optional
132
+ * because a vendor's holders may be scraped long before anybody has read its
133
+ * columns — see `holding.HoldingMappers`.
134
+ */
135
+ export type BoundToolholding = ToolholdingDefinition & FamilyFacts & {
136
+ readonly kind: ToolholdingKind;
137
+ /** The adapter that turns this family's rows into records, where there is one. */
138
+ readonly records?: HoldingMapper;
139
+ };
126
140
  /**
127
141
  * A family's id: `<brand>:<vendor-local id>`.
128
142
  *
@@ -0,0 +1,396 @@
1
+ /**
2
+ * What a holder and a collet become, and the two gates that refuse one.
3
+ *
4
+ * `records.ts` is the cutting-tool half of this package's output. This is the
5
+ * toolholding half, and it is a separate module rather than a third `ToolKind`
6
+ * because the three vocabularies genuinely do not overlap: a tool answers `DC`,
7
+ * a flute count and a workpiece material group; a holder answers a taper, a
8
+ * clamping mode and a gage length; a collet answers a series and a capacity
9
+ * band. `families/index.ts` states the same rule for the config tables —
10
+ *
11
+ * > Separate tables rather than a `kind` on one, because a holder and a collet
12
+ * > are not variants of a thing.
13
+ *
14
+ * — and the records follow it. What they *do* share is identity, the unit rule
15
+ * and the guid space, which is why both live here and not in two more files:
16
+ * `identity.recordGuid` mints a holder and a tool into one namespace per brand,
17
+ * so a consumer building a catalog from both can refuse a collision between
18
+ * them.
19
+ *
20
+ * ## Two copies of a dimension, and only where something compares them
21
+ *
22
+ * A fit-bearing dimension is kept twice — once in the family's native unit for
23
+ * display, once in canonical millimetres for arithmetic. The reason is that fit
24
+ * and filtering are different questions: a 3/8 in shank is 9.525 mm and
25
+ * genuinely seats in a metric 9-10 mm collet, so a fit comparison must convert,
26
+ * while a range *filter* must still refuse to, because "between 9 and 10 mm" is
27
+ * not a question an inch tool answers.
28
+ *
29
+ * Only {@link HolderRecord.bore}, {@link HolderRecord.gaugeLength},
30
+ * {@link ColletRecord.clampMin} and {@link ColletRecord.clampMax} get a twin.
31
+ * Everything else — usable length, body diameter, adjustment range — is
32
+ * displayed and never compared, and a second copy of a number nothing reads is
33
+ * a field to keep in sync for free.
34
+ *
35
+ * **The twin is derived here rather than read from the other unit column**, and
36
+ * that is a deliberate departure from the reference implementation, which read
37
+ * `D1_mm` directly whatever the family's unit was. Vendors publish pairs that
38
+ * disagree — see {@link checkUnitAgreement} — and a record whose `bore` and
39
+ * `boreMm` came from two contradicting cells is one record stating two sizes.
40
+ * Deriving makes `boreMm` exactly `bore` in millimetres by construction, so the
41
+ * pair cannot drift and a test can pin one to the other.
42
+ *
43
+ * ## Which refusal a vendor's row earns
44
+ *
45
+ * The split is **blank versus wrong**, and it decides whether one bad part ends
46
+ * a family:
47
+ *
48
+ * - A cell the vendor left **blank** that a part cannot exist without — no gage
49
+ * length, no bore on a bore-clamping holder, no capacity on a collet — is an
50
+ * `errors.IncompletePartError`. `registry.toHolding` warns and drops the row,
51
+ * the same call `registry.toRecords` makes for a cutting tool, and for the
52
+ * same reason: MariTool leaves `Shank Size` blank on four HSK holders out of
53
+ * 527, and losing five families over four rows is not a trade worth making.
54
+ * - A value that is **present and unreadable**, or two present values that
55
+ * **contradict** — a clamping mode this package has no word for, a holder
56
+ * that names both a bore and a collet series, a collet whose nominal size
57
+ * falls outside its own published capacity — is an `errors.VendorResponseError`
58
+ * and stops the family. Those say the vendor's vocabulary or this package's
59
+ * reading of it has moved, and skipping past one quietly is how a scraper
60
+ * starts publishing a catalog nobody checked.
61
+ */
62
+ import { type UnitSystem } from './conventions.js';
63
+ import type { BoundToolholding } from './family.js';
64
+ import { type BrandName } from './identity.js';
65
+ import { type ScrapedRow, type Warn } from './scrape.js';
66
+ /**
67
+ * How a holder grips the thing it holds.
68
+ *
69
+ * Four values where the reference implementation had two. `bore` and `collet`
70
+ * are its own; `shrink` and `hydraulic` are here because MariTool's leaf
71
+ * categories already classify parts as those and the distinction is a real one
72
+ * a buyer makes — a shrink-fit holder needs an induction heater on the bench
73
+ * and a hydraulic chuck needs an actuation screw, where both are otherwise the
74
+ * same answer to "what fits in it".
75
+ *
76
+ * They are the same *fit* question, which is what {@link BORE_CLAMPINGS} says:
77
+ * all three grip a shank directly and are held to one rule. Kennametal's
78
+ * shrink-fit and hydraulic families declare `bore` and stay declaring it —
79
+ * the vendor states the mode as a bore and this package does not re-classify a
80
+ * family's own words. `style` is the finer axis and already carries
81
+ * `shrink-fit-gp` and `hydraulic-chuck`.
82
+ */
83
+ export type ClampingMode = 'bore' | 'collet' | 'shrink' | 'hydraulic';
84
+ /** Every {@link ClampingMode}, for a message that can list what it knows. */
85
+ export declare const CLAMPING_MODES: readonly ClampingMode[];
86
+ /**
87
+ * The clamping modes that grip a shank directly, and therefore publish a bore.
88
+ *
89
+ * One rule over three values rather than three rules: the gate in
90
+ * {@link checkHolder} asks whether a holder takes a shank or a collet, and
91
+ * every mode but `collet` takes a shank.
92
+ */
93
+ export declare const BORE_CLAMPINGS: readonly ClampingMode[];
94
+ /**
95
+ * Whether the flange face seats on the spindle face as well as the cone.
96
+ *
97
+ * Never defaulted. `taper` is the common case, and defaulting to it would
98
+ * record a dual-contact family as a plain cone on no evidence — the same
99
+ * silent-wrong-answer shape as a bore-clamping holder with no bore.
100
+ */
101
+ export type ContactMode = 'taper' | 'face';
102
+ /** Every {@link ContactMode}, for the same reason {@link CLAMPING_MODES} is a list. */
103
+ export declare const CONTACT_MODES: readonly ContactMode[];
104
+ /** What every toolholding record shares with every `records.ToolRecord`. */
105
+ export interface HoldingIdentity {
106
+ /** `holder` or `collet` — which of the two record types this is. */
107
+ readonly kind: ToolholdingKind;
108
+ /**
109
+ * The brand key the record was minted under — `identity.BRANDS`'s own key.
110
+ *
111
+ * Here for the reason `records.ToolRecord.brand` is: {@link HoldingIdentity.guid}
112
+ * is minted in this brand's namespace, so without the key the guid is
113
+ * underivable from the record.
114
+ */
115
+ readonly brand: BrandName;
116
+ /** What this brand's records call the vendor — `identity.BRANDS[brand].vendor`. */
117
+ readonly vendor: string;
118
+ /** `identity.recordGuid(brand, materialNumber)`, minted by the factories below. */
119
+ readonly guid: string;
120
+ readonly materialNumber: string;
121
+ readonly catalogNumber: string;
122
+ /**
123
+ * The vendor's own free text about this part, verbatim — `''` where the
124
+ * vendor publishes none.
125
+ *
126
+ * **Never a copy of another field**, the rule `records.ToolRecord.description`
127
+ * already states. The reference implementation set it to the catalog number
128
+ * on every holder and collet, which put one string in two fields and told a
129
+ * consumer nothing it did not already have. Kennametal and REGO-FIX publish
130
+ * no description column for toolholding, so `''` is the honest answer for
131
+ * both; MariTool publishes a product name and that is what its records carry.
132
+ */
133
+ readonly description: string;
134
+ readonly productLink: string;
135
+ /**
136
+ * Which unit system this record's native dimensions are in.
137
+ *
138
+ * **Per record, not per family.** REGO-FIX publishes `PG 25 Ø 6.0 mm` and
139
+ * `PG 25 Ø 1/4"` as two rows of one product group, and MariTool gages two
140
+ * parts on one listing page in different systems. A family-level constant
141
+ * would be contradicted row by row, which is why `families/maritool.ts`
142
+ * declares no `unit` fact and the scraper promotes an `L1_in`/`L1_mm` pair
143
+ * with one cell filled instead.
144
+ */
145
+ readonly unit: UnitSystem;
146
+ }
147
+ /** One holder — a spindle interface, a way of gripping, and a gage length. */
148
+ export interface HolderRecord extends HoldingIdentity {
149
+ readonly kind: 'holder';
150
+ /** The spindle interface, as the vendor designates it — `BT30`, `HSK63A`. */
151
+ readonly taper: string;
152
+ readonly contact: ContactMode;
153
+ readonly clamping: ClampingMode;
154
+ /** The vendor's own product style — `er-collet-chuck`, `shrink-fit-gp`. */
155
+ readonly style: string;
156
+ /**
157
+ * The collet series this holder takes, on a collet-clamping holder only.
158
+ *
159
+ * Joins to {@link ColletRecord.series}. Written exactly as the vendor
160
+ * designates it, so a `PGST15` collet matches no `PG25` holder — the
161
+ * conservative direction on purpose, because hiding a collet that would have
162
+ * fitted costs an option while offering one that does not fit costs a
163
+ * machinist a purchase.
164
+ */
165
+ readonly colletSeries: string | null;
166
+ /** `D1` — the bore a shank seats in, on a bore-clamping holder only. */
167
+ readonly bore: number | null;
168
+ readonly boreMm: number | null;
169
+ /** `L1` — gage line to nose. Required: without it there is no stickout. */
170
+ readonly gaugeLength: number;
171
+ readonly gaugeLengthMm: number;
172
+ /** `L2` — usable length. */
173
+ readonly usableLength: number | null;
174
+ /** `L9` — clamping length. */
175
+ readonly clampingLength: number | null;
176
+ /** `V` — the adjustment range. */
177
+ readonly adjustmentRange: number | null;
178
+ /** `D2` — body diameter. */
179
+ readonly bodyDiameter: number | null;
180
+ /** `D11` — lock-nut diameter. */
181
+ readonly lockNutDiameter: number | null;
182
+ /** `conventions.CAD_COLUMN`, or null where the vendor publishes no model. */
183
+ readonly cadModelUrl: string | null;
184
+ /** `conventions.CAD_DXF_COLUMN`, or null where the vendor publishes no profile. */
185
+ readonly cadDxfUrl: string | null;
186
+ }
187
+ /** One collet — a series, a capacity band, and the sizes in between. */
188
+ export interface ColletRecord extends HoldingIdentity {
189
+ readonly kind: 'collet';
190
+ /** `ER16`, `PG25`, `PGST15` — joins to {@link HolderRecord.colletSeries}. */
191
+ readonly series: string;
192
+ /** The vendor's own product style — `er-standard`, `pg-coolant-flush`. */
193
+ readonly style: string;
194
+ /** `D1` — the nominal size the vendor designates the collet by. */
195
+ readonly nominal: number | null;
196
+ /**
197
+ * `CCCN`/`CCCX` — the vendor's published clamping capacity, never derived.
198
+ *
199
+ * DIN 6499 is usually summarised as a 1 mm band, which is wrong at the small
200
+ * end of every series (`16ER010M` clamps 1.0 down to 0.5) and wrong by a
201
+ * whole millimetre on a sealed coolant-through collet, where the two are
202
+ * equal and the collet takes one exact size.
203
+ */
204
+ readonly clampMin: number;
205
+ readonly clampMax: number;
206
+ readonly clampMinMm: number;
207
+ readonly clampMaxMm: number;
208
+ /** `BDX` — body diameter. */
209
+ readonly bodyDiameter: number | null;
210
+ /** `LF` — functional length. */
211
+ readonly functionalLength: number | null;
212
+ /** `L` — overall length. */
213
+ readonly overallLength: number | null;
214
+ }
215
+ /** Either toolholding record. Narrow on {@link HoldingIdentity.kind}. */
216
+ export type HoldingRecord = HolderRecord | ColletRecord;
217
+ /**
218
+ * Which of the two toolholding tables a family came from.
219
+ *
220
+ * Not a key on `family.ToolholdingDefinition`: which table declares a family
221
+ * *is* the fact, and a `kind` beside it would be a second copy to disagree with
222
+ * it. `registry.boundToolholding` projects it onto the bound config, which is
223
+ * where the registry already knows the answer.
224
+ */
225
+ export type ToolholdingKind = 'holder' | 'collet';
226
+ /** One toolholding row -> one record. A vendor adapter supplies these. */
227
+ export type HolderMapper = (row: ScrapedRow, family: BoundToolholding, options?: {
228
+ warn?: Warn;
229
+ }) => HolderRecord;
230
+ /** The collet half of the same contract. */
231
+ export type ColletMapper = (row: ScrapedRow, family: BoundToolholding, options?: {
232
+ warn?: Warn;
233
+ }) => ColletRecord;
234
+ /**
235
+ * A vendor adapter's toolholding mappers, both optional.
236
+ *
237
+ * **Partial on purpose.** REGO-FIX publishes holders and collets, MariTool
238
+ * holders only, and Harvey, EMUGE and Destiny Tool neither. A brand absent from
239
+ * `registry.HOLDING_ADAPTERS`, or present with no mapper for the kind, keeps
240
+ * today's behaviour exactly: the scrape ends at rows and a receipt. That is
241
+ * what makes minting records additive rather than a break, and it is the honest
242
+ * state for a vendor whose columns nobody has read yet.
243
+ */
244
+ export interface HoldingMappers {
245
+ holder?: HolderMapper;
246
+ collet?: ColletMapper;
247
+ }
248
+ /** Either mapper, as the registry stores the one a family binds. */
249
+ export type HoldingMapper = HolderMapper | ColletMapper;
250
+ /**
251
+ * A per-family constant a toolholding mapper cannot proceed without.
252
+ *
253
+ * `family.fact`'s counterpart for a family with no `id` and no `ToolKind`. The
254
+ * two are separate rather than one widened function because the subject of the
255
+ * message differs: a cutting-tool family is named by its vendor-local id and a
256
+ * toolholding family by the catalog name a human reads.
257
+ *
258
+ * Refusing rather than defaulting, for the reason `family.fact` states: every
259
+ * default is a claim the family never made, and a missing `taper` becoming
260
+ * `''` ships a holder that fits no spindle and raises nothing.
261
+ */
262
+ export declare function holdingFact<T>(family: BoundToolholding, key: string, value: T | undefined): T;
263
+ /**
264
+ * A value the vendor left blank on one part, refused as an incomplete part.
265
+ *
266
+ * The toolholding counterpart of `columns.required`, and it throws the same
267
+ * type for the same reason: this is the one refusal `registry.toHolding` skips
268
+ * past, because a single part with an unpublished cell must not end a family's
269
+ * conversion. See `errors.IncompletePartError` for why the others must not be
270
+ * skipped alike.
271
+ */
272
+ export declare function published<T extends string | number>(value: T | null | undefined, what: string, label: string): T;
273
+ /**
274
+ * `value`, stated in `from`, as a number in `to` — a no-op when they agree.
275
+ *
276
+ * The one conversion every toolholding mapper makes, so that the rounding above
277
+ * is applied in one place rather than wherever somebody remembers to.
278
+ */
279
+ export declare function asUnit(value: number, from: UnitSystem, to: UnitSystem): number;
280
+ /** `value`, stated in `unit`, as millimetres. */
281
+ export declare function millimeters(value: number, unit: UnitSystem): number;
282
+ export declare function millimeters(value: number | null, unit: UnitSystem): number | null;
283
+ /**
284
+ * One dimension in `unit`, converted from the other system where that is all
285
+ * the vendor published.
286
+ *
287
+ * **The fallback is load-bearing, not defensive.** Kennametal's `D1` is a unit
288
+ * pair on the BT30 hydraulic chucks and metric-only on the HSK63A HP line — an
289
+ * *inch* family with no `D1_in` column at all. A bare suffixed read is correct
290
+ * on the first and yields null on the second, producing a holder with no bore:
291
+ * it matches no tool, raises nothing, and looks like an empty result rather
292
+ * than a bug.
293
+ *
294
+ * The grammar is `measure.fractionValue`'s, which is the package's one reader
295
+ * for a machinist's number and refuses a range rather than summing it.
296
+ */
297
+ export declare function dim(row: ScrapedRow, label: string, unit: UnitSystem): number | null;
298
+ /**
299
+ * Report where a vendor's own millimetre and inch cells disagree.
300
+ *
301
+ * **Vendors really do publish contradictory pairs.** Kennametal's `16ERSS0312`
302
+ * states `D1`'s metric cell as `0.3125` — the inch value sitting in the metric
303
+ * column, a factor of 25.4 out — and `25ER130M` publishes `CCCN` as both
304
+ * 12.0 mm and 0.437 in, which is 11.1 mm. Both are in the source HTML.
305
+ *
306
+ * **This reports; it does not gate.** Two disagreeing cells cannot say which
307
+ * one is wrong, so refusing the family would trade a knowable warning for an
308
+ * unusable pipeline, and correcting a cell here would make this package a place
309
+ * tool data is authored by hand. What protects the output instead is that
310
+ * {@link dim} reads the family's *native* column and never the other one, plus
311
+ * {@link checkCollet}'s native-unit test that a nominal size falls inside its
312
+ * own published capacity. Every disagreement found so far sits in the column
313
+ * {@link dim} ignores; escalate this to a gate if one ever lands in a native
314
+ * column.
315
+ *
316
+ * **The tolerance is the vendor's own rounding, not a percentage.** A relative
317
+ * tolerance cannot tell rounding from error at small sizes: `16ER010M`
318
+ * publishes 0.5 mm as `0.02` in, correct to the two decimals it states and
319
+ * 1.6 % off as a ratio. Half a unit in each column's last printed place is
320
+ * exactly the slack the printed precision allows, and it is nowhere near the
321
+ * 7.6 mm a value in the wrong column produces.
322
+ *
323
+ * Returns whether it warned, so a caller can count.
324
+ */
325
+ export declare function checkUnitAgreement(row: ScrapedRow, label: string, what: string, warn?: Warn): boolean;
326
+ /**
327
+ * One cell as a {@link ContactMode}, refusing a word this package cannot read.
328
+ *
329
+ * A `VendorResponseError` and not an incomplete part: a *blank* contact is the
330
+ * caller's `published` call, and a contact the vendor states in a word nobody
331
+ * has mapped is the vocabulary having moved.
332
+ */
333
+ export declare function contactMode(value: string, what: string): ContactMode;
334
+ /** One cell as a {@link ClampingMode}, on the same terms as {@link contactMode}. */
335
+ export declare function clampingMode(value: string, what: string): ClampingMode;
336
+ /**
337
+ * One cell as a {@link UnitSystem}, on the same terms as {@link contactMode}.
338
+ *
339
+ * For the vendor that states the unit per row rather than per family: REGO-FIX
340
+ * publishes `PG 25 Ø 6.0 mm` and `PG 25 Ø 1/4"` in one product group, so its
341
+ * collet scrape writes the system it read off each designation into a column.
342
+ */
343
+ export declare function unitSystem(value: string, what: string): UnitSystem;
344
+ /**
345
+ * Vendor HTML is a system boundary, so this validates rather than guarding
346
+ * against a caller mistake.
347
+ *
348
+ * The `clamping` discriminant and the fields it implies must agree. **That is
349
+ * what turns Kennametal's missing-bore case into a failed conversion instead of
350
+ * a holder that quietly fits nothing** — a bore-clamping holder with no bore is
351
+ * the exact shape of that bug, and it raises nothing anywhere else.
352
+ */
353
+ export declare function checkHolder(record: HolderRecord): void;
354
+ /**
355
+ * The same boundary rule for a collet.
356
+ *
357
+ * A collet with no capacity would match every shank or none depending on which
358
+ * way a comparison read a null, which is why {@link ColletRecord.clampMin} and
359
+ * {@link ColletRecord.clampMax} are not nullable and the mapper refuses the row
360
+ * before it gets here.
361
+ */
362
+ export declare function checkCollet(record: ColletRecord): void;
363
+ /**
364
+ * The nullable holder dimensions a mapper may simply not mention.
365
+ *
366
+ * They stay **required on the type** so a consumer reading a record never
367
+ * handles `undefined`; only the construction is optional, which is the shape
368
+ * `records.toolRecord` already has. Kennametal publishes every one of them and
369
+ * REGO-FIX four fewer, and writing `usableLength: null` five times in an
370
+ * adapter is how a null becomes a default nobody notices.
371
+ */
372
+ type OptionalHolderFields = 'colletSeries' | 'bore' | 'usableLength' | 'clampingLength' | 'adjustmentRange' | 'bodyDiameter' | 'lockNutDiameter' | 'cadModelUrl' | 'cadDxfUrl';
373
+ /** What a mapper supplies to build a holder; the rest is derived or minted. */
374
+ type HolderFields = Omit<HolderRecord, 'kind' | 'guid' | 'vendor' | 'productLink' | 'boreMm' | 'gaugeLengthMm' | OptionalHolderFields> & Partial<Pick<HolderRecord, OptionalHolderFields>>;
375
+ /**
376
+ * Build a {@link HolderRecord}: mint its guid, derive its millimetre twins, and
377
+ * refuse the states that cannot be true.
378
+ *
379
+ * `guid`, `vendor` and `productLink` are not inputs at all — every adapter
380
+ * minting them would be three copies of `recordGuid(brand, materialNumber)` to
381
+ * drift, on the value that is the join key for every downstream consumer. The
382
+ * nullable dimensions stay **required on the type** so a consumer reading a
383
+ * record never handles `undefined`; only the construction is optional, which is
384
+ * the shape `records.toolRecord` already has.
385
+ *
386
+ * The result is frozen: a record is an interchange value, and a mapper that
387
+ * mutated one would be reaching back across the seam this type exists to draw.
388
+ */
389
+ export declare function holderRecord(fields: HolderFields): HolderRecord;
390
+ /** The same, for a collet. REGO-FIX publishes none of these four. */
391
+ type OptionalColletFields = 'nominal' | 'bodyDiameter' | 'functionalLength' | 'overallLength';
392
+ /** What a mapper supplies to build a collet. */
393
+ type ColletFields = Omit<ColletRecord, 'kind' | 'guid' | 'vendor' | 'productLink' | 'clampMinMm' | 'clampMaxMm' | OptionalColletFields> & Partial<Pick<ColletRecord, OptionalColletFields>>;
394
+ /** Build a {@link ColletRecord}, on the same terms as {@link holderRecord}. */
395
+ export declare function colletRecord(fields: ColletFields): ColletRecord;
396
+ export {};