@instruments/materialgraph 0.6.1 → 0.7.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.
@@ -1,3 +1,4 @@
1
+ import type { ValueResolution } from "./catalog-discovery-types.js";
1
2
  import type { VariantId } from "./identifiers.js";
2
3
  /**
3
4
  * One criterion a match satisfied, with the trust behind it.
@@ -107,6 +108,32 @@ export interface BySpecMatch {
107
108
  minConfidence: number | null;
108
109
  verdict: BySpecVerdict;
109
110
  }
111
+ /**
112
+ * One criterion the server could not resolve to canonical vocabulary, with the
113
+ * resolver's ranked suggestions for it.
114
+ *
115
+ * `attributeId` and `value` echo the parsed criterion, both of them, so a failed
116
+ * query can be restated without losing either half. (The HTTP route trims the
117
+ * attribute and value around the first `:` before validation.) Which HALF failed
118
+ * is read off `resolution.query`, the token actually resolved:
119
+ *
120
+ * - `resolution.query === attributeId` — the attribute id is unknown, and
121
+ * `resolution.candidates` are ATTRIBUTE ids.
122
+ * - `resolution.query === value` — the attribute is known but its value is no
123
+ * canonical term, and `resolution.candidates` are TERM ids, each carrying the
124
+ * `dictionaryId` it came from.
125
+ *
126
+ * EVERY miss is reported, not just the first, so one round trip is enough to fix
127
+ * a whole spec.
128
+ */
129
+ export interface BySpecUnresolvedCriterion {
130
+ /** The parsed attribute id, after the route's surrounding-whitespace trim. */
131
+ attributeId: string;
132
+ /** The parsed value — never overwritten by the attribute id when that id is unknown. */
133
+ value: string;
134
+ /** Ranked candidates for whichever half failed. See {@link ValueResolution}. */
135
+ resolution: ValueResolution;
136
+ }
110
137
  /**
111
138
  * The find-by-spec envelope — A PAGE, AND NOTHING BUT A PAGE.
112
139
  *
@@ -116,6 +143,9 @@ export interface BySpecMatch {
116
143
  * states NO corpus total, so "N materials meet this specification" is not a
117
144
  * claim you can make from one response. Raise `limit`, or say "at least N".
118
145
  * `brandsSpanned` is likewise the brand count OF THIS SAMPLE.
146
+ *
147
+ * And check `unresolvedCriteria` BEFORE reading any of that: when it is present
148
+ * the numbers beside it describe a query that never ran.
119
149
  */
