@instruments/materialgraph 0.29.0 → 0.31.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 (89) hide show
  1. package/brands-client.d.ts +1 -1
  2. package/by-colour-types.d.ts +4 -4
  3. package/catalogue-client.d.ts +3 -3
  4. package/catalogue-client.js +3 -3
  5. package/catalogue-types.d.ts +1 -1
  6. package/client-vocabulary.d.ts +3 -5
  7. package/client-vocabulary.js +9 -6
  8. package/client.d.ts +85 -174
  9. package/client.js +98 -211
  10. package/colour-region-types.d.ts +18 -0
  11. package/colours-client.d.ts +10 -83
  12. package/colours-client.js +1 -85
  13. package/errors.d.ts +1 -1
  14. package/errors.js +1 -0
  15. package/find-companions-types.d.ts +70 -0
  16. package/find-companions-types.js +12 -0
  17. package/find-variants-by-colour-types.d.ts +197 -0
  18. package/find-variants-by-vector-types.d.ts +67 -0
  19. package/find-variants-by-vector-types.js +5 -0
  20. package/get-variant-types.d.ts +50 -0
  21. package/get-variant-types.js +10 -0
  22. package/http-transport.d.ts +1 -1
  23. package/identifiers.d.ts +1 -1
  24. package/identifiers.js +1 -1
  25. package/image-reference-types.d.ts +18 -0
  26. package/index.d.ts +9 -7
  27. package/index.js +1 -1
  28. package/list-enum-values-types.d.ts +58 -0
  29. package/list-variants-types.d.ts +6 -2
  30. package/material-asset-types.d.ts +2 -2
  31. package/material-types.d.ts +13 -151
  32. package/operations.d.ts +1 -1
  33. package/operations.js +27 -88
  34. package/package.json +1 -1
  35. package/palette-search-types.d.ts +3 -49
  36. package/resolve-colour-types.d.ts +5 -5
  37. package/resolve-variant-types.d.ts +75 -0
  38. package/{identify-types.js → resolve-variant-types.js} +2 -2
  39. package/response-schemas.generated.d.ts +1 -1
  40. package/response-schemas.generated.js +46 -84
  41. package/run-types.d.ts +23 -216
  42. package/runs.d.ts +10 -58
  43. package/runs.js +19 -106
  44. package/schema-resource-types.d.ts +2 -139
  45. package/schema-resource-types.js +0 -8
  46. package/search-card-types.d.ts +1 -1
  47. package/search-index-plan-types.d.ts +6 -2
  48. package/search-request-types.d.ts +10 -2
  49. package/spec-types.d.ts +4 -4
  50. package/spec-types.js +2 -2
  51. package/specifications.d.ts +1 -9
  52. package/specifications.js +0 -12
  53. package/sse.d.ts +2 -2
  54. package/sse.js +2 -2
  55. package/types.d.ts +4 -38
  56. package/validation.d.ts +0 -74
  57. package/validation.js +2 -116
  58. package/webmcp.generated.js +9 -9
  59. package/workflows-client.d.ts +54 -0
  60. package/workflows-client.js +41 -0
  61. package/client-source-material-comments.d.ts +0 -51
  62. package/client-source-material-comments.js +0 -6
  63. package/codes-client.d.ts +0 -118
  64. package/codes-client.js +0 -99
  65. package/codes-types.d.ts +0 -325
  66. package/colour-aggregate-types.d.ts +0 -163
  67. package/coordinate-client.d.ts +0 -23
  68. package/coordinate-client.js +0 -13
  69. package/demand-batch-types.d.ts +0 -118
  70. package/demand-types.d.ts +0 -154
  71. package/identify-types.d.ts +0 -82
  72. package/material-composition-client.d.ts +0 -21
  73. package/material-composition-client.js +0 -13
  74. package/material-composition-types.d.ts +0 -403
  75. package/material-composition-types.js +0 -1
  76. package/source-material-validation.d.ts +0 -7
  77. package/source-material-validation.js +0 -214
  78. package/spec-facet-types.d.ts +0 -48
  79. package/spec-facet-types.js +0 -7
  80. package/substitute-types.d.ts +0 -102
  81. package/substitute-types.js +0 -1
  82. package/suggest-types.d.ts +0 -67
  83. package/suggest-types.js +0 -28
  84. package/taxonomy-coverage-types.d.ts +0 -34
  85. package/taxonomy-coverage-types.js +0 -1
  86. /package/{codes-types.js → colour-region-types.js} +0 -0
  87. /package/{colour-aggregate-types.js → find-variants-by-colour-types.js} +0 -0
  88. /package/{demand-batch-types.js → image-reference-types.js} +0 -0
  89. /package/{demand-types.js → list-enum-values-types.js} +0 -0
