@instruments/materialgraph 0.5.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.
@@ -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
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The identifier vocabulary — WHICH strings you may look inside, and which you
3
+ * may not.
4
+ *
5
+ * MaterialGraph hands back two very different kinds of string, and the whole
6
+ * class of caller bugs this file exists to prevent comes from confusing them:
7
+ *
8
+ * - **Opaque tokens** ({@link VariantId}, {@link ProductId}, {@link RunId},
9
+ * {@link BatchId}, {@link ProjectId}, {@link SchemeId}, {@link SlotId},
10
+ * {@link Cursor}). Round-trip them BYTE-FOR-BYTE. Never parse one, never
11
+ * construct one, never pattern-match one, never sort by one, and never read
12
+ * meaning out of its length, prefix or character set. `62f8qmqb62nib58qnhf50`
13
+ * has no structure, and the prefixes that do exist today (`smb_…`, `smf_…`)
14
+ * are a convenience for humans reading logs, not a contract.
15
+ * - **Canonical vocabulary** — `attributeId`, `term`, `productTypeId`,
16
+ * `categoryKey`, `jurisdictionId`, `brandSlug`. These ARE meaningful, stable,
17
+ * and yours to compose queries from — but you DISCOVER them rather than
18
+ * invent them (`specificationFacets()`, `attributeValues()`,
19
+ * `searchVocabulary()`, `taxonomy()`, `colourFacets()`, `codes.context()`).
20
+ * A hand-guessed canonical token is the second-most-common caller bug.
21
+ *
22
+ * Every alias below is exactly `string`. They carry no runtime weight and no
23
+ * nominal typing — they are documentation you can hover, and a signpost at the
24
+ * point of use.
25
+ */
26
+ /**
27
+ * An OPAQUE identifier token. Preserve it verbatim; never parse, construct, or
28
+ * pattern-match one. See the module docblock for the full rule.
29
+ */
30
+ export type OpaqueId = string;
31
+ /**
32
+ * A variant id — the identity of ONE buyable material (this product in this
33
+ * colourway/finish). The unit almost every read is keyed on.
34
+ *
35
+ * Opaque: preserve verbatim, never parse or construct one.
36
+ */
37
+ export type VariantId = OpaqueId;
38
+ /**
39
+ * A product id — the family a {@link VariantId} belongs to. Sibling variants
40
+ * share it.
41
+ *
42
+ * Opaque: preserve verbatim, never parse or construct one.
43
+ */
44
+ export type ProductId = OpaqueId;
45
+ /**
46
+ * A Run id from the hosted agentic surface. Opaque: preserve verbatim, never
47
+ * parse or construct one.
48
+ */
49
+ export type RunId = OpaqueId;
50
+ /**
51
+ * A source-material batch id (`smb_…`). The prefix is a reading convenience,
52
+ * not a contract — treat the whole string as opaque.
53
+ */
54
+ export type BatchId = OpaqueId;
55
+ /** A stored source-material file id (`smf_…`). Opaque, prefix notwithstanding. */
56
+ export type SourceFileId = OpaqueId;
57
+ /** A persistent Material Box project id. Opaque. */
58
+ export type ProjectId = OpaqueId;
59
+ /** A composition scheme id, unique within its Run. Opaque. */
60
+ export type SchemeId = OpaqueId;
61
+ /** A composition slot id, unique within its scheme. Opaque. */
62
+ export type SlotId = OpaqueId;
63
+ /**
64
+ * A pagination cursor. Opaque keyset state, not an offset and not a page
65
+ * number: pass the value you were handed back unchanged to continue, and never
66
+ * synthesise one. A response with no cursor is the end of the walk.
67
+ *
68
+ * A cursor is only valid for the query that produced it — changing sort,
69
+ * filters or limit invalidates it.
70
+ */
71
+ export type Cursor = string;
package/identifiers.js ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The identifier vocabulary — WHICH strings you may look inside, and which you
3
+ * may not.
4
+ *
5
+ * MaterialGraph hands back two very different kinds of string, and the whole
6
+ * class of caller bugs this file exists to prevent comes from confusing them:
7
+ *
8
+ * - **Opaque tokens** ({@link VariantId}, {@link ProductId}, {@link RunId},
9
+ * {@link BatchId}, {@link ProjectId}, {@link SchemeId}, {@link SlotId},
10
+ * {@link Cursor}). Round-trip them BYTE-FOR-BYTE. Never parse one, never
11
+ * construct one, never pattern-match one, never sort by one, and never read
12
+ * meaning out of its length, prefix or character set. `62f8qmqb62nib58qnhf50`
13
+ * has no structure, and the prefixes that do exist today (`smb_…`, `smf_…`)
14
+ * are a convenience for humans reading logs, not a contract.
15
+ * - **Canonical vocabulary** — `attributeId`, `term`, `productTypeId`,
16
+ * `categoryKey`, `jurisdictionId`, `brandSlug`. These ARE meaningful, stable,
17
+ * and yours to compose queries from — but you DISCOVER them rather than
18
+ * invent them (`specificationFacets()`, `attributeValues()`,
19
+ * `searchVocabulary()`, `taxonomy()`, `colourFacets()`, `codes.context()`).
20
+ * A hand-guessed canonical token is the second-most-common caller bug.
21
+ *
22
+ * Every alias below is exactly `string`. They carry no runtime weight and no
23
+ * nominal typing — they are documentation you can hover, and a signpost at the
24
+ * point of use.
25
+ */
26
+ export {};
package/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export { MaterialGraphClient, DEFAULT_BASE_URL, MAX_BATCH_IDS } from "./client.j
2
2
  export type { MaterialGraphClientOptions, BrowseNamespace } from "./client.js";