120
150
  export interface BySpecResponse {
121
151
  /** The criteria as parsed — check them before trusting a surprising result. */
@@ -129,4 +159,19 @@ export interface BySpecResponse {
129
159
  brandsSpanned: number;
130
160
  /** The sample, photo-first, capped by `limit`. */
131
161
  matches: BySpecMatch[];
162
+ /**
163
+ * Present ONLY when a criterion failed exact canonical-vocabulary membership —
164
+ * and its presence means THE QUERY WAS NEVER RUN. A pre-flight gate short-circuits
165
+ * before the search, so the rest of the envelope is a placeholder rather than
166
+ * a finding: `matches` is `[]` and `totalMatches` is `0` because nothing was
167
+ * searched, not because nothing qualifies. Reporting that zero as "no material
168
+ * meets this specification" states a fact about the corpus on the strength of
169
+ * a question nobody asked it.
170
+ *
171
+ * Treat these criteria as UNAPPLIED and surface them: retry with a
172
+ * `resolution.candidates` id (subject to `recommendedAction` — `ambiguous`
173
+ * means ask, not pick), or say the value could not be resolved. Absent means
174
+ * every criterion resolved and the query did run.
175
+ */
176
+ unresolvedCriteria?: BySpecUnresolvedCriterion[];
132
177
  }
@@ -24,6 +24,34 @@ export interface ResolveValueCandidate {
24
24
  /** Present only for a colour_family candidate resolved via the colorscope fallback. */
25
25
  via?: OpenEnum<"colorscope">;
26
26
  }
27
+ /**
28
+ * The resolver's verdict on ONE free-text token — the single shape every
29
+ * resolving surface speaks, so a caller learns to read it once. Returned whole
30
+ * by {@link MaterialGraphClient.resolveAttributeValue}, and carried per bad
31
+ * criterion by `bySpec`'s `unresolvedCriteria`.
32
+ *
33
+ * READ `recommendedAction`, NOT JUST `best`. `ambiguous` with a populated
34
+ * `candidates` list means the phrase genuinely matched several terms — taking
35
+ * `best` silently there is how a query ends up answering a question nobody
36
+ * asked. `unresolved` means no candidate cleared the resolver's confidence
37
+ * threshold (and may still carry weak candidates); it says nothing about how
38
+ * many materials would match a corrected criterion.
39
+ *
40
+ * The resolver ranks, classifies and reports; it never substitutes the nearest
41
+ * token for the one you asked for. `best` is populated whenever ANY candidate
42
+ * exists — including under an `ambiguous` or `unresolved` status — so it is a
43
+ * leader, not a licence.
44
+ *
45
+ * `query` is the trimmed token that was actually resolved, which is how you tell
46
+ * WHICH half of a failed `attribute:value` pair was the problem.
47
+ */
48
+ export interface ValueResolution {
49
+ status: OpenEnum<"resolved" | "ambiguous" | "unresolved">;
50
+ query: string;
51
+ best: ResolveValueCandidate | null;
52
+ candidates: ResolveValueCandidate[];
53
+ recommendedAction: OpenEnum<"use_best" | "choose_from_candidates" | "ask_user" | "none_found">;
54
+ }
27
55
  /** Response from resolving a free-text value against one attribute's dictionary. */
28
56
  export interface ResolveAttributeValueResponse {
29
57
  attribute: {
@@ -31,20 +59,8 @@ export interface ResolveAttributeValueResponse {
31
59
  name: string;
32
60
  dictionaryId: string;
33
61
  };
34
- /**
35
- * READ `recommendedAction`, NOT JUST `best`. `ambiguous` with a populated
36
- * `candidates` list means the phrase genuinely matched several terms — taking
37
- * `best` silently there is how a query ends up answering a question nobody
38
- * asked. `unresolved` means the vocabulary holds nothing like it, which is not
39
- * the same as the corpus holding no such materials.
40
- */
41
- resolution: {
42
- status: OpenEnum<"resolved" | "ambiguous" | "unresolved">;
43
- query: string;
44
- best: ResolveValueCandidate | null;
45
- candidates: ResolveValueCandidate[];
46
- recommendedAction: OpenEnum<"use_best" | "choose_from_candidates" | "ask_user" | "none_found">;
47
- };
62
+ /** The verdict on `q`. Read `recommendedAction` — see {@link ValueResolution}. */
63
+ resolution: ValueResolution;
48
64
  }
49
65
  /** Options for searching registry attributes and value-dictionary terms together. */
50
66
  export interface VocabularySearchOptions {
package/client.d.ts CHANGED
@@ -333,7 +333,10 @@ export declare class MaterialGraphClient {
333
333
  * Instead use `search({ filters })` when the spec merely GATES a ranked
334
334
  * question, `codes.advisory()` when the real question is what a space demands,
335
335
  * and {@link MaterialGraphClient.specificationFacets} first if you do not know
336
- * the canonical tokens — guessing one returns an honest, empty answer.
336
+ * the canonical tokens. Guessing one no longer returns a silently empty
337
+ * answer: an unresolvable criterion means the query NEVER RAN, and the
338
+ * response says so — read {@link BySpecResponse.unresolvedCriteria} before
339
+ * treating an empty `matches` as "nothing qualifies".
337
340
  *
338
341
  * A PAGE, NOT A CORPUS, unusually so: `totalMatches` is the size of the
339
342
  * returned sample under `limit`, not a corpus count. See {@link BySpecResponse}.
package/client.js CHANGED
@@ -294,7 +294,9 @@ export class MaterialGraphClient {
294
294
  * you how much of the rail is real order evidence.
295
295
  */
296
296
  pairings(variantId, options, signal) {
297
- return this.#get("get_material_pairings", { categories: options?.categories, limit: options?.limit }, signal, { variantId });
297
+ return this.#get("get_material_pairings", { ...options }, signal, {
298
+ variantId,
299
+ });
298
300
  }
299
301
  materials(ids, options, signal) {
300
302
  if (ids.length > MAX_BATCH_IDS) {
@@ -396,7 +398,10 @@ export class MaterialGraphClient {
396
398
  * Instead use `search({ filters })` when the spec merely GATES a ranked
397
399
  * question, `codes.advisory()` when the real question is what a space demands,
398
400
  * and {@link MaterialGraphClient.specificationFacets} first if you do not know
399
- * the canonical tokens — guessing one returns an honest, empty answer.
401
+ * the canonical tokens. Guessing one no longer returns a silently empty
402
+ * answer: an unresolvable criterion means the query NEVER RAN, and the
403
+ * response says so — read {@link BySpecResponse.unresolvedCriteria} before
404
+ * treating an empty `matches` as "nothing qualifies".
400
405
  *
401
406
  * A PAGE, NOT A CORPUS, unusually so: `totalMatches` is the size of the
402
407
  * returned sample under `limit`, not a corpus count. See {@link BySpecResponse}.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instruments/materialgraph",
3
- "version": "0.6.1",
3
+ "version": "0.7.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/types.d.ts CHANGED
@@ -395,6 +395,8 @@ export interface CospecPairing {
395
395
  liftLabel: string;
396
396
  demand: CompanionDemand | null;
397
397
  topProjectTypes: ProjectTypeShare[];
398
+ /** Graph-owned colour ranking facts, present only for an explicitly ordered rail. */
399
+ ranking?: PairingRanking;
398
400
  }
399
401
  /**
400
402
  * A pairing item topped up from colour palette-similarity — a LOOK-ALIKE, not
@@ -405,6 +407,30 @@ export interface CospecPairing {
405
407
  export interface ColourPairing extends MaterialTile {
406
408
  source: "colour";
407
409
  score: number;
410
+ /** Graph-owned colour ranking facts, present only for an explicitly ordered rail. */
411
+ ranking?: PairingRanking;
412
+ }
413
+ export type PairingOrderMode = "relevance" | "lightness_asc" | "lightness_desc";
414
+ export type PairingCorpus = "all" | "material_bank";
415
+ export interface PairingRanking {
416
+ anchorColourSimilarity: number | null;
417
+ lightness: number | null;
418
+ anchorColourSimilarityMeasurable: boolean;
419
+ lightnessMeasurable: boolean;
420
+ basis: OpenEnum<"dominant_colour_oklab">;
421
+ }
422
+ export interface PairingOrdering {
423
+ requested: OpenEnum<PairingOrderMode>;
424
+ applied: OpenEnum<PairingOrderMode>;
425
+ direction: OpenEnum<"ascending" | "descending">;
426
+ anchorColourBasis: OpenEnum<"one_minus_oklab_euclidean_distance">;
427
+ colourSpace: OpenEnum<"oklab">;
428
+ lightnessRange: {
429
+ min: 0;
430
+ max: 1;
431
+ };
432
+ missingValues: OpenEnum<"last">;
433
+ tieBreak: OpenEnum<"variantId_asc">;
408
434
  }
409
435
  /**
410
436
  * One pairing, discriminated by `source` into three tiers of very different
@@ -422,6 +448,10 @@ export interface PairingsResponse {
422
448
  * check it before presenting the rail as demand evidence.
423
449
  */
424
450
  cospecCount: number;
451
+ /** Present only when `order` was requested. Evidence tiers remain independent. */
452
+ ordering?: PairingOrdering;
453
+ /** Opaque continuation bound to the seed, filters, corpus, and order. */
454
+ nextCursor?: Cursor;
425
455
  }
426
456
  export interface MaterialResponse {
427
457
  material: MaterialDetail;
@@ -665,7 +695,14 @@ export interface MaterialsBatchOptions {
665
695
  export interface PairingsOptions {
666
696
  /** Comma-separated coarse category display names; filters both arms. */
667
697
  categories?: string;
698
+ /** Page size, 1–24 inclusive (default 12). The server rejects (400), not clamps, values outside this range. */
668
699
  limit?: number;
700
+ /** Graph-owned order applied within fixed cospec → considered → colour tiers. */
701
+ order?: PairingOrderMode;
702
+ /** Optional Material Bank/orderable gate; requires an explicit `order`. */
703
+ corpus?: PairingCorpus;
704
+ /** The previous response's `nextCursor`, passed back verbatim. */
705
+ cursor?: Cursor;
669
706
  }
670
707
  /** Options for `catalog/attributes/resolve`. */
671
708
  export interface ResolveAttributeOptions {