@@ -1,403 +0,0 @@
1
- /**
2
- * Hand-mirrored wire types for MATERIAL composition — what a product is made
3
- * of (`GET /api/v1/materials/{variantId}/composition`, operation
4
- * `get_material_composition`, and `GET /api/v1/composition/report`, operation
5
- * `get_composition_report`; MG-1969).
6
- *
7
- * Composition means only this. It is unrelated to `./slot-pool-types.ts`,
8
- * the candidate pools a scheme composer outside MaterialGraph reads.
9
- *
10
- * Like the rest of the SDK these interfaces carry NO runtime dependency on any
11
- * `@materialgraph/*` package; they mirror the `VariantComposition` and
12
- * `CompositionCorpusSummary` shapes in `apps/web/public/openapi.yaml`.
13
- *
14
- * TOLERANT-PARSING CONTRACT: response enums are widened with {@link OpenEnum}
15
- * so a new server value still type-checks; objects are read structurally. The
16
- * one exception is {@link VariantCompositionAbsence}, which is closed —
17
- * see below.
18
- */
19
- import type { VariantId } from "./identifiers.js";
20
- import type { OpenEnum } from "./types.js";
21
- /**
22
- * Why no parse stands behind a material. CLOSED on purpose: a consumer must
23
- * branch on every arm, and widening it would let a new reason fall through a
24
- * branch that reads "one of these two, so it must be the other".
25
- *
26
- * - `no_material_bank_sku` — the variant was never a Material Bank SKU, so
27
- * there is no catalogue row anyone could have parsed.
28
- * - `not_linked` — it has a SKU and the corpus has not reached it. A statement
29
- * about MaterialGraph's coverage, never about the material.
30
- *
31
- * Neither is the same as a present parse whose `status` is `empty`: that is the
32
- * catalogue row saying it declared nothing, which is knowledge.
33
- */
34
- export type VariantCompositionAbsence = "no_material_bank_sku" | "not_linked";
35
- /** `failed_closed` means the parser refused rather than guessed. */
36
- export type CompositionParseStatus = OpenEnum<"parsed" | "empty" | "failed_closed">;
37
- /** One document or catalogue column the record's facts are taken from. */
38
- export type CompositionSource = {
39
- id: string;
40
- kind: OpenEnum<"epd" | "hpd" | "technical_document" | "manufacturer" | "reseller" | "test_report" | "catalogue_field" | "inference" | "other">;
41
- /** A URL, a document name, or the catalogue column name. */
42
- locator: string;
43
- retrievedAt?: string;
44
- /** The source's wording, verbatim. Never corrected or completed. */
45
- statement?: string;
46
- title?: string;
47
- };
48
- /**
49
- * A percentage as the source gives it, or one of the four ways a source can
50
- * name a node and still withhold the number. `undisclosed` holds its place in
51
- * the sum; `alternate` sits outside it.
52
- */
53
- export type CompositionAmount = {
54
- kind: "exact";
55
- value: number;
56
- } | {
57
- kind: "range";
58
- max: number;
59
- min: number;
60
- } | {
61
- kind: "bound";
62
- operator: OpenEnum<"at_most" | "at_least" | "less_than" | "greater_than">;
63
- value: number;
64
- } | {
65
- kind: "unknown";
66
- } | {
67
- kind: "undisclosed";
68
- reason: OpenEnum<"trade_secret" | "not_provided">;
69
- } | {
70
- kind: "conditionally_present";
71
- max: number;
72
- } | {
73
- kind: "band";
74
- band: string;
75
- version: string;
76
- vocabulary: string;
77
- } | {
78
- kind: "alternate";
79
- } | {
80
- kind: string;
81
- };
82
- /** How much of its parent, or of the whole product, a node is. */
83
- export type CompositionShare = {
84
- amount: CompositionAmount;
85
- /** What the percentage measures, only as the source says it. Never converted. */
86
- basis: OpenEnum<"mass" | "volume" | "fibre_mass" | "area" | "count" | "thickness" | "unspecified">;
87
- of: OpenEnum<"parent" | "product">;
88
- source: string;
89
- };
90
- /** An absolute figure the source states, in its own unit. Never in the sum. */
91
- export type CompositionQuantity = {
92
- amount: {
93
- kind: "exact";
94
- value: number;
95
- } | {
96
- kind: "range";
97
- max: number;
98
- min: number;
99
- } | {
100
- kind: "bound";
101
- operator: OpenEnum<"at_most" | "at_least" | "less_than" | "greater_than">;
102
- value: number;
103
- } | {
104
- kind: string;
105
- };
106
- /** The figure and unit as the source writes them. */
107
- raw: string;
108
- unit: OpenEnum<"mm" | "in" | "mil" | "gauge" | "oz_yd2" | "g_m2" | "kg" | "kg_m3" | "pcf" | "dtex" | "denier" | "ply">;
109
- };
110
- /**
111
- * Which substance a component is. `resolution` is the field to read before
112
- * trusting `term`: `identified` is the only arm carrying a dictionary id that
113
- * means what the source said.
114
- */
115
- export type CompositionTermRef = {
116
- /** The source's own words, verbatim. */
117
- raw: string;
118
- resolution: OpenEnum<"identified" | "proprietary" | "withheld" | "unresolved">;
119
- source?: string;
120
- term?: string;
121
- };
122
- /** Dictionary ids qualifying a term without changing it. */
123
- export type CompositionModifiers = {
124
- brand?: string;
125
- cut?: string;
126
- grade?: string;
127
- origin?: string;
128
- process?: string[];
129
- provenance?: string;
130
- treatment?: string[];
131
- };
132
- /** A statement about a node that is not a share. Claims never enter the sum. */
133
- export type CompositionClaim = {
134
- amount?: CompositionAmount;
135
- kind: OpenEnum<"free_of" | "contains" | "recyclable" | "renewable" | "bio_based" | "biodegradable" | "compostable" | "certified" | "other">;
136
- /** The claim's wording, verbatim. */
137
- raw: string;
138
- source: string;
139
- term?: string;
140
- };
141
- /** The level above which the source says every substance is listed. */
142
- export type CompositionThreshold = {
143
- raw: string;
144
- regime: OpenEnum<"100_ppm" | "1000_ppm" | "ghs_sds" | "other">;
145
- scope: OpenEnum<"product" | "material">;
146
- source: string;
147
- };
148
- /**
149
- * Whether the source presents a node's children as exhaustive. `partial` and
150
- * `unspecified` are both statements about the SOURCE; neither licenses adding
151
- * a child to make a list add up.
152
- */
153
- export type CompositionCoverage = {
154
- claim: OpenEnum<"complete" | "partial" | "unspecified">;
155
- source?: string;
156
- threshold?: CompositionThreshold;
157
- };
158
- /** Why the unstated part of a composition is unstated. */
159
- export type CompositionRemainder = {
160
- kind: "legally_omitted";
161
- regime: OpenEnum<"eu_1007_2011" | "us_16_cfr_303">;
162
- rule: string;
163
- source: string;
164
- } | {
165
- kind: "undisclosed";
166
- reason: OpenEnum<"trade_secret" | "not_provided">;
167
- source: string;
168
- } | {
169
- kind: string;
170
- };
171
- /** One named substance with its share. Never an unstated remainder. */
172
- export type CompositionComponent = {
173
- claims: CompositionClaim[];
174
- /** "Of which" subsets of this component; their shares are of it. */
175
- components?: CompositionComponent[];
176
- id: string;
177
- identity: CompositionTermRef;
178
- modifiers: CompositionModifiers;
179
- quantity?: CompositionQuantity;
180
- /** What the substance does inside its part, only where the source says. */
181
- role?: string;
182
- share: CompositionShare;
183
- source: string;
184
- };
185
- /** A layer or piece of the product. Sibling parts are co-present and sum. */
186
- export type CompositionPart = {
187
- claims: CompositionClaim[];
188
- components: CompositionComponent[];
189
- construction: string[];
190
- coverage: CompositionCoverage;
191
- id: string;
192
- /** The part's name as the source writes it. */
193
- label?: string;
194
- /** Alternatives the buyer chooses between; options never sum with siblings. */
195
- options?: CompositionPart[];
196
- parts?: CompositionPart[];
197
- /** Stacking order among siblings: 0 is the top or outer face. */
198
- position: number;
199
- quantity?: CompositionQuantity;
200
- remainder?: CompositionRemainder;
201
- role?: string;
202
- share: CompositionShare;
203
- source: string;
204
- };
205
- /** The composition of one product, as the cited sources state it. */
206
- export type CompositionRecord = {
207
- claims: CompositionClaim[];
208
- /** The whole product's own inventory; never summed with `parts`. */
209
- components: CompositionComponent[];
210
- construction: string[];
211
- coverage: CompositionCoverage;
212
- id: string;
213
- name: string;
214
- /** Alternatives the buyer chooses between, each a full record. */
215
- options?: CompositionRecord[];
216
- parts: CompositionPart[];
217
- primarySource?: string;
218
- remainder?: CompositionRemainder;
219
- sources: CompositionSource[];
220
- };
221
- /**
222
- * The record flattened to the `composition_weighted` attribute value.
223
- *
224
- * `basis: "tokens_only"` says the PROPORTIONS are unknown, never that the
225
- * components are, and every entry then carries a component with no `fraction`.
226
- * An absent `fraction` is an unknown share; `0` is a declared none.
227
- */
228
- export type CompositionWeightedValue = {
229
- basis: OpenEnum<"declared_percent" | "single_material" | "tokens_only">;
230
- components: {
231
- /** A `composition_component` dictionary term id. */
232
- component: string;
233
- /** 0 to 1, never a percentage. */
234
- fraction?: number;
235
- tolerance?: number;
236
- }[];
237
- [key: string]: unknown;
238
- };
239
- /** What meeting a need lets a reader do with the record. */
240
- export type CompositionUnlock = OpenEnum<"specify" | "substitute" | "compare">;
241
- /**
242
- * Why a record was not measured against a bar. Null exactly when
243
- * `completeness` is present. CLOSED, like {@link VariantCompositionAbsence}:
244
- * a consumer must branch on every arm.
245
- *
246
- * - `no_record` — nothing was parsed, so there is nothing to measure.
247
- * - `no_product_type` — a record exists, but the tuple resolved to no product
248
- * type, so there is no bar to pick.
249
- * - `no_bar_set` — both exist and MaterialGraph has authored no requirement
250
- * for that product type. A gap in OUR registry. Rendering it as an unscored
251
- * record reads as a fault in the maker's data.
252
- */
253
- export type CompositionCompletenessAbsence = "no_record" | "no_product_type" | "no_bar_set";
254
- /**
255
- * A record measured against its product type's authored bar, AT READ TIME.
256
- * Absent (rather than zeroed) when it could not be measured; read
257
- * `completenessAbsent` for which of the three reasons applies.
258
- */
259
- export type CompositionCompleteness = {
260
- met: number;
261
- needs: {
262
- id: string;
263
- label: string;
264
- met: boolean;
265
- unlocks: CompositionUnlock[];
266
- }[];
267
- productTypeId: string;
268
- /** met / total, 0 to 1, to three decimal places. */
269
- share: number;
270
- total: number;
271
- /** Open when every need tagged with it is met. */
272
- unlocks: {
273
- compare: boolean;
274
- specify: boolean;
275
- substitute: boolean;
276
- };
277
- };
278
- /** The stored parse, with the contract it was written under. */
279
- export type VariantCompositionParse = {
280
- parseKey: string;
281
- status: CompositionParseStatus;
282
- /** Null when the parse stored none; `empty` and `failed_closed` always do. */
283
- record: CompositionRecord | null;
284
- projection: CompositionWeightedValue | null;
285
- /** Unresolved wording, or the error text when the parse failed closed. */
286
- unresolved: {
287
- raw: string;
288
- source: string;
289
- }[];
290
- modelId: string;
291
- schemaVersion: string;
292
- dictionaryVersion: string;
293
- parsedAt: string;
294
- };
295
- /**
296
- * What a material is made of, traced to the catalogue columns it came from.
297
- *
298
- * READ `absent` FIRST. When it is set there is no parse, and its value says
299
- * why. When it is null there is a parse, and `parse.status` may still be
300
- * `empty` — the catalogue row was read and declared nothing.
301
- *
302
- * `productType` is inferred for the TUPLE, which many products share, so it is
303
- * never a statement about this variant's own category.
304
- */
305
- export type VariantComposition = {
306
- variantId: VariantId;
307
- materialBankSku: string | null;
308
- /** The verbatim catalogue cells, keyed by column name. */
309
- catalogue: {
310
- tuple: Record<string, string>;
311
- } | null;
312
- parse: VariantCompositionParse | null;
313
- productType: {
314
- id: string;
315
- share: number | null;
316
- } | null;
317
- completeness: CompositionCompleteness | null;
318
- /** Null exactly when `completeness` is present. */
319
- completenessAbsent: CompositionCompletenessAbsence | null;
320
- absent: VariantCompositionAbsence | null;
321
- };
322
- /** A count in both currencies. The Live SKU figure is the answer. */
323
- export type CompositionCorpusWeight = {
324
- tuples: number;
325
- liveSkus: number;
326
- };
327
- export type CompositionCorpusShare = CompositionCorpusWeight & {
328
- liveSkuShare: number | null;
329
- tupleShare: number | null;
330
- };
331
- export type CompositionCorpusNeedCoverage = {
332
- id: string;
333
- label: string;
334
- unlocks: CompositionUnlock[];
335
- /** Share of the type's measured Live SKUs whose record meets this need. */
336
- liveSkuShare: number | null;
337
- tupleShare: number | null;
338
- };
339
- export type CompositionCorpusTypeCoverage = {
340
- productTypeId: string;
341
- measured: CompositionCorpusWeight;
342
- failedClosed: CompositionCorpusWeight;
343
- empty: CompositionCorpusWeight;
344
- needs: CompositionCorpusNeedCoverage[];
345
- /**
346
- * Share of the type's measured rows meeting EVERY need — the whole bar.
347
- * Not the same as an unlock being open, which only requires the needs
348
- * tagged with that unlock. Draw a completeness bar from this.
349
- */
350
- complete: {
351
- liveSkuShare: number | null;
352
- tupleShare: number | null;
353
- };
354
- unlocks: {
355
- compare: number | null;
356
- specify: number | null;
357
- substitute: number | null;
358
- };
359
- /** The need the most Live SKUs are missing, or null when every need is met. */
360
- mostMissedNeed: {
361
- id: string;
362
- label: string;
363
- liveSkus: number;
364
- } | null;
365
- };
366
- /**
367
- * How far the material composition corpus has got.
368
- *
369
- * `authoringQueue` lists product types SKUs were assigned to but no
370
- * requirement has been written for. They have NO BAR SET, which is why they
371
- * are listed apart rather than scored zero.
372
- */
373
- export type CompositionCorpusSummary = {
374
- completeness: {
375
- authoringQueue: (CompositionCorpusWeight & {
376
- productTypeId: string;
377
- })[];
378
- byProductType: CompositionCorpusTypeCoverage[];
379
- unknownProductType: CompositionCorpusWeight;
380
- };
381
- coverage: {
382
- failedClosed: CompositionCorpusWeight;
383
- /** Tuples with no text to parse at all; not a parser failure. */
384
- noValue: CompositionCorpusWeight;
385
- resolvable: CompositionCorpusShare;
386
- validity: CompositionCorpusShare;
387
- withValue: CompositionCorpusWeight;
388
- };
389
- dictionaryVersion: string;
390
- /** The corpus's own list-price spend, never a per-request figure. */
391
- tokens: {
392
- costUsd: {
393
- input: number;
394
- output: number;
395
- total: number;
396
- };
397
- inputTokens: number;
398
- outputTokens: number;
399
- };
400
- parses: number;
401
- linkedSkus: number;
402
- generatedAt: string;
403
- };
@@ -1 +0,0 @@
1
- export {};
@@ -1,7 +0,0 @@
1
- import type { PostSourceMaterialCommentResult, SourceMaterialBatchDetail, SourceMaterialCommentThread } from "./run-types.js";
2
- /** Validate a batch-detail body, returning it typed, or throw explaining why. */
3
- export declare const checkSourceMaterialBatchDetail: (value: unknown, batchId: string) => SourceMaterialBatchDetail;
4
- /** Validate a comment-thread body, returning it typed, or throw explaining why. */
5
- export declare const checkSourceMaterialCommentThread: (value: unknown, sourceId: string) => SourceMaterialCommentThread;
6
- /** Validate a posted-comment body, returning it typed, or throw explaining why. */
7
- export declare const checkPostSourceMaterialCommentResult: (value: unknown, sourceId: string) => PostSourceMaterialCommentResult;
@@ -1,214 +0,0 @@
1
- /**
2
- * The runtime validator for `GET /api/v1/source-material/{batchId}` — the
3
- * hand-rolled, zero-runtime-dependency mirror of
4
- * `sourceMaterialBatchDetailSchema` and its transitive closure
5
- * (`sourceMaterialBatchDetailFileSchema`, `sourceImageAnalysisSchema`,
6
- * `sourceImageAnalysisResultSchema`) in
7
- * `packages/sources/src/source-material/contract.ts`.
8
- *
9
- * WHY IT EXISTS. `contract-parity.test.ts` proves the SDK's STATIC types match
10
- * the canonical schemas; that is a compile-time proof about shapes and validates
11
- * nothing at run time. This is the run-time half, and it must ACCEPT and REJECT
12
- * exactly what the schema does — pinned by `source-material-parity.test.ts`.
13
- *
14
- * THREE CONTRACT PROPERTIES IT ENFORCES, each easy to lose in a refactor:
15
- * 1. `analyses` is REQUIRED and an array on every file. `[]` means "nothing
16
- * computed yet"; an absent field is a broken response, not an empty one.
17
- * Preprocessing is manual today and automatic later — only the trigger
18
- * changes, never this contract.
19
- * 2. `comments` is REQUIRED and an array on every file, and every root comment
20
- * carries a REQUIRED `replies` array. Same distinction: "nobody said
21
- * anything about this file" is a state the client draws, and an absent
22
- * field is a malformed response.
23
- * 3. `result` is a JSON record all the way down (`z.json()`), not `unknown`.
24
- * See `requireJsonValue` for why the schema is that strict and why this
25
- * mirror cannot actually fire over the wire.
26
- *
27
- * ENUMS ARE CLOSED here, matching the canonical `z.enum(…)` members, while the
28
- * published types are `OpenEnum` so a newer server value keeps type-checking.
29
- * The validator follows the SCHEMA: the type stays wider than what is proven,
30
- * which is imprecise but never a lie — the same choice
31
- * `search-result-validation.ts` makes for `basis`.
32
- *
33
- * Package-internal (not re-exported from `index.ts`): a published symbol is a
34
- * permanent contract, and promoting it later is additive.
35
- */
36
- import { MaterialGraphError } from "./errors.js";
37
- import { requireArray, requireBoolean, requireEnum, requireSafeInteger, requireJsonRecord, requireNonEmptyString, requireNullableTrimmedBoundedString, requireRecord, requireString, requireTrimmedBoundedString, } from "./validation.js";
38
- /** `sourceImageAnalysisKindSchema` members, verbatim. */
39
- const ANALYSIS_KINDS = ["scene_match"];
40
- /** `sourceImageAnalysisStatusSchema` members, verbatim. */
41
- const ANALYSIS_STATUSES = ["ok", "failed"];
42
- /** `sourceMaterialRoleSchema` members, verbatim. */
43
- const SOURCE_ROLES = [
44
- "brief",
45
- "reference_image",
46
- "document",
47
- "document_image",
48
- "inspiration_image",
49
- "unclassified",
50
- ];
51
- const detailFailure = (batchId) => (reason) => MaterialGraphError.unexpected(`GET /api/v1/source-material/${batchId} returned a malformed body: ${reason}`);
52
- const checkAnalysis = (value, what, fail) => {
53
- const record = requireRecord(value, what, fail);
54
- // `model` and `analysedAt` ARE `.min(1)` canonically — fields this endpoint
55
- // introduces, where an empty value is meaningless and `model` already cannot
56
- // be written empty. Parity here is achieved by the schema being strict, never
57
- // by this mirror freelancing past it.
58
- return {
59
- analysedAt: requireNonEmptyString(record.analysedAt, `${what}.analysedAt`, fail),
60
- kind: requireEnum(record.kind, ANALYSIS_KINDS, `${what}.kind`, fail),
61
- model: requireNonEmptyString(record.model, `${what}.model`, fail),
62
- result: requireJsonRecord(record.result, `${what}.result`, fail),
63
- status: requireEnum(record.status, ANALYSIS_STATUSES, `${what}.status`, fail),
64
- };
65
- };
66
- /** `sourceMaterialCommentAuthorSchema`'s `attribution` members, verbatim. */
67
- const COMMENT_ATTRIBUTIONS = ["reported"];
68
- /** The canonical cap on both author fields: `.trim().min(1).max(160)`. */
69
- const AUTHOR_FIELD_MAX = 160;
70
- /**
71
- * A reply, and the shared half of a root comment.
72
- *
73
- * `body`, `commentId` and `createdAt` are `.min(1)` canonically — fields this
74
- * endpoint introduces, where an empty value carries nothing a client could
75
- * render. `author.organisation` is nullable, NOT optional: `null` is the answer
76
- * for a note with no organisation, and an absent key is a malformed body.
77
- *
78
- * The author fields are `.trim().min(1).max(160)` canonically and are mirrored
79
- * as exactly that. A plain non-empty check — which is what this used to do —
80
- * disagreed with the schema in both directions: it accepted `" "`, which the
81
- * schema rejects once trimmed, and accepted a name of any length, which the
82
- * schema caps. A validator that is looser than the contract is a validator that
83
- * lets the two drift without anything failing.
84
- */
85
- const checkCommentBase = (record, what, fail) => {
86
- const author = requireRecord(record.author, `${what}.author`, fail);
87
- const recordedBy = requireRecord(record.recordedBy, `${what}.recordedBy`, fail);
88
- return {
89
- author: {
90
- // Closed here, matching the canonical `z.literal("reported")`, while the
91
- // published type is `OpenEnum` so a newer server value keeps
92
- // type-checking — the same split the analysis enums make.
93
- attribution: requireEnum(author.attribution, COMMENT_ATTRIBUTIONS, `${what}.author.attribution`, fail),
94
- name: requireTrimmedBoundedString(author.name, `${what}.author.name`, AUTHOR_FIELD_MAX, fail),
95
- organisation: requireNullableTrimmedBoundedString(author.organisation, `${what}.author.organisation`, AUTHOR_FIELD_MAX, fail),
96
- },
97
- body: requireNonEmptyString(record.body, `${what}.body`, fail),
98
- commentId: requireNonEmptyString(record.commentId, `${what}.commentId`, fail),
99
- createdAt: requireNonEmptyString(record.createdAt, `${what}.createdAt`, fail),
100
- recordedBy: {
101
- principalId: requireNonEmptyString(recordedBy.principalId, `${what}.recordedBy.principalId`, fail),
102
- },
103
- };
104
- };
105
- const checkComment = (value, what, fail) => {
106
- const record = requireRecord(value, what, fail);
107
- // Same reasoning as `analyses`: no `.default([])` canonically, so an absent
108
- // `replies` is malformed rather than empty.
109
- const replies = requireArray(record.replies, `${what}.replies`, fail);
110
- return {
111
- ...checkCommentBase(record, what, fail),
112
- replies: replies.map((reply, index) => checkCommentBase(requireRecord(reply, `${what}.replies[${index}]`, fail), `${what}.replies[${index}]`, fail)),
113
- };
114
- };
115
- const checkFile = (value, what, fail) => {
116
- const record = requireRecord(value, what, fail);
117
- // NOT `requireDefaultedArray`: the schema has no `.default([])`, so an absent
118
- // `analyses` is a malformed body rather than an empty one. Keeping that
119
- // distinction is the whole point of the always-an-array contract. `comments`
120
- // is required for the same reason: "nobody has said anything" and "the server
121
- // forgot to tell you" are different answers.
122
- const analyses = requireArray(record.analyses, `${what}.analyses`, fail);
123
- const comments = requireArray(record.comments, `${what}.comments`, fail);
124
- // Required, not defaulted: "you are seeing the whole thread" is a claim the
125
- // server makes or does not, never one a client should infer from a length.
126
- const commentsTruncated = requireBoolean(record.commentsTruncated, `${what}.commentsTruncated`, fail);
127
- // `requireString`, NOT `requireNonEmptyString`, for the fields this endpoint
128
- // INHERITS from the shared source-material contract (`sourceId`, `sha256`,
129
- // `url` — plus `batchId`/`createdAt` below). Those are plain `z.string()`
130
- // there and shared with the upload path; a client that rejected `""` would
131
- // refuse a response the server can legitimately produce, and tightening them
132
- // is a change to a contract this endpoint does not own.
133
- return {
134
- analyses: analyses.map((analysis, index) => checkAnalysis(analysis, `${what}.analyses[${index}]`, fail)),
135
- // `requireSafeInteger`, NOT `requireIntegerAtLeast(…, 0)`. Canonically these
136
- // are a bare `z.number().int()` on the INHERITED
137
- // `sourceMaterialFileReceiptSchema`, which carries no lower bound — so a
138
- // negative is accepted there and must be accepted here.
139
- //
140
- // A negative `bytes` is nonsense and a negative `pages` more so, and this is
141
- // still not the place to say so: a client validator holding an opinion the
142
- // contract does not is how the two drift, and how a response the server may
143
- // legitimately produce starts throwing at the boundary. Tightening those
144
- // fields is a canonical change to a schema shared with the upload path, and
145
- // belongs in its own ticket with this mirror following after.
146
- bytes: requireSafeInteger(record.bytes, `${what}.bytes`, fail),
147
- comments: comments.map((comment, index) => checkComment(comment, `${what}.comments[${index}]`, fail)),
148
- commentsTruncated,
149
- filename: requireString(record.filename, `${what}.filename`, fail),
150
- mediaType: requireString(record.mediaType, `${what}.mediaType`, fail),
151
- pages: record.pages === null
152
- ? null
153
- : requireSafeInteger(record.pages, `${what}.pages`, fail),
154
- role: requireEnum(record.role, SOURCE_ROLES, `${what}.role`, fail),
155
- sha256: requireString(record.sha256, `${what}.sha256`, fail),
156
- sourceId: requireString(record.sourceId, `${what}.sourceId`, fail),
157
- url: requireString(record.url, `${what}.url`, fail),
158
- };
159
- };
160
- /** Validate a batch-detail body, returning it typed, or throw explaining why. */
161
- export const checkSourceMaterialBatchDetail = (value, batchId) => {
162
- const fail = detailFailure(batchId);
163
- const record = requireRecord(value, "body", fail);
164
- const sources = requireArray(record.sources, "body.sources", fail);
165
- return {
166
- batchId: requireString(record.batchId, "body.batchId", fail),
167
- createdAt: requireString(record.createdAt, "body.createdAt", fail),
168
- sources: sources.map((file, index) => checkFile(file, `body.sources[${index}]`, fail)),
169
- };
170
- };
171
- const commentsFailure = (verb, sourceId) => (reason) => MaterialGraphError.unexpected(`${verb} .../sources/${sourceId}/comments returned a malformed body: ${reason}`);
172
- /** Validate a comment-thread body, returning it typed, or throw explaining why. */
173
- export const checkSourceMaterialCommentThread = (value, sourceId) => {
174
- const fail = commentsFailure("GET", sourceId);
175
- const record = requireRecord(value, "body", fail);
176
- const comments = requireArray(record.comments, "body.comments", fail);
177
- return {
178
- comments: comments.map((comment, index) => checkComment(comment, `body.comments[${index}]`, fail)),
179
- sourceId: requireString(record.sourceId, "body.sourceId", fail),
180
- truncated: requireBoolean(record.truncated, "body.truncated", fail),
181
- };
182
- };
183
- /** `storedSourceMaterialCommentSchema`'s discriminator members, verbatim. */
184
- const STORED_COMMENT_KINDS = ["root", "reply"];
185
- /**
186
- * Validate a stored comment, following its `kind` discriminator.
187
- *
188
- * The two branches are checked as the two DIFFERENT shapes they are: a root must
189
- * carry `replies`, a reply must carry `parentCommentId` and must not be assumed
190
- * to carry `replies` at all. Validating both as the root shape is exactly the
191
- * defect this discriminated union was added to fix, and a mirror that did it
192
- * would put the lie back on the client side.
193
- */
194
- const checkStoredComment = (value, what, fail) => {
195
- const record = requireRecord(value, what, fail);
196
- const kind = requireEnum(record.kind, STORED_COMMENT_KINDS, `${what}.kind`, fail);
197
- if (kind === "reply") {
198
- return {
199
- kind: "reply",
200
- ...checkCommentBase(record, what, fail),
201
- parentCommentId: requireNonEmptyString(record.parentCommentId, `${what}.parentCommentId`, fail),
202
- };
203
- }
204
- return { kind: "root", ...checkComment(record, what, fail) };
205
- };
206
- /** Validate a posted-comment body, returning it typed, or throw explaining why. */
207
- export const checkPostSourceMaterialCommentResult = (value, sourceId) => {
208
- const fail = commentsFailure("POST", sourceId);
209
- const record = requireRecord(value, "body", fail);
210
- return {
211
- comment: checkStoredComment(record.comment, "body.comment", fail),
212
- sourceId: requireString(record.sourceId, "body.sourceId", fail),
213
- };
214
- };
@@ -1,48 +0,0 @@
1
- export type SpecFacetValue = {
2
- value: string;
3
- variants: number;
4
- brands: number;
5
- label?: string;
6
- };
7
- /**
8
- * A NUMERIC facet's observed span and the coverage behind it.
9
- *
10
- * A range, not buckets — no bucketing scheme has been decided, and one invented
11
- * on the wire would be treated as canonical by everyone who read it. Both bounds
12
- * are INCLUSIVE, matching `specRange`.
13
- *
14
- * READ `variants` BEFORE TRUSTING A FILTER. It is how many variants carry a
15
- * value that parses as a number, i.e. how much of the corpus a range can reach;
16
- * a facet that exists says the attribute is queryable, not that it is populated.
17
- * `unparseableVariants` is the honesty companion: values recorded here that are
18
- * not numbers, which a range EXCLUDES rather than reading as zero.
19
- */
20
- export type SpecFacetRange = {
21
- min: number;
22
- max: number;
23
- variants: number;
24
- unparseableVariants: number;
25
- };
26
- export type SpecFacet = {
27
- attributeId: string;
28
- name: string;
29
- isArray: boolean;
30
- /**
31
- * How this facet is queried. `token` — pick a value from `values` and send it
32
- * as a `{field: attributeId, op: "in", values: [...]}` filter leaf.
33
- * `numeric` — `values` is EMPTY and `range` carries the span; send inclusive
34
- * bounds as `{field: attributeId, op: "gte"/"lte", value}` leaves instead.
35
- */
36
- kind: "token" | "numeric" | (string & {});
37
- brands: number;
38
- /** EMPTY for a `numeric` facet, which has no token vocabulary. */
39
- values: SpecFacetValue[];
40
- /** Present ONLY on a `numeric` facet. */
41
- range?: SpecFacetRange;
42
- description?: string;
43
- };
44
- export type SpecFacetsListResponse = {
45
- mode: "facets";
46
- count: number;
47
- facets: SpecFacet[];
48
- };
@@ -1,7 +0,0 @@
1
- // ── Specification facets + attribute values ─────────────────────────────────
2
- //
3
- // Hand-maintained mirror of the `SpecificationFacet` OpenAPI schema (the source
4
- // of truth on the wire); the `sdk-openapi-parity` test gates this against the
5
- // generated spec. Split out of `types.ts` to keep that file under its growth
6
- // baseline; re-exported from `types.ts` so importers see no change.
7
- export {};