@instruments/materialgraph 0.9.1 → 0.11.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 (64) hide show
  1. package/brands-client.d.ts +48 -0
  2. package/brands-client.js +45 -0
  3. package/browse-types.d.ts +147 -0
  4. package/browse-types.js +1 -0
  5. package/by-colour-types.d.ts +105 -5
  6. package/client-browse.d.ts +13 -3
  7. package/client-browse.js +12 -2
  8. package/client-exploration.d.ts +9 -0
  9. package/client-exploration.js +7 -0
  10. package/client-source-material-comments.d.ts +51 -0
  11. package/client-source-material-comments.js +40 -0
  12. package/client.d.ts +64 -71
  13. package/client.js +90 -78
  14. package/colour-aggregate-types.d.ts +58 -0
  15. package/colours-client.d.ts +8 -2
  16. package/colours-client.js +15 -2
  17. package/composition-client.d.ts +33 -0
  18. package/composition-client.js +27 -0
  19. package/composition-types.d.ts +46 -0
  20. package/coordinate-client.d.ts +23 -0
  21. package/coordinate-client.js +25 -0
  22. package/errors.js +24 -8
  23. package/exploration-types.d.ts +110 -0
  24. package/exploration-types.js +1 -0
  25. package/fan-deck-types.d.ts +42 -0
  26. package/fan-deck-types.js +9 -0
  27. package/http-transport.js +24 -5
  28. package/index.d.ts +7 -3
  29. package/index.js +2 -2
  30. package/match-stream-types.d.ts +1 -1
  31. package/match-stream-types.js +16 -3
  32. package/match-stream.js +14 -4
  33. package/material-types.d.ts +1 -1
  34. package/operations.d.ts +1 -1
  35. package/operations.js +187 -26
  36. package/package.json +1 -1
  37. package/palette-search-types.d.ts +31 -0
  38. package/resolve-client.d.ts +22 -3
  39. package/resolve-client.js +31 -4
  40. package/resolve-types.d.ts +70 -1
  41. package/run-events.d.ts +1 -1
  42. package/run-events.js +4 -1
  43. package/run-types.d.ts +132 -0
  44. package/runs.d.ts +35 -1
  45. package/runs.js +69 -5
  46. package/schedule-types.d.ts +128 -0
  47. package/schema-resource-types.d.ts +1 -1
  48. package/search-refinement-types.d.ts +39 -4
  49. package/search-request-types.d.ts +172 -0
  50. package/search-request-types.js +1 -0
  51. package/search-result-validation.js +21 -4
  52. package/slot-pool-types.d.ts +370 -0
  53. package/slot-pool-types.js +1 -0
  54. package/source-material-validation.d.ts +5 -1
  55. package/source-material-validation.js +114 -5
  56. package/spec-facet-types.d.ts +53 -0
  57. package/spec-facet-types.js +7 -0
  58. package/specifications.d.ts +26 -0
  59. package/specifications.js +32 -0
  60. package/sse.js +3 -1
  61. package/suggest-types.js +3 -1
  62. package/types.d.ts +176 -216
  63. package/validation.d.ts +12 -0
  64. package/validation.js +23 -0
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The BRAND lens: the catalogue collection, one brand's profile, and one
3
+ * brand's whole colour range, as thin wrappers over the shared
4
+ * {@link HttpTransport}. `MaterialGraphClient.brands` / `.brand` /
5
+ * `.brandColourDeck` delegate here; the contract lives in this file.
6
+ *
7
+ * A brand is a COMPANY, not a catalogue. Every count these reads return is
8
+ * coverage — what MaterialGraph has ingested — and never a claim about how
9
+ * many products the maker sells.
10
+ */
11
+ import type { BrandCatalogListPage, BrandsOptions } from "./brand-types.js";
12
+ import type { BrandFanDeck } from "./fan-deck-types.js";
13
+ import { HttpTransport } from "./http-transport.js";
14
+ import type { BrandOptions, BrandProfileResponse } from "./types.js";
15
+ /**
16
+ * THE QUESTION: "whose catalogs are in here, and how big is each?" The SDK's
17
+ * one OFFSET-paginated surface: read `meta.total` for the collection size and
18
+ * `meta.hasMore` to keep walking. Instead use {@link brandProfile} for one
19
+ * brand's profile and trade signals.
20
+ */
21
+ export declare function listBrands(transport: HttpTransport, options?: BrandsOptions, signal?: AbortSignal): Promise<BrandCatalogListPage>;
22
+ /**
23
+ * THE QUESTION: "who is this brand, and what do they claim?" — profile plus the
24
+ * trade/spec signals ON RECORD. `trade` is keyed by attribute id and an ABSENT
25
+ * key means no value is held — never infer `false`. Instead use `search()` to
26
+ * find their materials; this describes the company, not the catalog.
27
+ */
28
+ export declare function brandProfile(transport: HttpTransport, brandSlug: string, options?: BrandOptions, signal?: AbortSignal): Promise<BrandProfileResponse>;
29
+ /**
30
+ * THE QUESTION: "show me this maker's whole range, the way a fan deck reads."
31
+ * Colours grouped by family — creams together, then greys, then browns — and
32
+ * ordered light to dark within each. No stored scalar produces that order: a
33
+ * lightness sort interleaves the whites and the creams, and a reader loses the
34
+ * sense that they are looking at two different families.
35
+ *
36
+ * Deliberately unpaginated, so there is no cursor to walk and no all-brands
37
+ * form: family order is only correct over a COMPLETE range, because the greys
38
+ * cannot be placed against the browns while holding one page of each. A range
39
+ * past the server ceiling reports `truncatedAt` rather than being quietly cut,
40
+ * and an absent `truncatedAt` means the deck is whole.
41
+ *
42
+ * Every swatch carries the maker's PUBLISHED hex — this read never returns a
43
+ * colour derived from imagery. For "what is near this colour, across makers"
44
+ * use `byColour()` instead.
45
+ *
46
+ * @param brandSlug The brand's slug, as `listBrands()` reports it.
47
+ */
48
+ export declare function brandColourDeck(transport: HttpTransport, brandSlug: string, signal?: AbortSignal): Promise<BrandFanDeck>;
@@ -0,0 +1,45 @@
1
+ import { HttpTransport } from "./http-transport.js";
2
+ import { resolveOperation } from "./operations.js";
3
+ /**
4
+ * THE QUESTION: "whose catalogs are in here, and how big is each?" The SDK's
5
+ * one OFFSET-paginated surface: read `meta.total` for the collection size and
6
+ * `meta.hasMore` to keep walking. Instead use {@link brandProfile} for one
7
+ * brand's profile and trade signals.
8
+ */
9
+ export function listBrands(transport, options = {}, signal) {
10
+ return transport.requestJson(resolveOperation("list_brands"), {
11
+ query: { q: options.q, limit: options.limit, offset: options.offset },
12
+ signal,
13
+ });
14
+ }
15
+ /**
16
+ * THE QUESTION: "who is this brand, and what do they claim?" — profile plus the
17
+ * trade/spec signals ON RECORD. `trade` is keyed by attribute id and an ABSENT
18
+ * key means no value is held — never infer `false`. Instead use `search()` to
19
+ * find their materials; this describes the company, not the catalog.
20
+ */
21
+ export function brandProfile(transport, brandSlug, options, signal) {
22
+ return transport.requestJson(resolveOperation("get_brand_profile"), { query: { q: options?.q }, pathParams: { brandSlug }, signal });
23
+ }
24
+ /**
25
+ * THE QUESTION: "show me this maker's whole range, the way a fan deck reads."
26
+ * Colours grouped by family — creams together, then greys, then browns — and
27
+ * ordered light to dark within each. No stored scalar produces that order: a
28
+ * lightness sort interleaves the whites and the creams, and a reader loses the
29
+ * sense that they are looking at two different families.
30
+ *
31
+ * Deliberately unpaginated, so there is no cursor to walk and no all-brands
32
+ * form: family order is only correct over a COMPLETE range, because the greys
33
+ * cannot be placed against the browns while holding one page of each. A range
34
+ * past the server ceiling reports `truncatedAt` rather than being quietly cut,
35
+ * and an absent `truncatedAt` means the deck is whole.
36
+ *
37
+ * Every swatch carries the maker's PUBLISHED hex — this read never returns a
38
+ * colour derived from imagery. For "what is near this colour, across makers"
39
+ * use `byColour()` instead.
40
+ *
41
+ * @param brandSlug The brand's slug, as `listBrands()` reports it.
42
+ */
43
+ export function brandColourDeck(transport, brandSlug, signal) {
44
+ return transport.requestJson(resolveOperation("brand_colour_deck"), { pathParams: { brandSlug }, signal });
45
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Wire types for the browse lane — GET `/api/v1/search/browse`.
3
+ *
4
+ * The SDK's one genuinely cursor-paginated surface, and the only lane with no
5
+ * query, no relevance and no ranking: the corpus in a stated order. That is what
6
+ * a category listing or a brand fan deck actually needs, and it is why `count`
7
+ * matters here more than anywhere else — a listing that cannot state its own
8
+ * size has to either walk every page or report a page size as a total.
9
+ *
10
+ * Lifted out of `types.ts` for the repository's source-file size budget when the
11
+ * lane gained its non-colour sorts and its feed count (MG-1483 / MG-1542).
12
+ * Nothing about the client's surface changed: `index.ts` re-exports these
13
+ * exactly as before.
14
+ */
15
+ import type { ByColourSwatch } from "./by-colour-types.js";
16
+ import type { Cursor, ProductId, VariantId } from "./identifiers.js";
17
+ import type { CountedTotal, OpenEnum } from "./types.js";
18
+ /** One item from the pure colour-scalar browse. */
19
+ export interface BrowseResultItem {
20
+ /** Opaque — round-trip verbatim. */
21
+ variantId: VariantId;
22
+ brand: string;
23
+ product: string;
24
+ brandSlug: string | null;
25
+ productId: ProductId | null;
26
+ color: string | null;
27
+ variantName: string | null;
28
+ imageUrl: string | null;
29
+ scalars: {
30
+ l: number | null;
31
+ chroma: number | null;
32
+ vibrancy: number | null;
33
+ };
34
+ retired?: boolean;
35
+ /**
36
+ * The row's own image palette — the FULL stored palette of the SAME image
37
+ * `imageUrl` serves, uncapped and in stored order, each swatch with `oklab`
38
+ * (see {@link ByColourSwatch}), so a card can render, sort or group colours
39
+ * without a second call. A one-swatch palette means the extractor found one
40
+ * colour (solid-colour materials are genuinely monochrome), not that the list
41
+ * was trimmed. ABSENT when no palette is on record for that image: absent
42
+ * means unknown, NOT that the material has no colours. These swatches come
43
+ * from product imagery, not from the manufacturer.
44
+ */
45
+ palette?: ByColourSwatch[];
46
+ }
47
+ /** The browse orders. `name` (A→Z) is the default; `recent` is newest first. */
48
+ export type BrowseSort = OpenEnum<"name" | "recent" | "light" | "dark" | "vivid" | "muted" | "colourful">;
49
+ /**
50
+ * The browse feed's total — how many materials the browse DESCRIBES, which is
51
+ * never `results.length`.
52
+ *
53
+ * Uncapped, unlike {@link SearchCounts}: `basis:"exact"` is the true figure
54
+ * whatever its size, and `basis:"lowerBound"` means the count query timed out
55
+ * and the value is a floor. Counted over the SAME filters the page was drawn
56
+ * from, so a `types=`, `brands=` or `corpusScope=` narrowed browse reports the
57
+ * narrowed total — you will never get a whole-graph number beside a filtered
58
+ * page.
59
+ */
60
+ export interface BrowseCounts {
61
+ variants: CountedTotal;
62
+ }
63
+ /**
64
+ * A single page of the browse — the SDK's one genuinely cursor-paginated
65
+ * surface. `results.length` is this page. Ask for `count: true` and `counts`
66
+ * states the total; without it the browse states no total at all, and a corpus
67
+ * claim can only come from walking every page (`client.browse.pages(...)` does
68
+ * the loop).
69
+ */
70
+ export interface BrowseResponse {
71
+ /** THIS PAGE. */
72
+ results: BrowseResultItem[];
73
+ /** Opaque keyset for the next page. Absent = the walk is finished. */
74
+ cursor?: Cursor;
75
+ /**
76
+ * The order this page was built in — echoed, because `sort` is optional and
77
+ * defaulted, so a caller that omitted it need not know the server's default
78
+ * to label its own listing.
79
+ */
80
+ sort: BrowseSort;
81
+ /**
82
+ * The total, present ONLY when `count: true` was passed. Absent means it was
83
+ * not asked for — never that the set is empty.
84
+ */
85
+ counts?: BrowseCounts;
86
+ /**
87
+ * The corpus this page was drawn from — present ONLY when the request
88
+ * narrowed it. ABSENT means the whole graph: every material MaterialGraph
89
+ * knows, orderable or not.
90
+ */
91
+ corpusScope?: {
92
+ scope: OpenEnum<"all" | "materialBankOrderable">;
93
+ /** What the scope admits, in one sentence, for a human reader. */
94
+ description: string;
95
+ };
96
+ }
97
+ /** Params for the pure colour `search/browse` surface. */
98
+ export interface BrowseParams {
99
+ /**
100
+ * OPTIONAL. Defaults to `name` (A→Z); `recent` is newest first.
101
+ *
102
+ * The five colour ramps (`light`/`dark`/`vivid`/`muted`/`colourful`) order by
103
+ * an EXTRACTED colour scalar and therefore see only colour-extracted
104
+ * materials — a genuinely smaller set than `name` and `recent` walk. That is
105
+ * not a bug to route around; ask for `count` and each sort reports the size of
106
+ * the set it actually describes.
107
+ */
108
+ sort?: BrowseSort;
109
+ /**
110
+ * The previous page's `cursor`, verbatim. Opaque keyset state — not an offset.
111
+ * Changing `sort`, `limit`, `types`, `brands` or `corpusScope` invalidates it.
112
+ */
113
+ cursor?: Cursor;
114
+ /** Page size. */
115
+ limit?: number;
116
+ /**
117
+ * Return `counts.variants` — the total size of the set this browse walks,
118
+ * counted over the same filters the page came from.
119
+ *
120
+ * Opt-in, because it is a second query. A category listing that cannot state
121
+ * how many things it is listing has to either walk every page or report the
122
+ * page size as a total; this is the third option.
123
+ */
124
+ count?: boolean;
125
+ /**
126
+ * Comma-separated taxonomy keys to restrict the browse to, at EITHER tier of
127
+ * `/api/v1/taxonomy/categories`. A GROUP key (`tile_surfaces`) expands to
128
+ * every product type under it, a TYPE key (`ceramic_porcelain_tile`) stands
129
+ * for itself, and a mixed list is their deduplicated union. A key in neither
130
+ * tier is a 400 listing the valid keys — never an empty page.
131
+ */
132
+ types?: string;
133
+ /**
134
+ * Comma-separated brand slugs (`farrow-ball`) or display names
135
+ * (`Farrow & Ball`), case-insensitive. One brand plus a colour `sort` is that
136
+ * brand's fan deck. An unknown brand is a 400 naming it, never an empty page.
137
+ */
138
+ brands?: string;
139
+ /**
140
+ * Which corpus to walk. ABSENT means the whole graph — every material
141
+ * MaterialGraph knows, orderable or not. `materialBankOrderable` restricts to
142
+ * materials carrying a Material Bank identity, applied inside the scan, so a
143
+ * scoped page is still a full page and `cursor` still means another eligible
144
+ * row exists.
145
+ */
146
+ corpusScope?: OpenEnum<"all" | "materialBankOrderable">;
147
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -7,7 +7,7 @@
7
7
  * enums are widened so a new server value still type-checks.
8
8
  */
9
9
  import type { VariantId } from "./identifiers.js";
10
- import type { OklabTriple, PaletteSwatch } from "./types.js";
10
+ import type { OklabTriple, OpenEnum, PaletteSwatch } from "./types.js";
11
11
  /** Coarse group + fine type of a colour match's primary product type. */
12
12
  export interface MatchCategory {
13
13
  group: string | null;
@@ -16,10 +16,10 @@ export interface MatchCategory {
16
16
  /**
17
17
  * One colour-palette match.
18
18
  *
19
- * `hex`, `distance` and `closeness` are OKLab geometry over colour EXTRACTED
20
- * FROM PRODUCT IMAGERY, not declared by the manufacturer. Report it as visual
21
- * likeness ("reads close to this colour"), never as a stated colour
22
- * specification.
19
+ * `distance` and `closeness` are OKLab geometry over colour EXTRACTED FROM
20
+ * PRODUCT IMAGERY. Report the ORDER as visual likeness ("reads close to this
21
+ * colour"), never as a stated colour specification. `hex` itself may be the
22
+ * manufacturer's published colour — read `sourceBasis` before repeating it.
23
23
  */
24
24
  export interface ByColourMatch {
25
25
  /** Opaque — round-trip verbatim. */
@@ -34,6 +34,31 @@ export interface ByColourMatch {
34
34
  closeness: number;
35
35
  category: MatchCategory | null;
36
36
  oklab: OklabTriple | null;
37
+ /**
38
+ * Where `hex` came from — PER MATCH, never per response. `"declared"`: the
39
+ * manufacturer published it (a paint's own colour system), so it may be
40
+ * repeated as their stated colour. `"derived"`: extracted from product
41
+ * imagery — visual likeness only. Widened to `string` so a new server value
42
+ * still type-checks. ABSENT when `hex` is null, or on a response captured
43
+ * before the field existed.
44
+ */
45
+ sourceBasis?: OpenEnum<"declared" | "derived">;
46
+ /**
47
+ * OKLab distance from the QUERY colour to this match's `hex` — two single
48
+ * colours. NOT `distance`, which is the imagery ranking's geometry over whole
49
+ * PALETTES. The two disagree routinely: a match ranked 4th by imagery is often
50
+ * the closest by hex. ABSENT when the match has no hex, or when the query was
51
+ * a palette rather than one colour.
52
+ */
53
+ colourDistance?: number;
54
+ /**
55
+ * `colourDistance` as a named band, so nobody thresholds a float. Not a
56
+ * confidence and not a percentage — `very-close` still covers differences a
57
+ * client would reject in a paint order, so read `sourceBasis` before quoting
58
+ * any of it as a specification. Widened to `string` so a new server band still
59
+ * type-checks. ABSENT exactly when `colourDistance` is.
60
+ */
61
+ proximity?: OpenEnum<"indistinguishable" | "very-close" | "close" | "related" | "unrelated">;
37
62
  /**
38
63
  * The match's own image palette — the FULL stored palette, uncapped and in
39
64
  * stored order, each swatch with `oklab` (see {@link ByColourSwatch}), so a
@@ -81,11 +106,54 @@ export interface ColourCollapse {
81
106
  /** Rows dropped for resolving to a better-ranked row's stored photograph. */
82
107
  droppedBySharedImage: number;
83
108
  }
109
+ /**
110
+ * Receipt naming which retrieval lane answered, and the colours it actually ran
111
+ * on. The live endpoint always sends it.
112
+ *
113
+ * Two lanes answer two different questions. A ONE-COLOUR query is a CONTAINMENT
114
+ * question — which materials hold a swatch near this colour — and is answered as
115
+ * a bounded OKLab ball over stored palette swatches. A multi-colour query is a
116
+ * SIMILARITY question — which palettes as a whole resemble this palette — and is
117
+ * answered by nearest-neighbour over the palette embedding. The two rank on
118
+ * different geometries, so `colourDistance` on a match means a different thing
119
+ * in each and is NOT comparable across them.
120
+ */
121
+ export interface ColourLaneReceipt {
122
+ /** Which lane answered. Widened so a new server lane still type-checks. */
123
+ lane: OpenEnum<"containment" | "palette">;
124
+ /** Why this lane, in one sentence, for a human reader. */
125
+ reason: string;
126
+ /**
127
+ * How the page was ordered. `representative` — the containment default —
128
+ * draws the page in the contained set's own proportions of solid and
129
+ * multi-swatch materials, nearest-first within each, because ordering
130
+ * containment by proximity alone returns a page of solid-colour materials.
131
+ */
132
+ order: OpenEnum<"representative" | "nearest" | "similarity">;
133
+ /**
134
+ * The colours the search ACTUALLY ran on, after minor-proportion accents were
135
+ * dropped. Compare against `query.colours`: a difference means the search that
136
+ * ran is not the search you asked for.
137
+ */
138
+ effectiveColours: string[];
139
+ /**
140
+ * Colours removed from your query for carrying a proportion below the accent
141
+ * threshold. EMPTY is the normal case. Non-empty and those colours were absent
142
+ * from retrieval entirely — they did not merely rank low, so an empty-looking
143
+ * result is not evidence that nothing holds them.
144
+ */
145
+ droppedColours: string[];
146
+ }
84
147
  /** The `search/colours` response. */
85
148
  export interface ByColourResponse {
86
149
  query: {
87
150
  colours: string[];
88
151
  categories?: string[];
152
+ /**
153
+ * Echoed brand filter as resolved SLUGS — what the names you passed
154
+ * resolved to. Present only when a brand scope applied.
155
+ */
156
+ brands?: string[];
89
157
  /** Echoed `family` filter, normalised. Present only when one was applied. */
90
158
  family?: string;
91
159
  oklab?: OklabTriple;
@@ -105,6 +173,24 @@ export interface ByColourResponse {
105
173
  * overfetch allowed, and this says so in counts rather than adjectives.
106
174
  */
107
175
  collapse?: ColourCollapse;
176
+ /**
177
+ * Which retrieval lane answered and the colours it ran on. The live endpoint
178
+ * always sends it; optional here only so responses captured before the lane
179
+ * split still type-check.
180
+ */
181
+ lane?: ColourLaneReceipt;
182
+ /**
183
+ * The corpus this page was drawn from — present ONLY when the request
184
+ * narrowed it. ABSENT means the whole graph: every material MaterialGraph
185
+ * knows, orderable or not. Under a scope the `familyGate` and `collapse` pool
186
+ * counts describe the SCOPED corpus too, because the scope was applied inside
187
+ * retrieval rather than over the page it produced.
188
+ */
189
+ corpusScope?: {
190
+ scope: OpenEnum<"all" | "materialBankOrderable">;
191
+ /** What the scope admits, in one sentence, for a human reader. */
192
+ description: string;
193
+ };
108
194
  }
109
195
  /** Params for `search/colours`. */
110
196
  export interface ByColourParams {
@@ -118,6 +204,20 @@ export interface ByColourParams {
118
204
  b?: number;
119
205
  /** HARD category filter — display names or taxonomy ids (repeatable). */
120
206
  category?: string | string[];
207
+ /**
208
+ * HARD brand filter (repeatable) — a brand SLUG (`sherwin-williams`) or its
209
+ * display name (`Sherwin-Williams`), case-insensitive.
210
+ *
211
+ * This is how the CROSS-BRAND question is asked: pass one brand's colour as
212
+ * `hex` and scope to another to get that brand's nearest colours to it. It is
213
+ * applied inside retrieval, so `limit` still fills from the scoped brands —
214
+ * filtering a returned page instead comes back short, because a
215
+ * nearest-neighbour scan does not know to look further.
216
+ *
217
+ * An unknown brand is a 400 naming it, never an empty page. Omitting it (or
218
+ * passing an empty list) searches every brand.
219
+ */
220
+ brand?: string | string[];
121
221
  /**
122
222
  * HARD colour-family filter — ONE colorscope taxonomy family id, from the
123
223
  * closed 15-value vocabulary colorscope itself publishes (`white`, `cream`,
@@ -9,7 +9,7 @@
9
9
  * the pagination LOOP, which is the only real logic here and the only part worth
10
10
  * testing on its own.
11
11
  */
12
- import type { BrowseParams, BrowseResponse } from "./types.js";
12
+ import type { BrowseParams, BrowseResponse } from "./browse-types.js";
13
13
  /**
14
14
  * `client.browse(...)` — callable for ONE PAGE, `.pages()` for the whole walk.
15
15
  *
@@ -24,14 +24,24 @@ export interface BrowseNamespace {
24
24
  /** Fetch one page. The client supplies this, so the transport stays private. */
25
25
  export type BrowsePageFetcher = (params: BrowseParams, signal?: AbortSignal) => Promise<BrowseResponse>;
26
26
  /**
27
- * The query string for one browse page — the four params the route reads, named
28
- * here so the client method and the pagination loop cannot disagree about them.
27
+ * The query string for one browse page — EVERY param the route reads, named here
28
+ * so the client method and the pagination loop cannot disagree about them.
29
+ *
30
+ * "Every" is load-bearing. This is an allow-list wrapper, and an allow-list
31
+ * wrapper that rebuilds a query from named fields is precisely the shape that
32
+ * silently drops a param the server already honours: `brands` shipped on the
33
+ * route and never reached it from here, so the fan deck was unreachable through
34
+ * the published client while the server had supported it all along. When the
35
+ * route gains a param, it gains a line HERE too, or it is not shipped.
29
36
  */
30
37
  export declare function browseQuery(params: BrowseParams): {
31
38
  sort: BrowseParams["sort"];
32
39
  cursor: BrowseParams["cursor"];
33
40
  limit: BrowseParams["limit"];
41
+ count: BrowseParams["count"];
34
42
  types: BrowseParams["types"];
43
+ brands: BrowseParams["brands"];
44
+ corpusScope: BrowseParams["corpusScope"];
35
45
  };
36
46
  /**
37
47
  * Walk every page from `params.cursor` onward.
package/client-browse.js CHANGED
@@ -1,13 +1,23 @@
1
1
  /**
2
- * The query string for one browse page — the four params the route reads, named
3
- * here so the client method and the pagination loop cannot disagree about them.
2
+ * The query string for one browse page — EVERY param the route reads, named here
3
+ * so the client method and the pagination loop cannot disagree about them.
4
+ *
5
+ * "Every" is load-bearing. This is an allow-list wrapper, and an allow-list
6
+ * wrapper that rebuilds a query from named fields is precisely the shape that
7
+ * silently drops a param the server already honours: `brands` shipped on the
8
+ * route and never reached it from here, so the fan deck was unreachable through
9
+ * the published client while the server had supported it all along. When the
10
+ * route gains a param, it gains a line HERE too, or it is not shipped.
4
11
  */
5
12
  export function browseQuery(params) {
6
13
  return {
7
14
  sort: params.sort,
8
15
  cursor: params.cursor,
9
16
  limit: params.limit,
17
+ count: params.count,
10
18
  types: params.types,
19
+ brands: params.brands,
20
+ corpusScope: params.corpusScope,
11
21
  };
12
22
  }
13
23
  /**
@@ -0,0 +1,9 @@
1
+ import type { CandidateReadInput, CandidateReadResponse, GraphOperatorEvidence } from "./exploration-types.js";
2
+ import type { HttpTransport } from "./http-transport.js";
3
+ import type { VariantId } from "./identifiers.js";
4
+ /** Discovery and traversal share one namespace so availability precedes use. */
5
+ export interface ExplorationNamespace {
6
+ operators(variantId: VariantId, signal?: AbortSignal): Promise<GraphOperatorEvidence>;
7
+ candidates(variantId: VariantId, input: CandidateReadInput, signal?: AbortSignal): Promise<CandidateReadResponse>;
8
+ }
9
+ export declare function createExplorationNamespace(transport: HttpTransport): ExplorationNamespace;
@@ -0,0 +1,7 @@
1
+ import { resolveOperation } from "./operations.js";
2
+ export function createExplorationNamespace(transport) {
3
+ return {
4
+ operators: (variantId, signal) => transport.requestJson(resolveOperation("list_material_operators"), { pathParams: { variantId }, signal }),
5
+ candidates: (variantId, input, signal) => transport.requestJson(resolveOperation("read_material_candidates"), { body: input, pathParams: { variantId }, signal }),
6
+ };
7
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The per-file comment surface, namespaced as `mg.sourceMaterialComments` — the
3
+ * argument people leave on one uploaded file (MG-1345).
4
+ *
5
+ * A namespace rather than two more flat methods on an already-large facade,
6
+ * following `mg.codes` and `mg.colours`: reading a thread and adding to it are
7
+ * one family, they share a `(batchId, sourceId)` referent, and neither makes
8
+ * sense without the other. The namespace holds no transport of its own — the
9
+ * client hands it the shared one, so auth, rate-limit accounting and error
10
+ * shaping stay in one place.
11
+ *
12
+ * TWO RULES A CONSUMER MUST NOT GET WRONG.
13
+ *
14
+ * **The author name is REPORTED, never verified.** Every comment carries
15
+ * `author.attribution: "reported"` — a name somebody typed — plus `recordedBy`,
16
+ * the authenticated principal that actually wrote the row. Render "recorded as
17
+ * from Ruth Fielding", never "Ruth Fielding said". The API takes a machine key,
18
+ * and the person whose argument a remark carries is usually not the person at
19
+ * the keyboard; presenting the one as the other is the failure this field
20
+ * exists to prevent.
21
+ *
22
+ * **A reply is not a root.** {@link SourceMaterialCommentsNamespace.post}
23
+ * returns a result discriminated on `kind`: switch on it rather than reaching
24
+ * for `replies`, which a `reply` does not have. Threads are one level deep and
25
+ * the database enforces it, so a reply can never grow replies of its own.
26
+ *
27
+ * ROUTING OUT OF THIS NAMESPACE. `mg.sourceMaterialBatch(batchId)` already
28
+ * inlines every file's thread, so opening a batch is ONE call — do not loop this
29
+ * namespace over a batch's files to rebuild what that read returns. Reach for
30
+ * `list` when you are watching a single file.
31
+ */
32
+ import { HttpTransport } from "./http-transport.js";
33
+ import type { PostSourceMaterialCommentInput, PostSourceMaterialCommentResult, SourceMaterialCommentThread } from "./run-types.js";
34
+ export interface SourceMaterialCommentsNamespace {
35
+ /**
36
+ * One file's thread, oldest root first, each root carrying its replies.
37
+ * `comments` is always an array; `truncated` says whether you are holding the
38
+ * whole thread. A file that is not in that batch, or a batch you do not own,
39
+ * raises the same 404 as a file that never existed.
40
+ */
41
+ list(batchId: string, sourceId: string, signal?: AbortSignal): Promise<SourceMaterialCommentThread>;
42
+ /**
43
+ * Add one remark. Pass `parentCommentId` to answer a ROOT comment on the same
44
+ * file; nesting deeper is a 400. Pass `idempotencyKey` so a retry after a
45
+ * network timeout returns the comment you already posted rather than a
46
+ * permanent duplicate.
47
+ */
48
+ post(batchId: string, sourceId: string, input: PostSourceMaterialCommentInput, signal?: AbortSignal): Promise<PostSourceMaterialCommentResult>;
49
+ }
50
+ /** Bind the comment operations to the client's shared transport. */
51
+ export declare function createSourceMaterialCommentsNamespace(transport: HttpTransport): SourceMaterialCommentsNamespace;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The per-file comment surface, namespaced as `mg.sourceMaterialComments` — the
3
+ * argument people leave on one uploaded file (MG-1345).
4
+ *
5
+ * A namespace rather than two more flat methods on an already-large facade,
6
+ * following `mg.codes` and `mg.colours`: reading a thread and adding to it are
7
+ * one family, they share a `(batchId, sourceId)` referent, and neither makes
8
+ * sense without the other. The namespace holds no transport of its own — the
9
+ * client hands it the shared one, so auth, rate-limit accounting and error
10
+ * shaping stay in one place.
11
+ *
12
+ * TWO RULES A CONSUMER MUST NOT GET WRONG.
13
+ *
14
+ * **The author name is REPORTED, never verified.** Every comment carries
15
+ * `author.attribution: "reported"` — a name somebody typed — plus `recordedBy`,
16
+ * the authenticated principal that actually wrote the row. Render "recorded as
17
+ * from Ruth Fielding", never "Ruth Fielding said". The API takes a machine key,
18
+ * and the person whose argument a remark carries is usually not the person at
19
+ * the keyboard; presenting the one as the other is the failure this field
20
+ * exists to prevent.
21
+ *
22
+ * **A reply is not a root.** {@link SourceMaterialCommentsNamespace.post}
23
+ * returns a result discriminated on `kind`: switch on it rather than reaching
24
+ * for `replies`, which a `reply` does not have. Threads are one level deep and
25
+ * the database enforces it, so a reply can never grow replies of its own.
26
+ *
27
+ * ROUTING OUT OF THIS NAMESPACE. `mg.sourceMaterialBatch(batchId)` already
28
+ * inlines every file's thread, so opening a batch is ONE call — do not loop this
29
+ * namespace over a batch's files to rebuild what that read returns. Reach for
30
+ * `list` when you are watching a single file.
31
+ */
32
+ import { HttpTransport } from "./http-transport.js";
33
+ import { listSourceMaterialComments, postSourceMaterialComment, } from "./runs.js";
34
+ /** Bind the comment operations to the client's shared transport. */
35
+ export function createSourceMaterialCommentsNamespace(transport) {
36
+ return {
37
+ list: (batchId, sourceId, signal) => listSourceMaterialComments(transport, batchId, sourceId, signal),
38
+ post: (batchId, sourceId, input, signal) => postSourceMaterialComment(transport, batchId, sourceId, input, signal),
39
+ };
40
+ }