@instruments/materialgraph 0.6.0 → 0.6.1
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/brand-types.d.ts +20 -1
- package/brand-types.js +7 -0
- package/by-spec-types.d.ts +62 -3
- package/catalog-discovery-types.d.ts +19 -2
- package/client-browse.d.ts +7 -1
- package/client.d.ts +339 -53
- package/client.js +313 -32
- package/codes-client.d.ts +36 -0
- package/codes-client.js +26 -0
- package/codes-types.d.ts +77 -11
- package/colours-client.d.ts +41 -6
- package/colours-client.js +27 -6
- package/composition-types.d.ts +27 -18
- package/demand-batch-types.d.ts +118 -0
- package/demand-batch-types.js +1 -0
- package/demand-types.d.ts +134 -0
- package/demand-types.js +1 -0
- package/identifiers.d.ts +71 -0
- package/identifiers.js +26 -0
- package/index.d.ts +1 -0
- package/material-asset-types.d.ts +20 -1
- package/material-block-types.d.ts +30 -1
- package/material-types.d.ts +45 -7
- package/package.json +1 -1
- package/palette-search-types.d.ts +17 -2
- package/project-types.d.ts +20 -8
- package/project-types.js +0 -7
- package/run-types.d.ts +20 -8
- package/schedule-types.d.ts +18 -7
- package/schedule-types.js +0 -11
- package/schema-resource-types.d.ts +15 -0
- package/search-refinement-types.d.ts +24 -4
- package/spec-types.d.ts +14 -5
- package/spec-types.js +14 -5
- package/suggest-types.d.ts +10 -1
- package/suggest-types.js +5 -0
- package/types.d.ts +179 -214
package/codes-types.d.ts
CHANGED
|
@@ -13,18 +13,47 @@
|
|
|
13
13
|
* ADVISORY ONLY: every verdict and the envelope carry the AHJ caveat — this is
|
|
14
14
|
* model code as adopted, never a legal determination.
|
|
15
15
|
*/
|
|
16
|
+
import type { VariantId } from "./identifiers.js";
|
|
16
17
|
import type { OpenEnum } from "./types.js";
|
|
17
18
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* A code context: the space the registry is queried for.
|
|
20
|
+
*
|
|
21
|
+
* CONDITIONAL REQUIREMENT — two axes must END UP resolved, and each may be
|
|
22
|
+
* stated OR correlated. Every field below is optional in the type because the
|
|
23
|
+
* validator resolves them per axis independently; the rule it enforces at
|
|
24
|
+
* runtime is:
|
|
25
|
+
*
|
|
26
|
+
* > occupancyGroup, resolved = `input.occupancyGroup` ?? correlate(roomType)
|
|
27
|
+
* > spaceType, resolved = `input.spaceType` ?? correlate(roomType)
|
|
28
|
+
*
|
|
29
|
+
* and either resolving to nothing is a 400, verbatim:
|
|
30
|
+
*
|
|
31
|
+
* - `"An occupancy group is required — state occupancyGroup, or a roomType that
|
|
32
|
+
* correlates to one."`
|
|
33
|
+
* - `"A space type is required — state spaceType, or a roomType that correlates
|
|
34
|
+
* to one."`
|
|
35
|
+
* - `"Room type \"{roomType}\" does not correlate to an IBC occupancy group —
|
|
36
|
+
* state occupancyGroup explicitly."`
|
|
37
|
+
* - `"Room type \"{roomType}\" does not correlate to a Table 803.13 finish
|
|
38
|
+
* location — state spaceType explicitly."`
|
|
39
|
+
*
|
|
40
|
+
* So: give `occupancyGroup` + `spaceType`, or give `roomType` and let both
|
|
41
|
+
* correlate, or mix them — an explicit axis always wins over a correlated one,
|
|
42
|
+
* and a room is only consulted for an axis you did not state. Correlation never
|
|
43
|
+
* guesses: a room that maps to neither is an error naming the axis, and the
|
|
44
|
+
* response's `resolvedContext` is where you audit what was inferred for you.
|
|
45
|
+
*
|
|
46
|
+
* Discover every accepted value — jurisdictions, occupancy groups, finish
|
|
47
|
+
* locations, and the room correlations themselves — from
|
|
48
|
+
* {@link CodesClient.context}. Nothing here should be hardcoded.
|
|
21
49
|
*/
|
|
22
50
|
export interface CodeContextRequest {
|
|
23
|
-
/** A seeded jurisdiction id (e.g. `"us-tx"`); an unknown id is a 400. */
|
|
51
|
+
/** A seeded jurisdiction id (e.g. `"us-tx"`); an unknown id is a 400 listing the seeded set. */
|
|
24
52
|
jurisdictionId: string;
|
|
25
53
|
/**
|
|
26
54
|
* The IBC occupancy group for the space (e.g. `"I-2"` hospital, `"A-2"`
|
|
27
|
-
* restaurant). Required unless `roomType` correlates to one.
|
|
55
|
+
* restaurant). Required unless `roomType` correlates to one. An uncovered
|
|
56
|
+
* group is a 400 listing the covered ones.
|
|
28
57
|
*/
|
|
29
58
|
occupancyGroup?: string;
|
|
30
59
|
/**
|
|
@@ -42,8 +71,12 @@ export interface CodeContextRequest {
|
|
|
42
71
|
sprinklered: boolean;
|
|
43
72
|
}
|
|
44
73
|
export interface CodeAdvisoryInput extends CodeContextRequest {
|
|
45
|
-
/**
|
|
46
|
-
|
|
74
|
+
/**
|
|
75
|
+
* 1–100 opaque variant ids to badge, echoed back in `results` in this exact
|
|
76
|
+
* order and cardinality (duplicates are echoed, never collapsed). Preserve
|
|
77
|
+
* them verbatim — never parse or construct one.
|
|
78
|
+
*/
|
|
79
|
+
variantIds: VariantId[];
|
|
47
80
|
}
|
|
48
81
|
/** The resolved jurisdiction the verdicts were evaluated against. */
|
|
49
82
|
export interface CodeAdvisoryJurisdiction {
|
|
@@ -101,7 +134,15 @@ export interface CodeRequirementThreshold {
|
|
|
101
134
|
unit: string;
|
|
102
135
|
} | null;
|
|
103
136
|
}
|
|
104
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* The accepted fact a verdict was decided from — null when the material carried
|
|
139
|
+
* none, which is exactly the case that produces `verdict: "unknown"`.
|
|
140
|
+
*
|
|
141
|
+
* `confidence` and `sourceBasis` are the strength of the fact, not of the code
|
|
142
|
+
* reading. Quote them whenever you quote the badge: a `meets` resting on a
|
|
143
|
+
* `derived` value at low confidence is a materially different statement from one
|
|
144
|
+
* resting on a `tested` figure.
|
|
145
|
+
*/
|
|
105
146
|
export interface CodeRequirementEvidence {
|
|
106
147
|
attributeId: OpenEnum<"fire_classifications" | "slip_resistance_dcof">;
|
|
107
148
|
value: string;
|
|
@@ -114,7 +155,14 @@ export interface CodeRequirementVerdict {
|
|
|
114
155
|
dimension: OpenEnum<"fire" | "floor" | "slip" | "decorative">;
|
|
115
156
|
section: string;
|
|
116
157
|
requirement: CodeRequirementThreshold;
|
|
117
|
-
/**
|
|
158
|
+
/**
|
|
159
|
+
* What may be said about this material against this requirement:
|
|
160
|
+
* - `meets` — a defensible fact clears the threshold. Advisory, still not a
|
|
161
|
+
* determination.
|
|
162
|
+
* - `shortfall` — a defensible fact FAILS to clear it. A real negative.
|
|
163
|
+
* - `unknown` — no defensible fact maps onto `scale`. NOT a failure and NOT a
|
|
164
|
+
* pass: "no comparable rating on file". Never present it as either.
|
|
165
|
+
*/
|
|
118
166
|
verdict: OpenEnum<"meets" | "shortfall" | "unknown">;
|
|
119
167
|
evidence: CodeRequirementEvidence | null;
|
|
120
168
|
/** The AHJ advisory caveat — model code as adopted, never a legal determination. */
|
|
@@ -126,6 +174,16 @@ export interface CodeRequirementVerdict {
|
|
|
126
174
|
* `not_applicable` (nothing applied) is deliberately distinct from `meets`.
|
|
127
175
|
*/
|
|
128
176
|
export interface CodeAdvisorySummary {
|
|
177
|
+
/**
|
|
178
|
+
* The rolled-up card, and what each token licenses:
|
|
179
|
+
* - `meets` — every applicable requirement was `meets`. Advisory pass.
|
|
180
|
+
* - `shortfall` — at least one requirement failed on real evidence. Blocking.
|
|
181
|
+
* - `unverified` — nothing failed, but something is `unknown`. COMMON, and
|
|
182
|
+
* never a pass: it means "no comparable rating is on file". Show it as an
|
|
183
|
+
* open question, not a green tick.
|
|
184
|
+
* - `not_applicable` — the space imposes nothing this material answers for.
|
|
185
|
+
* Deliberately distinct from `meets`: it is silence, not approval.
|
|
186
|
+
*/
|
|
129
187
|
status: OpenEnum<"meets" | "shortfall" | "unverified" | "not_applicable">;
|
|
130
188
|
/** One deterministic sentence naming what failed, e.g. `"Falls short on fire for A-2 corridors."`. */
|
|
131
189
|
headline: string;
|
|
@@ -134,7 +192,8 @@ export interface CodeAdvisorySummary {
|
|
|
134
192
|
}
|
|
135
193
|
/** Per-variant advisory outcome, in input order. */
|
|
136
194
|
export interface CodeAdvisoryVariantResult {
|
|
137
|
-
|
|
195
|
+
/** Opaque — echoed verbatim from `variantIds`, in that position. */
|
|
196
|
+
variantId: VariantId;
|
|
138
197
|
/** `evaluated` when the id resolved; `unknown_variant` (with `requirements: []`) when it did not. */
|
|
139
198
|
state: OpenEnum<"evaluated" | "unknown_variant">;
|
|
140
199
|
/** What this material is and what it was scored against. Null only for `state: "unknown_variant"`. */
|
|
@@ -168,7 +227,14 @@ export interface ResolvedCodeContext {
|
|
|
168
227
|
/** The `ms_room` term the axes were correlated from, or null. */
|
|
169
228
|
roomType: string | null;
|
|
170
229
|
}
|
|
171
|
-
/**
|
|
230
|
+
/**
|
|
231
|
+
* How far a jurisdiction's rows have been reviewed against that AHJ's own tables.
|
|
232
|
+
*
|
|
233
|
+
* - `thorough` — reviewed against this jurisdiction's own adopted tables.
|
|
234
|
+
* - `model_baseline` — UNAMENDED model IBC stood in, because that AHJ's own
|
|
235
|
+
* amendments have not been reviewed. Local amendments may be stricter, so
|
|
236
|
+
* say "per model IBC" rather than naming the jurisdiction as the authority.
|
|
237
|
+
*/
|
|
172
238
|
export type CodeCoverageTier = OpenEnum<"thorough" | "model_baseline">;
|
|
173
239
|
export interface CodeJurisdictionSummary {
|
|
174
240
|
id: string;
|
package/colours-client.d.ts
CHANGED
|
@@ -17,6 +17,20 @@
|
|
|
17
17
|
* materialized view refreshed on a schedule, not on write, so a caller can and
|
|
18
18
|
* should check how much of the corpus the answer actually covers before
|
|
19
19
|
* treating it as complete. The two SEARCH responses are live and uncached.
|
|
20
|
+
*
|
|
21
|
+
* ROUTING OUT OF THIS NAMESPACE. Colour is a lens, and these are not the only
|
|
22
|
+
* doors into it:
|
|
23
|
+
* - ONE colour → `mg.byColour({ hex })`. This namespace's `palette` is for N
|
|
24
|
+
* colours at stated proportions, which is a different ranking, not a
|
|
25
|
+
* convenience wrapper.
|
|
26
|
+
* - A colour NAME rather than a hex → `mg.resolveColour(name)` first.
|
|
27
|
+
* - Colour WORDS (families, moods, undertones) → `mg.colourFacets()`.
|
|
28
|
+
* - Walking the corpus by lightness/vividness → `mg.browse({ sort })`.
|
|
29
|
+
* - Colour as one signal among words, a seed and a mood → `mg.search(...)`.
|
|
30
|
+
*
|
|
31
|
+
* And the honesty rule the whole lens rests on: every colour here is DERIVED
|
|
32
|
+
* FROM PRODUCT IMAGERY, not declared by the manufacturer. Report visual
|
|
33
|
+
* likeness, never a stated colour specification.
|
|
20
34
|
*/
|
|
21
35
|
import type { CategoryColourProfilesResponse, ColourProfilesParams, ColourStatsResponse } from "./colour-aggregate-types.js";
|
|
22
36
|
import type { QueryParams } from "./http-transport.js";
|
|
@@ -31,8 +45,14 @@ export declare class ColoursClient {
|
|
|
31
45
|
#private;
|
|
32
46
|
constructor(transport: ColoursTransport);
|
|
33
47
|
/**
|
|
34
|
-
*
|
|
35
|
-
* (GET `/api/v1/colours/profiles`).
|
|
48
|
+
* THE QUESTION: "what colours does this CATEGORY come in?" — the representative
|
|
49
|
+
* palette of 1–8 categories in ONE call (GET `/api/v1/colours/profiles`).
|
|
50
|
+
*
|
|
51
|
+
* An aggregate over a category, not a list of materials: use `mg.byColour()` or
|
|
52
|
+
* `mg.search()` when you want the materials themselves, and
|
|
53
|
+
* {@link ColoursClient.stats} for coverage across the whole corpus rather than
|
|
54
|
+
* one category's palette. Check `basis` — this reads a scheduled materialized
|
|
55
|
+
* view, so it can lag the catalog.
|
|
36
56
|
*
|
|
37
57
|
* `groupBy: "group"` takes product-type GROUP keys and merges every leaf type
|
|
38
58
|
* under a group into one histogram BEFORE ranking — a group's palette, not the
|
|
@@ -54,6 +74,14 @@ export declare class ColoursClient {
|
|
|
54
74
|
*/
|
|
55
75
|
profiles(params: ColourProfilesParams, signal?: AbortSignal): Promise<CategoryColourProfilesResponse>;
|
|
56
76
|
/**
|
|
77
|
+
* THE QUESTION: "how much colour does the graph hold AT ALL?" — corpus-level
|
|
78
|
+
* coverage, before you trust any colour answer.
|
|
79
|
+
*
|
|
80
|
+
* The sanity check under every other colour call. Use
|
|
81
|
+
* {@link ColoursClient.profiles} for one category's palette and
|
|
82
|
+
* `mg.colourFacets()` for the vocabulary. `basis.omissions` lists what this
|
|
83
|
+
* call deliberately does not answer — read it rather than assuming silence.
|
|
84
|
+
*
|
|
57
85
|
* Corpus colour coverage (GET `/api/v1/colours/stats`): how much of the graph
|
|
58
86
|
* carries colour at all, the split across the canonical colour families, and
|
|
59
87
|
* coverage per product-type group. `basis` states the source and freshness of
|
|
@@ -62,8 +90,12 @@ export declare class ColoursClient {
|
|
|
62
90
|
*/
|
|
63
91
|
stats(signal?: AbortSignal): Promise<ColourStatsResponse>;
|
|
64
92
|
/**
|
|
65
|
-
*
|
|
66
|
-
* colour repeated (POST `/api/v1/search/palettes`).
|
|
93
|
+
* THE QUESTION: "what goes with this WHOLE PALETTE?" — N colours at once, at
|
|
94
|
+
* stated proportions, not one colour repeated (POST `/api/v1/search/palettes`).
|
|
95
|
+
*
|
|
96
|
+
* Use `mg.byColour()` for a single colour, {@link ColoursClient.gradient} when
|
|
97
|
+
* the colours form an ordered TRANSITION rather than a set, and
|
|
98
|
+
* `mg.coordinate()` when the scheme is made of materials rather than colours.
|
|
67
99
|
*
|
|
68
100
|
* Each colour scores `sim × prop`: `sim` is OKLab closeness on the same
|
|
69
101
|
* mapping `byColour` ranks with, and `prop` rewards a candidate whose own
|
|
@@ -90,8 +122,11 @@ export declare class ColoursClient {
|
|
|
90
122
|
*/
|
|
91
123
|
palette(input: SearchPaletteInput, signal?: AbortSignal): Promise<SearchPaletteResponse>;
|
|
92
124
|
/**
|
|
93
|
-
*
|
|
94
|
-
* order (POST `/api/v1/search/gradients`).
|
|
125
|
+
* THE QUESTION: "what moves from this colour to that one?" — materials that
|
|
126
|
+
* travel ALONG a transition, in order (POST `/api/v1/search/gradients`).
|
|
127
|
+
*
|
|
128
|
+
* Order is the whole point: use {@link ColoursClient.palette} when the colours
|
|
129
|
+
* are a set rather than a path, and `mg.byColour()` for a single colour.
|
|
95
130
|
*
|
|
96
131
|
* Not a palette: the score is multiplied by `coverage`, the share of the path
|
|
97
132
|
* the candidate actually spans, so a material owning only one end ranks below
|
package/colours-client.js
CHANGED
|
@@ -4,8 +4,14 @@ export class ColoursClient {
|
|
|
4
4
|
this.#transport = transport;
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
* (GET `/api/v1/colours/profiles`).
|
|
7
|
+
* THE QUESTION: "what colours does this CATEGORY come in?" — the representative
|
|
8
|
+
* palette of 1–8 categories in ONE call (GET `/api/v1/colours/profiles`).
|
|
9
|
+
*
|
|
10
|
+
* An aggregate over a category, not a list of materials: use `mg.byColour()` or
|
|
11
|
+
* `mg.search()` when you want the materials themselves, and
|
|
12
|
+
* {@link ColoursClient.stats} for coverage across the whole corpus rather than
|
|
13
|
+
* one category's palette. Check `basis` — this reads a scheduled materialized
|
|
14
|
+
* view, so it can lag the catalog.
|
|
9
15
|
*
|
|
10
16
|
* `groupBy: "group"` takes product-type GROUP keys and merges every leaf type
|
|
11
17
|
* under a group into one histogram BEFORE ranking — a group's palette, not the
|
|
@@ -34,6 +40,14 @@ export class ColoursClient {
|
|
|
34
40
|
}, signal);
|
|
35
41
|
}
|
|
36
42
|
/**
|
|
43
|
+
* THE QUESTION: "how much colour does the graph hold AT ALL?" — corpus-level
|
|
44
|
+
* coverage, before you trust any colour answer.
|
|
45
|
+
*
|
|
46
|
+
* The sanity check under every other colour call. Use
|
|
47
|
+
* {@link ColoursClient.profiles} for one category's palette and
|
|
48
|
+
* `mg.colourFacets()` for the vocabulary. `basis.omissions` lists what this
|
|
49
|
+
* call deliberately does not answer — read it rather than assuming silence.
|
|
50
|
+
*
|
|
37
51
|
* Corpus colour coverage (GET `/api/v1/colours/stats`): how much of the graph
|
|
38
52
|
* carries colour at all, the split across the canonical colour families, and
|
|
39
53
|
* coverage per product-type group. `basis` states the source and freshness of
|
|
@@ -44,8 +58,12 @@ export class ColoursClient {
|
|
|
44
58
|
return this.#transport.get("get_colour_stats", undefined, signal);
|
|
45
59
|
}
|
|
46
60
|
/**
|
|
47
|
-
*
|
|
48
|
-
* colour repeated (POST `/api/v1/search/palettes`).
|
|
61
|
+
* THE QUESTION: "what goes with this WHOLE PALETTE?" — N colours at once, at
|
|
62
|
+
* stated proportions, not one colour repeated (POST `/api/v1/search/palettes`).
|
|
63
|
+
*
|
|
64
|
+
* Use `mg.byColour()` for a single colour, {@link ColoursClient.gradient} when
|
|
65
|
+
* the colours form an ordered TRANSITION rather than a set, and
|
|
66
|
+
* `mg.coordinate()` when the scheme is made of materials rather than colours.
|
|
49
67
|
*
|
|
50
68
|
* Each colour scores `sim × prop`: `sim` is OKLab closeness on the same
|
|
51
69
|
* mapping `byColour` ranks with, and `prop` rewards a candidate whose own
|
|
@@ -74,8 +92,11 @@ export class ColoursClient {
|
|
|
74
92
|
return this.#transport.post("search_by_palette", input, signal);
|
|
75
93
|
}
|
|
76
94
|
/**
|
|
77
|
-
*
|
|
78
|
-
* order (POST `/api/v1/search/gradients`).
|
|
95
|
+
* THE QUESTION: "what moves from this colour to that one?" — materials that
|
|
96
|
+
* travel ALONG a transition, in order (POST `/api/v1/search/gradients`).
|
|
97
|
+
*
|
|
98
|
+
* Order is the whole point: use {@link ColoursClient.palette} when the colours
|
|
99
|
+
* are a set rather than a path, and `mg.byColour()` for a single colour.
|
|
79
100
|
*
|
|
80
101
|
* Not a palette: the score is multiplied by `coverage`, the share of the path
|
|
81
102
|
* the candidate actually spans, so a material owning only one end ranks below
|
package/composition-types.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* TOLERANT-PARSING CONTRACT: response enums are widened with {@link OpenEnum}
|
|
13
13
|
* so a new server value still type-checks; objects are read structurally.
|
|
14
14
|
*/
|
|
15
|
+
import type { RunId, SchemeId, SlotId, VariantId } from "./identifiers.js";
|
|
15
16
|
import type { OpenEnum } from "./types.js";
|
|
16
17
|
/**
|
|
17
18
|
* The four v1 variation axes. Request-side, so this is a STRICT union (the
|
|
@@ -25,21 +26,22 @@ export type VaryAxis = "palette" | "sustainability" | "brandDiversity" | "priceT
|
|
|
25
26
|
export declare const VARY_AXES: readonly VaryAxis[];
|
|
26
27
|
/** Pin one slot to a specific variant so vary re-derives only the rest. */
|
|
27
28
|
export interface SchemeLock {
|
|
28
|
-
slotId:
|
|
29
|
-
/** Must equal that slot's CURRENT pick in the source scheme. */
|
|
30
|
-
variantId:
|
|
29
|
+
slotId: SlotId;
|
|
30
|
+
/** Must equal that slot's CURRENT pick in the source scheme. Opaque. */
|
|
31
|
+
variantId: VariantId;
|
|
31
32
|
}
|
|
32
33
|
/** Candidate exclusions applied on top of the automatic source-pick exclusion. */
|
|
33
34
|
export interface SchemeExclusions {
|
|
34
|
-
|
|
35
|
+
/** Opaque variant ids to keep out of every slot. */
|
|
36
|
+
variantIds?: VariantId[];
|
|
35
37
|
brandSlugs?: string[];
|
|
36
38
|
}
|
|
37
39
|
/** Body for `POST /api/v1/schemes/vary` ({@link MaterialGraphClient.varyScheme}). */
|
|
38
40
|
export interface VarySchemesRequest {
|
|
39
|
-
/** The succeeded `compose-material-schemes` Run to vary from. */
|
|
40
|
-
runId:
|
|
41
|
+
/** The succeeded `compose-material-schemes` Run to vary from. Opaque. */
|
|
42
|
+
runId: RunId;
|
|
41
43
|
/** Which scheme in the Run is option A. Defaults to the Run's FIRST scheme. */
|
|
42
|
-
schemeId?:
|
|
44
|
+
schemeId?: SchemeId;
|
|
43
45
|
axis: VaryAxis;
|
|
44
46
|
/** Slots kept pinned to their source pick; only unlocked slots re-derive. */
|
|
45
47
|
locks?: SchemeLock[];
|
|
@@ -59,8 +61,8 @@ export interface ComposeAnchorInput {
|
|
|
59
61
|
element: SlotElement;
|
|
60
62
|
/** Optional named space to scope the anchor to (matched by name/slug). */
|
|
61
63
|
spaceName?: string;
|
|
62
|
-
/** An exact catalog variant id. Mutually exclusive with `query`. */
|
|
63
|
-
variantId?:
|
|
64
|
+
/** An exact catalog variant id. Opaque. Mutually exclusive with `query`. */
|
|
65
|
+
variantId?: VariantId;
|
|
64
66
|
/** Free-text "Brand Product Colour". Mutually exclusive with `variantId`. */
|
|
65
67
|
query?: string;
|
|
66
68
|
/** Display name for slot naming and warnings. */
|
|
@@ -92,6 +94,7 @@ export interface CompliancePreset {
|
|
|
92
94
|
* every candidate but never exclude one.
|
|
93
95
|
*/
|
|
94
96
|
export interface ComposeMaterialSchemesInput {
|
|
97
|
+
/** The uploaded source-material batch to compose from. Opaque. */
|
|
95
98
|
batchId: string;
|
|
96
99
|
count?: number;
|
|
97
100
|
seed?: number;
|
|
@@ -137,12 +140,13 @@ export interface CompositionMatch {
|
|
|
137
140
|
}
|
|
138
141
|
/** One slot's chosen material plus the reason it was picked. */
|
|
139
142
|
export interface CompositionPick {
|
|
140
|
-
slotId:
|
|
143
|
+
slotId: SlotId;
|
|
141
144
|
slotName: string;
|
|
142
145
|
spaceId?: string | null;
|
|
143
146
|
element?: string | null;
|
|
144
147
|
material: {
|
|
145
|
-
|
|
148
|
+
/** Opaque — round-trip verbatim into the material lenses. */
|
|
149
|
+
variantId: VariantId;
|
|
146
150
|
brand: string;
|
|
147
151
|
product: string;
|
|
148
152
|
variant: string;
|
|
@@ -171,12 +175,16 @@ export interface CompositionObjective {
|
|
|
171
175
|
}
|
|
172
176
|
/** A hydrated scheme: one pick per slot, plus its objective breakdown. */
|
|
173
177
|
export interface CompositionScheme {
|
|
174
|
-
schemeId:
|
|
178
|
+
schemeId: SchemeId;
|
|
175
179
|
title: string;
|
|
176
180
|
picks: CompositionPick[];
|
|
177
181
|
objective: CompositionObjective;
|
|
178
182
|
}
|
|
179
|
-
/**
|
|
183
|
+
/**
|
|
184
|
+
* How far short of the request the derivation fell, with honest reasons.
|
|
185
|
+
* `produced < requested` is a real answer, not an error — `reasons` says why,
|
|
186
|
+
* and a null shortfall means nothing fell short.
|
|
187
|
+
*/
|
|
180
188
|
export interface CompositionShortfall {
|
|
181
189
|
requested: number;
|
|
182
190
|
produced: number;
|
|
@@ -185,9 +193,9 @@ export interface CompositionShortfall {
|
|
|
185
193
|
/** The axis + source provenance stamped onto every varied scheme. */
|
|
186
194
|
export interface VaryProvenance {
|
|
187
195
|
axis: VaryAxis;
|
|
188
|
-
sourceRunId:
|
|
189
|
-
sourceSchemeId:
|
|
190
|
-
lockedSlotIds:
|
|
196
|
+
sourceRunId: RunId;
|
|
197
|
+
sourceSchemeId: SchemeId;
|
|
198
|
+
lockedSlotIds: SlotId[];
|
|
191
199
|
/**
|
|
192
200
|
* Human-readable notes recording exactly what the axis did — counts, not
|
|
193
201
|
* adjectives (e.g. "brandDiversity: hard-excluded 2 brand(s) appearing ≥2×").
|
|
@@ -330,11 +338,12 @@ export interface CompositionAnsweredPayload {
|
|
|
330
338
|
* slot: how many were considered and the top picks (≤5) hydrated for identity.
|
|
331
339
|
*/
|
|
332
340
|
export interface SlotPoolEventPayload {
|
|
333
|
-
slotId:
|
|
341
|
+
slotId: SlotId;
|
|
334
342
|
slotName: string;
|
|
343
|
+
/** How many candidates were WEIGHED, not how many qualified. */
|
|
335
344
|
considered: number;
|
|
336
345
|
top: Array<{
|
|
337
|
-
variantId:
|
|
346
|
+
variantId: VariantId;
|
|
338
347
|
brand: string;
|
|
339
348
|
product: string;
|
|
340
349
|
imageUrl: string | null;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire types for BATCH candidate demand enrichment — `POST /api/v1/demand/batch`.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `types.ts` for the repository's source-file size budget, the same
|
|
5
|
+
* reason `material-types.ts` and `catalog-discovery-types.ts` sit beside it;
|
|
6
|
+
* re-exported from `types.ts`, so importers see no difference.
|
|
7
|
+
*
|
|
8
|
+
* THE LENS: this answers "how much is each of these already-chosen materials
|
|
9
|
+
* actually specified?" — enrichment over a bounded id list you already hold. It
|
|
10
|
+
* never resolves, ranks, reorders or discovers. Use
|
|
11
|
+
* {@link MaterialGraphClient.demand} for one material named in free text,
|
|
12
|
+
* {@link MaterialGraphClient.pairings} for what goes WITH a material, and
|
|
13
|
+
* {@link MaterialGraphClient.search} to find candidates in the first place.
|
|
14
|
+
*/
|
|
15
|
+
import type { VariantId } from "./identifiers.js";
|
|
16
|
+
import type { DemandMomentum, DemandQuarter, DemandSignal, OpenEnum } from "./types.js";
|
|
17
|
+
/**
|
|
18
|
+
* Body for `POST /api/v1/demand/batch` — enrich 1–48 already-selected candidate
|
|
19
|
+
* ids with their real specification demand, in input order and cardinality.
|
|
20
|
+
*/
|
|
21
|
+
export interface DemandBatchInput {
|
|
22
|
+
/** 1–48 canonical variant ids. Order AND cardinality are preserved — duplicates are echoed, never collapsed. */
|
|
23
|
+
variantIds: VariantId[];
|
|
24
|
+
/** The behavioural signal to read demand from. Defaults to `sampled` (order co-occurrence) server-side. */
|
|
25
|
+
signal?: DemandSignal;
|
|
26
|
+
/** Optional declared context (e.g. `{ projectType: "hospitality" }`) — echoed for provenance, never used to rank or filter. */
|
|
27
|
+
context?: Record<string, string>;
|
|
28
|
+
}
|
|
29
|
+
/** Headline population behind the metric — this variant's own counts. */
|
|
30
|
+
export interface DemandBatchPopulation {
|
|
31
|
+
specCount: number;
|
|
32
|
+
projectCount: number;
|
|
33
|
+
customerCount: number;
|
|
34
|
+
}
|
|
35
|
+
/** The consideration→specification funnel (project-grain; `exclusionRate` is annotate-only). */
|
|
36
|
+
export interface DemandBatchFunnel {
|
|
37
|
+
consideredProjects: number;
|
|
38
|
+
specifiedProjects: number;
|
|
39
|
+
excludedCount: number;
|
|
40
|
+
exclusionRate: number;
|
|
41
|
+
}
|
|
42
|
+
/** Per-result guardrail status — present on `internal` (`mg:admin`) results only. */
|
|
43
|
+
export interface DemandBatchEvidence {
|
|
44
|
+
belowPrivacyFloor: boolean;
|
|
45
|
+
belowQualityFloor: boolean;
|
|
46
|
+
suppressedForPublic: boolean;
|
|
47
|
+
}
|
|
48
|
+
/** A variant that carries a usable demand signal. */
|
|
49
|
+
export interface DemandBatchAvailableResult {
|
|
50
|
+
variantId: VariantId;
|
|
51
|
+
state: "available";
|
|
52
|
+
population: DemandBatchPopulation;
|
|
53
|
+
/** Exact per-quarter counts (sparse — zero-spec quarters are absent). */
|
|
54
|
+
intervals: DemandQuarter[];
|
|
55
|
+
momentum: DemandMomentum;
|
|
56
|
+
/** The funnel; null when the signal is not `sampled` or no `considered` row/session exists. */
|
|
57
|
+
funnel: DemandBatchFunnel | null;
|
|
58
|
+
/** Exact guardrail evidence for `internal` callers; null for public. */
|
|
59
|
+
evidence: DemandBatchEvidence | null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A variant with a stats row that is below the public quality floor. We HAVE a
|
|
63
|
+
* reading and are declining to publish it — not a claim that nobody specifies it.
|
|
64
|
+
*/
|
|
65
|
+
export interface DemandBatchInsufficientResult {
|
|
66
|
+
variantId: VariantId;
|
|
67
|
+
state: "insufficient_data";
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A variant with no stats — indistinguishable from a privacy-suppressed one for
|
|
71
|
+
* public callers, and therefore never reportable as "nobody specifies this".
|
|
72
|
+
*/
|
|
73
|
+
export interface DemandBatchNoSignalResult {
|
|
74
|
+
variantId: VariantId;
|
|
75
|
+
state: "no_signal";
|
|
76
|
+
}
|
|
77
|
+
/** A requested id that resolves to no real variant. */
|
|
78
|
+
export interface DemandBatchUnknownResult {
|
|
79
|
+
variantId: VariantId;
|
|
80
|
+
state: "unknown_variant";
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* One discriminated result per input id, in input order and cardinality.
|
|
84
|
+
*
|
|
85
|
+
* ABSENT IS NOT ZERO: only `available` carries numbers. The other three states
|
|
86
|
+
* each mean "no number to report", for three different reasons, and none of them
|
|
87
|
+
* licenses "this material is not specified" — see each member.
|
|
88
|
+
*/
|
|
89
|
+
export type DemandBatchResult = DemandBatchAvailableResult | DemandBatchInsufficientResult | DemandBatchNoSignalResult | DemandBatchUnknownResult;
|
|
90
|
+
/** The `POST /api/v1/demand/batch` envelope. */
|
|
91
|
+
export interface DemandBatchResponse {
|
|
92
|
+
/** The completed ingestion-session id these counts were mined in (the dataset generation). */
|
|
93
|
+
generationId: string;
|
|
94
|
+
metric: "specification_demand";
|
|
95
|
+
/** The metric universe — the window and signal the counts are scoped to. */
|
|
96
|
+
universe: {
|
|
97
|
+
window: "trailing_36mo";
|
|
98
|
+
signal: DemandSignal;
|
|
99
|
+
description: string;
|
|
100
|
+
};
|
|
101
|
+
/** The guardrail status under which `results` were projected. */
|
|
102
|
+
guardrail: {
|
|
103
|
+
audience: OpenEnum<"public" | "internal">;
|
|
104
|
+
minProjectCount: number;
|
|
105
|
+
minCustomerCount: number;
|
|
106
|
+
};
|
|
107
|
+
/** Outcome tallies over `results` — never a denominator for the metric. */
|
|
108
|
+
coverage: {
|
|
109
|
+
requested: number;
|
|
110
|
+
available: number;
|
|
111
|
+
insufficientData: number;
|
|
112
|
+
noSignal: number;
|
|
113
|
+
unknownVariant: number;
|
|
114
|
+
};
|
|
115
|
+
/** The echoed declared context, or null when none was supplied. */
|
|
116
|
+
context: Record<string, string> | null;
|
|
117
|
+
results: DemandBatchResult[];
|
|
118
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire types for the SINGLE-SEED demand reads — `GET /api/v1/demand` and
|
|
3
|
+
* `POST /api/v1/pairings/coordinate`.
|
|
4
|
+
*
|
|
5
|
+
* Split out of `types.ts` for the repository's source-file size budget, the same
|
|
6
|
+
* reason `material-types.ts` and `demand-batch-types.ts` sit beside it; both are
|
|
7
|
+
* re-exported from `types.ts`, so importers see no difference.
|
|
8
|
+
*
|
|
9
|
+
* THE LENS: demand answers "how much is this specified, and what travels with
|
|
10
|
+
* it?" from real order behaviour. It is not a search and not a recommendation
|
|
11
|
+
* engine — every number is bounded by the `trailing_36mo` window and by the
|
|
12
|
+
* {@link DemandSignal} it was read under, and a missing number means UNREAD, not
|
|
13
|
+
* zero.
|
|
14
|
+
*/
|
|
15
|
+
import type { ProductId, VariantId } from "./identifiers.js";
|
|
16
|
+
import type { CompanionDemand, DemandMomentum, DemandQuarter, OpenEnum } from "./types.js";
|
|
17
|
+
/** A resolved seed material (identity block shared by the demand reads). */
|
|
18
|
+
export interface DemandSeed {
|
|
19
|
+
/** Opaque — round-trip verbatim. */
|
|
20
|
+
variantId: VariantId;
|
|
21
|
+
brand: string;
|
|
22
|
+
product: string;
|
|
23
|
+
color: string | null;
|
|
24
|
+
variantName: string | null;
|
|
25
|
+
imageUrl: string | null;
|
|
26
|
+
brandSlug: string;
|
|
27
|
+
productId: ProductId;
|
|
28
|
+
}
|
|
29
|
+
/** Headline demand stats for a seed: counts, quarterly series, momentum. */
|
|
30
|
+
export interface MaterialDemandStats {
|
|
31
|
+
specCount: number;
|
|
32
|
+
projectCount: number;
|
|
33
|
+
customerCount: number;
|
|
34
|
+
/** Sparse — a quarter with no activity is ABSENT, not a zero row. */
|
|
35
|
+
byQuarter: DemandQuarter[];
|
|
36
|
+
momentum: DemandMomentum;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* `getMaterialDemand` result, passed through verbatim by the `/demand` route.
|
|
40
|
+
*
|
|
41
|
+
* `demand: null` means NO READING — the seed did not resolve, or nothing cleared
|
|
42
|
+
* the publication floor. It is never a reading of zero, and never licenses
|
|
43
|
+
* "nobody specifies this".
|
|
44
|
+
*/
|
|
45
|
+
export interface MaterialDemandResult {
|
|
46
|
+
found: boolean;
|
|
47
|
+
query: string;
|
|
48
|
+
seed: DemandSeed | null;
|
|
49
|
+
/** The only window v1 serves. Activity outside it is unread, not absent. */
|
|
50
|
+
window: "trailing_36mo";
|
|
51
|
+
demand: MaterialDemandStats | null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* `getCoSpecifiedMaterials` result, passed through verbatim. The `companions`
|
|
55
|
+
* list (materials specified alongside the seed) is included but left loosely
|
|
56
|
+
* typed — there is no captured fixture to pin its full shape.
|
|
57
|
+
*
|
|
58
|
+
* `resolution` is the honest report on the free-text seed: `ambiguous` means the
|
|
59
|
+
* phrase matched several materials and `seedAlternatives` holds them — pick one
|
|
60
|
+
* and re-ask by `variantId` rather than treating the first as the answer.
|
|
61
|
+
*/
|
|
62
|
+
export interface CoSpecifiedResult {
|
|
63
|
+
found: boolean;
|
|
64
|
+
resolution: OpenEnum<"resolved" | "ambiguous" | "not_found">;
|
|
65
|
+
query: string;
|
|
66
|
+
seed: DemandSeed | null;
|
|
67
|
+
seedCategory: string | null;
|
|
68
|
+
seedAlternatives: DemandSeed[];
|
|
69
|
+
companions?: unknown[];
|
|
70
|
+
}
|
|
71
|
+
/** The `/api/v1/demand` envelope — both domain reads side by side, verbatim. */
|
|
72
|
+
export interface DemandResponse {
|
|
73
|
+
query: string;
|
|
74
|
+
demand: MaterialDemandResult;
|
|
75
|
+
cospec: CoSpecifiedResult;
|
|
76
|
+
}
|
|
77
|
+
/** Seed a demand read by EXACTLY ONE of `variantId` or `material`. */
|
|
78
|
+
export interface DemandParams {
|
|
79
|
+
/** An exact variant id. Opaque — pass through verbatim. */
|
|
80
|
+
variantId?: VariantId;
|
|
81
|
+
/** Free text ("Brand Product Colour"), resolved server-side. May be ambiguous. */
|
|
82
|
+
material?: string;
|
|
83
|
+
}
|
|
84
|
+
export interface CoordinateSeed extends DemandSeed {
|
|
85
|
+
/** The input reference this seed resolved from. */
|
|
86
|
+
resolvedFrom: string;
|
|
87
|
+
}
|
|
88
|
+
export interface CoordinateCompanion {
|
|
89
|
+
/** Opaque — round-trip verbatim. */
|
|
90
|
+
variantId: VariantId;
|
|
91
|
+
brand: string;
|
|
92
|
+
product: string;
|
|
93
|
+
color: string | null;
|
|
94
|
+
variantName: string | null;
|
|
95
|
+
imageUrl: string | null;
|
|
96
|
+
brandSlug: string;
|
|
97
|
+
productId: ProductId;
|
|
98
|
+
totalLift: number;
|
|
99
|
+
liftLabel: string;
|
|
100
|
+
seedMatchCount: number;
|
|
101
|
+
seedContributions: Array<{
|
|
102
|
+
seedVariantId: VariantId;
|
|
103
|
+
lift: number;
|
|
104
|
+
}>;
|
|
105
|
+
demand: CompanionDemand | null;
|
|
106
|
+
swatches: Array<{
|
|
107
|
+
hex: string;
|
|
108
|
+
proportion: number;
|
|
109
|
+
}>;
|
|
110
|
+
/** Annotate-only appearance similarity — never reorders companions in v1. */
|
|
111
|
+
colourFit: number | null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The `POST /api/v1/pairings/coordinate` envelope. `unresolved` lists the input
|
|
115
|
+
* references that matched nothing — read it before presenting `companions` as
|
|
116
|
+
* "what goes with your palette", because a companion set derived from two of
|
|
117
|
+
* four seeds is a different claim.
|
|
118
|
+
*/
|
|
119
|
+
export interface CoordinateResponse {
|
|
120
|
+
seeds: CoordinateSeed[];
|
|
121
|
+
/** Input references that resolved to no material. Never silently dropped. */
|
|
122
|
+
unresolved: string[];
|
|
123
|
+
window: "trailing_36mo";
|
|
124
|
+
companions: CoordinateCompanion[];
|
|
125
|
+
}
|
|
126
|
+
/** Body for `POST /api/v1/pairings/coordinate`. */
|
|
127
|
+
export interface CoordinateInput {
|
|
128
|
+
/** Free-text material references, resolved server-side; misses land in `unresolved`. */
|
|
129
|
+
materials?: string[];
|
|
130
|
+
/** Exact variant ids. Opaque — pass through verbatim. */
|
|
131
|
+
variantIds?: VariantId[];
|
|
132
|
+
projectType?: string;
|
|
133
|
+
limit?: number;
|
|
134
|
+
}
|
package/demand-types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|