@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.
- package/brands-client.d.ts +48 -0
- package/brands-client.js +45 -0
- package/browse-types.d.ts +147 -0
- package/browse-types.js +1 -0
- package/by-colour-types.d.ts +105 -5
- package/client-browse.d.ts +13 -3
- package/client-browse.js +12 -2
- package/client-exploration.d.ts +9 -0
- package/client-exploration.js +7 -0
- package/client-source-material-comments.d.ts +51 -0
- package/client-source-material-comments.js +40 -0
- package/client.d.ts +64 -71
- package/client.js +90 -78
- package/colour-aggregate-types.d.ts +58 -0
- package/colours-client.d.ts +8 -2
- package/colours-client.js +15 -2
- package/composition-client.d.ts +33 -0
- package/composition-client.js +27 -0
- package/composition-types.d.ts +46 -0
- package/coordinate-client.d.ts +23 -0
- package/coordinate-client.js +25 -0
- package/errors.js +24 -8
- package/exploration-types.d.ts +110 -0
- package/exploration-types.js +1 -0
- package/fan-deck-types.d.ts +42 -0
- package/fan-deck-types.js +9 -0
- package/http-transport.js +24 -5
- package/index.d.ts +7 -3
- package/index.js +2 -2
- package/match-stream-types.d.ts +1 -1
- package/match-stream-types.js +16 -3
- package/match-stream.js +14 -4
- package/material-types.d.ts +1 -1
- package/operations.d.ts +1 -1
- package/operations.js +187 -26
- package/package.json +1 -1
- package/palette-search-types.d.ts +31 -0
- package/resolve-client.d.ts +22 -3
- package/resolve-client.js +31 -4
- package/resolve-types.d.ts +70 -1
- package/run-events.d.ts +1 -1
- package/run-events.js +4 -1
- package/run-types.d.ts +132 -0
- package/runs.d.ts +35 -1
- package/runs.js +69 -5
- package/schedule-types.d.ts +128 -0
- package/schema-resource-types.d.ts +1 -1
- package/search-refinement-types.d.ts +39 -4
- package/search-request-types.d.ts +172 -0
- package/search-request-types.js +1 -0
- package/search-result-validation.js +21 -4
- package/slot-pool-types.d.ts +370 -0
- package/slot-pool-types.js +1 -0
- package/source-material-validation.d.ts +5 -1
- package/source-material-validation.js +114 -5
- package/spec-facet-types.d.ts +53 -0
- package/spec-facet-types.js +7 -0
- package/specifications.d.ts +26 -0
- package/specifications.js +32 -0
- package/sse.js +3 -1
- package/suggest-types.js +3 -1
- package/types.d.ts +176 -216
- package/validation.d.ts +12 -0
- 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>;
|
package/brands-client.js
ADDED
|
@@ -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
|
+
}
|
package/browse-types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/by-colour-types.d.ts
CHANGED
|
@@ -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
|
-
* `
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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`,
|
package/client-browse.d.ts
CHANGED
|
@@ -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 —
|
|
28
|
-
*
|
|
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 —
|
|
3
|
-
*
|
|
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
|
+
}
|