3
3
  export { MATERIALGRAPH_API_VERSION, SDK_OPERATIONS } from "./operations.js";
4
4
  export type { SdkOperation, SdkOperationId } from "./operations.js";
5
+ export type * from "./identifiers.js";
5
6
  export { TERMINAL_RUN_EVENT_TYPES } from "./run-types.js";
6
7
  export type * from "./run-types.js";
7
8
  export type * from "./schedule-types.js";
@@ -1,5 +1,22 @@
1
+ /**
2
+ * Wire types for the material ASSET projection
3
+ * (`GET /api/v1/materials/{variantId}/assets`).
4
+ *
5
+ * THE LENS: "what may I show for this material?" — the redistribution-safe
6
+ * images and downloads, not every file on record. An asset absent here is not
7
+ * an asset that does not exist; it is one this surface does not hand out.
8
+ */
9
+ import type { VariantId } from "./identifiers.js";
1
10
  import type { OpenEnum } from "./types.js";
2
11
  export type MaterialAssetVisualRole = OpenEnum<"grid" | "detail" | "scene" | "swatch" | "technical" | "unknown">;
12
+ /**
13
+ * Whether we hold the bytes.
14
+ * - `downloaded` — held and served by us.
15
+ * - `linked_not_downloaded` — the source names it; we have not fetched it. Not
16
+ * a broken asset.
17
+ * - `failed` — we tried and could not. Says nothing about the source.
18
+ * - `retired_placeholder` — a stand-in for a withdrawn asset.
19
+ */
3
20
  export type MaterialAssetStatus = OpenEnum<"downloaded" | "linked_not_downloaded" | "failed" | "retired_placeholder">;
4
21
  export interface MaterialAssetProjectionImage {
5
22
  key: string;
@@ -22,13 +39,15 @@ export interface MaterialAssetProjectionDownload {
22
39
  fileExtension: string | null;
23
40
  filename: string | null;
24
41
  }
42
+ /** Tallies over what this projection returned — never a claim about the brand's own library. */
25
43
  export interface MaterialAssetProjectionSummary {
26
44
  imageCount: number;
27
45
  downloadCount: number;
28
46
  downloadedImageCount: number;
29
47
  }
30
48
  export interface MaterialAssetProjection {
31
- variantId: string;
49
+ /** Opaque — echoed from the request. */
50
+ variantId: VariantId;
32
51
  images: MaterialAssetProjectionImage[];
33
52
  downloads: MaterialAssetProjectionDownload[];
34
53
  summary: MaterialAssetProjectionSummary;
@@ -1,3 +1,16 @@
1
+ /**
2
+ * Wire types for the block-coverage projection
3
+ * (`GET /api/v1/materials/{variantId}/blocks`).
4
+ *
5
+ * THE AUTHORITY ON "ABSENT IS NOT FALSE". Everywhere else in the SDK an absent
6
+ * value is ambiguous — you cannot tell "we looked and there is none" from
7
+ * "nobody has looked". This projection is the one read that says WHICH, per
8
+ * field, in {@link MaterialCoverageStatus}. When a caller needs to render a
9
+ * negative ("this material carries no fire rating") rather than an unknown, this
10
+ * is the only surface entitled to back it: a `verified` field with a `false` /
11
+ * `0` / `[]` value. Never derive a negative from a value's absence elsewhere.
12
+ */
13
+ import type { VariantId } from "./identifiers.js";
1
14
  import type { OpenEnum } from "./types.js";
2
15
  /**
3
16
  * Coverage status, shared by blocks and fields.
@@ -22,8 +35,18 @@ import type { OpenEnum } from "./types.js";
22
35
  export type MaterialCoverageStatus = OpenEnum<"verified" | "partial" | "missing" | "not_applicable">;
23
36
  export type MaterialBlockStatus = MaterialCoverageStatus;
24
37
  export type MaterialBlockFieldStatus = MaterialCoverageStatus;
38
+ /**
39
+ * A field's value. `null` is AMBIGUOUS on its own — pair it with `status`:
40
+ * `verified` + `null` is a determined absence, `missing` + `null` is unread.
41
+ */
25
42
  export type MaterialBlockFieldValue = string | number | boolean | unknown[] | Record<string, unknown> | null;
43
+ /**
44
+ * One field of a block. READ `status` BEFORE `value` — a `false` under
45
+ * `status: "verified"` is a real negative you may publish; the same `false`
46
+ * under `missing` is a placeholder for an unanswered question.
47
+ */
26
48
  export interface MaterialBlockProjectionField {
49
+ /** Canonical vocabulary, not an opaque id — the same token `bySpec` accepts. */
27
50
  attributeId: string;
28
51
  label: string;
29
52
  value: MaterialBlockFieldValue;
@@ -35,9 +58,15 @@ export interface MaterialBlockProjectionItem {
35
58
  canonicalBlockName: string;
36
59
  status: MaterialBlockStatus;
37
60
  fields: MaterialBlockProjectionField[];
61
+ /** The work queue: attributes with `status: "missing"`. UNKNOWN, never "none". */
38
62
  missingAttributeIds: string[];
39
63
  }
64
+ /**
65
+ * Every canonical block for one material, each field carrying whether it is
66
+ * determined. The whole surface, not a page — there is no cursor here.
67
+ */
40
68
  export interface MaterialBlockProjection {
41
- variantId: string;
69
+ /** Opaque — echoed from the request. */
70
+ variantId: VariantId;
42
71
  blocks: MaterialBlockProjectionItem[];
43
72
  }
@@ -6,8 +6,23 @@
6
6
  * stays within the repository's source-size budget. Same contract, same
7
7
  * tolerant-parsing rules: response enums are OPEN, so `MaterialAvailability` is
8
8
  * widened — a new server state still type-checks.
9
+ *
10
+ * ABSENT IS NOT FALSE, and it is the rule this whole file hangs on. A material
11
+ * record reports what the graph HOLDS, never what is true of the object in the
12
+ * world. `null` and a missing key both mean UNKNOWN:
13
+ * - `dimensions: null` does not mean the material has no dimensions.
14
+ * - `supersededBy: null` does not mean the product is current.
15
+ * - a `facets` array without an attribute does not mean the material lacks that
16
+ * property — it means nobody has recorded one.
17
+ * - most dangerously, no boolean here may be inverted from an absence. Never
18
+ * render "not certified" / "no fire rating" from a missing entry; render
19
+ * "not on record".
20
+ * Read {@link MaterialBlockProjection} when you need to tell "we looked and
21
+ * there is none" apart from "nobody has looked" — that is the only projection
22
+ * that says which.
9
23
  */
10
24
  import type { ImageSource, MaterialColour, OpenEnum, PaletteSwatch } from "./types.js";
25
+ import type { ProductId, VariantId } from "./identifiers.js";
11
26
  /**
12
27
  * Whether a material can still be bought.
13
28
  *
@@ -27,9 +42,10 @@ export type MaterialAvailability = OpenEnum<"available" | "discontinued" | "supe
27
42
  * here; follow `variantId` for the next link in the chain.
28
43
  */
29
44
  export interface MaterialSuccessor {
30
- variantId: string;
45
+ /** Opaque — round-trip verbatim to read the successor. */
46
+ variantId: VariantId;
31
47
  brandSlug: string;
32
- productId: string;
48
+ productId: ProductId;
33
49
  brand: string;
34
50
  product: string;
35
51
  variant: string;
@@ -41,7 +57,8 @@ export interface MaterialSuccessor {
41
57
  * successor record — read the detail projection for that.
42
58
  */
43
59
  export interface MaterialTile {
44
- variantId: string;
60
+ /** Opaque — round-trip verbatim into every other material read. */
61
+ variantId: VariantId;
45
62
  brand: string;
46
63
  product: string;
47
64
  variantName: string | null;
@@ -51,7 +68,14 @@ export interface MaterialTile {
51
68
  colour: MaterialColour;
52
69
  availability: MaterialAvailability;
53
70
  }
54
- /** A canonical attribute/term pair carried on a detail record. */
71
+ /**
72
+ * A canonical attribute/term pair carried on a detail record.
73
+ *
74
+ * `attributeId` and `term` are canonical VOCABULARY, not opaque ids — the same
75
+ * tokens `bySpec` / `search({ filters })` accept, discoverable through
76
+ * `specificationFacets()` and `attributeValues()`. An attribute absent from
77
+ * `facets` is unrecorded, never known-false.
78
+ */
55
79
  export interface MaterialFacet {
56
80
  attributeId: string;
57
81
  term: string;
@@ -75,18 +99,32 @@ export interface DimensionEntry {
75
99
  weight_kg?: number;
76
100
  [key: string]: unknown;
77
101
  }
78
- /** The record projection of a material — the tile plus detail-only fields. */
102
+ /**
103
+ * The record projection of a material — the tile plus detail-only fields.
104
+ *
105
+ * The deepest read of ONE material, and still not an assertion about the object:
106
+ * every `| null` below is UNKNOWN, not "none". See the module docblock.
107
+ */
79
108
  export interface MaterialDetail extends MaterialTile {
80
109
  brandSlug: string;
81
- productId: string;
110
+ /** Opaque — the product family; siblings share it. */
111
+ productId: ProductId;
82
112
  color: string | null;
113
+ /** Material Bank's own opaque id, when one is mapped. Null = unmapped, not absent. */
83
114
  materialBankId: string | null;
84
115
  installationImageUrl: string | null;
85
116
  displayCategory: string | null;
86
117
  categoryPath: string[];
118
+ /** Extracted from imagery — visual likeness, never a declared colour spec. */
87
119
  palette: PaletteSwatch[];
120
+ /** What is ON RECORD. An absent attribute is unrecorded, never known-false. */
88
121
  facets: MaterialFacet[];
122
+ /** `null` = no dimensions recorded, NOT a dimensionless material. */
89
123
  dimensions: DimensionEntry[] | null;
90
- /** The declared successor, or null when no reviewed relationship names one. */
124
+ /**
125
+ * The declared successor, or null when NO REVIEWED RELATIONSHIP NAMES ONE —
126
+ * which is not the same as "this product is current". Read `availability` for
127
+ * that, and even then `unknown` is a real answer.
128
+ */
91
129
  supersededBy: MaterialSuccessor | null;
92
130
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instruments/materialgraph",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -11,6 +11,7 @@
11
11
  * {@link SearchPaletteInput.logic} and {@link GradientMatch.coverage} rather
12
12
  * than left to the endpoint docs.
13
13
  */
14
+ import type { VariantId } from "./identifiers.js";
14
15
  import type { OklabTriple, OpenEnum } from "./types.js";
15
16
  /** How the per-colour scores are combined across a palette. */
16
17
  export type PaletteLogic = OpenEnum<"all" | "any">;
@@ -50,7 +51,11 @@ export interface SearchGradientInput {
50
51
  /** Results to return; defaults to 24, capped at 48. */
51
52
  limit?: number;
52
53
  }
53
- /** One swatch of a candidate's colorscope-extracted palette. */
54
+ /**
55
+ * One swatch of a candidate's colorscope-EXTRACTED palette. Derived from product
56
+ * imagery, never a manufacturer's declared colour. `name` is null when no human
57
+ * name was resolved — unnamed, not colourless.
58
+ */
54
59
  export interface CandidateSwatch {
55
60
  hex: string;
56
61
  proportion: number;
@@ -70,7 +75,8 @@ export interface ColourMatchReceipt {
70
75
  }
71
76
  /** Identity and evidence shared by both multi-colour match shapes. */
72
77
  export interface MultiColourMatchBase {
73
- variantId: string;
78
+ /** Opaque — round-trip verbatim. */
79
+ variantId: VariantId;
74
80
  brand: string;
75
81
  product: string;
76
82
  brandSlug: string | null;
@@ -99,7 +105,13 @@ export interface GradientMatch extends MultiColourMatchBase {
99
105
  /** One receipt per sampled point along the path, in path order. */
100
106
  samples: ColourMatchReceipt[];
101
107
  }
108
+ /**
109
+ * A PAGE, NOT A CORPUS. `count` is `matches.length` under `limit` (24 default,
110
+ * 48 max) — this surface states no corpus total and has no cursor, so "N
111
+ * materials match this palette" is not a claim you can make from it.
112
+ */
102
113
  export interface SearchPaletteResponse {
114
+ /** The query as interpreted — check it before trusting a surprising order. */
103
115
  query: {
104
116
  colours: Array<{
105
117
  hex: string;
@@ -108,15 +120,18 @@ export interface SearchPaletteResponse {
108
120
  }>;
109
121
  logic: PaletteLogic;
110
122
  };
123
+ /** `matches.length`, capped by `limit`. Never a corpus total. */
111
124
  count: number;
112
125
  matches: PaletteMatch[];
113
126
  }
127
+ /** A PAGE, NOT A CORPUS — see {@link SearchPaletteResponse}. */
114
128
  export interface SearchGradientResponse {
115
129
  query: {
116
130
  stops: string[];
117
131
  /** The sampled path points the search actually ran against. */
118
132
  samples: string[];
119
133
  };
134
+ /** `matches.length`, capped by `limit`. Never a corpus total. */
120
135
  count: number;
121
136
  matches: GradientMatch[];
122
137
  }
@@ -4,7 +4,16 @@
4
4
  * has zero runtime dependency on `@materialgraph/*`. Kept shape-identical to the
5
5
  * server contract; the OpenAPI parity test gates method/path/operationId, and the
6
6
  * server route schemas gate the body shapes.
7
+ *
8
+ * THE LENS: a project is the DESIGNER'S OWN record — an append-only trail of
9
+ * what they attached, asked, preferred and adopted. It holds no material data of
10
+ * its own; every `variantId`, `batchId` and `runId` in it is a reference into
11
+ * the other lenses. Read the folded collections on {@link ProjectState}; the raw
12
+ * `events` log is there for audit, not for reconstruction.
13
+ *
14
+ * Every id here is OPAQUE — round-trip verbatim, never parse or construct one.
7
15
  */
16
+ import type { BatchId, ProjectId, RunId, VariantId } from "./identifiers.js";
8
17
  /** The six governed project-event kinds. */
9
18
  export type ProjectEventKind = "batch-attached" | "run-started" | "clarifying-answer" | "pairwise-preference" | "reference-chosen" | "scheme-adopted";
10
19
  /** The frozen tuple of every project-event kind, for enumeration. */
@@ -14,11 +23,11 @@ export interface ClarifyingAnswer {
14
23
  question: string;
15
24
  answer: string;
16
25
  }
17
- /** One pairwise preference; `chosenId` is `leftId` or `rightId`. */
26
+ /** One pairwise preference; `chosenId` is `leftId` or `rightId`. All three opaque. */
18
27
  export interface PairwisePreference {
19
- leftId: string;
20
- rightId: string;
21
- chosenId: string;
28
+ leftId: VariantId;
29
+ rightId: VariantId;
30
+ chosenId: VariantId;
22
31
  }
23
32
  /** One stored event; `payload` is validated to the shape of `kind`. */
24
33
  export interface ProjectEvent {
@@ -29,7 +38,8 @@ export interface ProjectEvent {
29
38
  }
30
39
  /** A lightweight project row for list views (no event fold). */
31
40
  export interface ProjectSummary {
32
- projectId: string;
41
+ /** Opaque — round-trip verbatim. */
42
+ projectId: ProjectId;
33
43
  principalId: string;
34
44
  name: string;
35
45
  createdAt: string;
@@ -37,8 +47,10 @@ export interface ProjectSummary {
37
47
  }
38
48
  /** A project's assembled read model — identity plus the folded event collections. */
39
49
  export interface ProjectState extends ProjectSummary {
40
- batchIds: string[];
41
- runIds: string[];
50
+ /** Source-material batches attached, oldest first. Opaque. */
51
+ batchIds: BatchId[];
52
+ /** Runs started from this project, oldest first. Opaque. */
53
+ runIds: RunId[];
42
54
  answers: ClarifyingAnswer[];
43
55
  preferences: PairwisePreference[];
44
56
  chosenReferences: string[];
@@ -61,7 +73,7 @@ export interface AppendProjectEventRequest {
61
73
  /** `POST /api/v1/projects/{projectId}/events` response — the appended event receipt. */
62
74
  export interface ProjectEventReceipt {
63
75
  eventId: string;
64
- projectId: string;
76
+ projectId: ProjectId;
65
77
  kind: ProjectEventKind;
66
78
  createdAt: string;
67
79
  }
package/project-types.js CHANGED
@@ -1,10 +1,3 @@
1
- /**
2
- * Wire types for the persistent Material Box project surface (`/api/v1/projects`).
3
- * Hand-mirrored from `@materialgraph/products/projects`' Zod contract — the SDK
4
- * has zero runtime dependency on `@materialgraph/*`. Kept shape-identical to the
5
- * server contract; the OpenAPI parity test gates method/path/operationId, and the
6
- * server route schemas gate the body shapes.
7
- */
8
1
  /** The frozen tuple of every project-event kind, for enumeration. */
9
2
  export const PROJECT_EVENT_KINDS = Object.freeze([
10
3
  "batch-attached",
package/run-types.d.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  * so a new server value still type-checks, and objects are read structurally —
11
11
  * unknown keys pass through untouched.
12
12
  */
13
+ import type { BatchId, RunId, SourceFileId } from "./identifiers.js";
13
14
  import type { JsonValue, OpenEnum } from "./types.js";
14
15
  /**
15
16
  * A file to upload. Two forms so the SDK is ergonomic on BOTH Node route
@@ -52,8 +53,8 @@ export interface UploadSourceMaterialLegacyInput {
52
53
  export type UploadSourceMaterialInput = UploadSourceMaterialFilesInput | UploadSourceMaterialLegacyInput;
53
54
  /** One stored file in an upload receipt. */
54
55
  export interface SourceMaterialReceiptFile {
55
- /** The stored file id (`smf_…`). */
56
- sourceId: string;
56
+ /** The stored file id (`smf_…`). Opaque — the prefix is a reading aid, not a contract. */
57
+ sourceId: SourceFileId;
57
58
  /**
58
59
  * The pipeline-assigned role. The vocabulary grows over time; {@link OpenEnum}
59
60
  * keeps a newer server value type-checking. `unclassified` is a legitimate
@@ -70,8 +71,8 @@ export interface SourceMaterialReceiptFile {
70
71
  }
71
72
  /** The `201` receipt from `POST /api/v1/source-material`. */
72
73
  export interface SourceMaterialBatchReceipt {
73
- /** The stored batch id (`smb_…`). */
74
- batchId: string;
74
+ /** The stored batch id (`smb_…`). Opaque — hand it straight to a compose Run. */
75
+ batchId: BatchId;
75
76
  sources: SourceMaterialReceiptFile[];
76
77
  }
77
78
  /**
@@ -120,8 +121,8 @@ export interface SourceMaterialBatchFile extends SourceMaterialReceiptFile {
120
121
  * file it holds, and the analyses computed over each file's bytes.
121
122
  */
122
123
  export interface SourceMaterialBatchDetail {
123
- /** The stored batch id (`smb_…`). */
124
- batchId: string;
124
+ /** The stored batch id (`smb_…`). Opaque. */
125
+ batchId: BatchId;
125
126
  /** ISO-8601 timestamp of the upload. */
126
127
  createdAt: string;
127
128
  sources: SourceMaterialBatchFile[];
@@ -180,16 +181,23 @@ export interface PublicRunArtifactRef {
180
181
  * never returned — inputs and outputs are the `redacted*Json` projections.
181
182
  */
182
183
  export interface PublicRun {
183
- id: string;
184
+ /** Opaque — round-trip verbatim into every other Run call. */
185
+ id: RunId;
184
186
  capabilityKind: CapabilityKind;
185
187
  capabilityId: string;
186
188
  clientReference: string | null;
189
+ /** `suspended` is a QUESTION, not a failure — see `suspensionStepId`. */
187
190
  status: PublicRunStatus;
188
191
  redactedInputJson: Record<string, unknown>;
189
192
  redactedOutputJson: Record<string, unknown> | null;
190
193
  usageJson: Record<string, unknown> | null;
191
194
  artifactRefs: PublicRunArtifactRef[] | null;
192
195
  failureJson: PublicRunFailure | null;
196
+ /**
197
+ * The step awaiting input while `status` is `suspended`. Pass it as
198
+ * {@link ResumeRunOptions.stepId} so a stale client cannot resume a step the
199
+ * Run has already moved past.
200
+ */
193
201
  suspensionStepId: string | null;
194
202
  eventCursor: number;
195
203
  attempts: number;
@@ -205,7 +213,11 @@ export interface CreatePublicRunRequest {
205
213
  capabilityId: string;
206
214
  /** The capability input, validated server-side against its live input schema. */
207
215
  inputJson: unknown;
208
- /** Stable caller key making Run creation idempotent across retries. */
216
+ /**
217
+ * Stable caller key making Run creation idempotent across retries. Supply it:
218
+ * without one, a retried POST starts a SECOND Run rather than replaying the
219
+ * first (`replayed` on the response tells you which happened).
220
+ */
209
221
  idempotencyKey?: string;
210
222
  /** Free-form caller correlation reference, echoed back on the Run. */
211
223
  clientReference?: string;