@instruments/materialgraph 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/by-spec-types.d.ts +73 -0
  2. package/by-spec-types.js +7 -0
  3. package/client.d.ts +186 -7
  4. package/client.js +242 -125
  5. package/codes-client.d.ts +82 -0
  6. package/codes-client.js +73 -0
  7. package/codes-types.d.ts +238 -0
  8. package/codes-types.js +1 -0
  9. package/colour-aggregate-types.d.ts +93 -0
  10. package/colour-aggregate-types.js +1 -0
  11. package/colours-client.d.ts +52 -0
  12. package/colours-client.js +39 -0
  13. package/composition-types.d.ts +343 -0
  14. package/composition-types.js +10 -0
  15. package/http-transport.d.ts +74 -0
  16. package/http-transport.js +201 -0
  17. package/index.d.ts +19 -0
  18. package/index.js +9 -0
  19. package/match-stream-types.d.ts +150 -0
  20. package/match-stream-types.js +28 -0
  21. package/match-stream.d.ts +22 -0
  22. package/match-stream.js +280 -0
  23. package/material-block-types.d.ts +23 -2
  24. package/material-types.d.ts +92 -0
  25. package/material-types.js +1 -0
  26. package/ndjson.d.ts +46 -0
  27. package/ndjson.js +103 -0
  28. package/operations.d.ts +6 -2
  29. package/operations.js +30 -2
  30. package/package.json +1 -1
  31. package/project-types.d.ts +67 -0
  32. package/project-types.js +16 -0
  33. package/projects.d.ts +13 -0
  34. package/projects.js +46 -0
  35. package/run-events.d.ts +82 -0
  36. package/run-events.js +29 -0
  37. package/run-types.d.ts +266 -0
  38. package/run-types.js +6 -0
  39. package/runs.d.ts +84 -0
  40. package/runs.js +253 -0
  41. package/schedule-types.d.ts +88 -0
  42. package/schedule-types.js +12 -0
  43. package/search-refinement-types.d.ts +26 -1
  44. package/search-result-validation.d.ts +27 -0
  45. package/search-result-validation.js +174 -0
  46. package/source-material-validation.d.ts +3 -0
  47. package/source-material-validation.js +109 -0
  48. package/spec-types.d.ts +82 -0
  49. package/spec-types.js +44 -0
  50. package/specifications.d.ts +72 -0
  51. package/specifications.js +82 -0
  52. package/sse.d.ts +35 -0
  53. package/sse.js +147 -0
  54. package/types.d.ts +204 -77
  55. package/validation.d.ts +119 -0
  56. package/validation.js +241 -0
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Finish-schedule wire types (MG-616) — the hand-mirror of
3
+ * `@materialgraph/products/schemes/finish-schedule`. Kept in their own module
4
+ * rather than `types.ts`, which sits at its source-size baseline.
5
+ *
6
+ * A finish schedule is a completed compose scheme rendered as ONE ROW PER SLOT:
7
+ * the shape a specifier issues. The contract that makes it trustworthy is that
8
+ * every row carries the IDENTICAL, fixed column set, and every attribute cell
9
+ * states its own verification alongside its value. An unknown is an explicit
10
+ * `missing` cell with a null value — never a blank that reads as complete.
11
+ */
12
+ /** One schedule column: the attribute and its header text. */
13
+ export interface FinishScheduleColumn {
14
+ attributeId: string;
15
+ /** The registry presentation label. */
16
+ label: string;
17
+ }
18
+ /**
19
+ * The six verification states a schedule cell can report, mirroring the shared
20
+ * verdict contract. On this public surface a `proposed` (pending-review) value
21
+ * is projected to `missing`, so it can never be told apart from a value that
22
+ * never existed.
23
+ */
24
+ export type FinishScheduleCellState = "matched" | "deviated" | "failed" | "unverified" | "missing" | "not_evaluated";
25
+ /**
26
+ * One attribute cell of one row: the value AND how far it can be trusted.
27
+ *
28
+ * Columns the Run's criteria evaluated carry the real verdict state. Columns
29
+ * backfilled from the catalogue (nothing in the brief asked about them) only
30
+ * ever read `matched` or `missing`: they assert presence-with-trust, not that
31
+ * the brief was met.
32
+ */
33
+ export interface FinishScheduleCell {
34
+ attributeId: string;
35
+ label: string;
36
+ /** The display value, or null when unknown. */
37
+ value: string | null;
38
+ state: FinishScheduleCellState;
39
+ confidence: number | null;
40
+ sourceBasis: string | null;
41
+ }
42
+ /** The rolled-up grade for one row, from its evaluated criterion cells only. */
43
+ export type FinishScheduleGrade = "meets_all" | "close" | "hard_no";
44
+ /** One row: one slot, one specified material, fully graded. */
45
+ export interface FinishScheduleRow {
46
+ slotId: string;
47
+ slotName: string;
48
+ /** The space NAME, or null for a spaceless slot. */
49
+ space: string | null;
50
+ element: string | null;
51
+ /** True when the designer pinned this pick rather than the engine choosing it. */
52
+ anchored: boolean;
53
+ brand: string;
54
+ product: string;
55
+ variant: string;
56
+ variantId: string;
57
+ /** Relative path to the public material record (`/materials/{variantId}`). */
58
+ recordUrl: string;
59
+ imageUrl: string | null;
60
+ hex: string | null;
61
+ /** Always the full, identically-ordered column set. */
62
+ attributes: FinishScheduleCell[];
63
+ fitNotes: string[];
64
+ grade: FinishScheduleGrade;
65
+ }
66
+ /** A whole composed scheme as an issuable finish schedule. */
67
+ export interface FinishSchedule {
68
+ runId: string;
69
+ schemeId: string;
70
+ title: string;
71
+ /** The compose contract version stamped on the Run, or `"unknown"`. */
72
+ composeVersion: string;
73
+ /** ISO timestamp the export was compiled at. */
74
+ generatedAt: string;
75
+ /** The stable column order every row's `attributes` array follows. */
76
+ columns: FinishScheduleColumn[];
77
+ rows: FinishScheduleRow[];
78
+ }
79
+ /** `GET /api/v1/runs/{runId}/schedule`. */
80
+ export interface FinishScheduleResponse {
81
+ schedule: FinishSchedule;
82
+ }
83
+ /** Options for {@link getRunSchedule}. */
84
+ export interface GetRunScheduleOptions {
85
+ /** Which composed scheme to export; defaults to the Run's first scheme. */
86
+ schemeId?: string;
87
+ signal?: AbortSignal;
88
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Finish-schedule wire types (MG-616) — the hand-mirror of
3
+ * `@materialgraph/products/schemes/finish-schedule`. Kept in their own module
4
+ * rather than `types.ts`, which sits at its source-size baseline.
5
+ *
6
+ * A finish schedule is a completed compose scheme rendered as ONE ROW PER SLOT:
7
+ * the shape a specifier issues. The contract that makes it trustworthy is that
8
+ * every row carries the IDENTICAL, fixed column set, and every attribute cell
9
+ * states its own verification alongside its value. An unknown is an explicit
10
+ * `missing` cell with a null value — never a blank that reads as complete.
11
+ */
12
+ export {};
@@ -1,12 +1,37 @@
1
1
  /** Types for structured refinements returned by MaterialGraph search. */
2
2
  import type { SearchInput } from "./types.js";
3
- /** A hard canonical-attribute filter accepted by the search engine. */
3
+ /**
4
+ * A hard canonical-attribute filter accepted by the search engine — either a
5
+ * term match (`{ attributeId, term }`) or a presence check
6
+ * (`{ attributeId, hasValue: true }`). Applied as a restriction BEFORE ranking.
7
+ *
8
+ * The `attributeId` may be a real registry id (`"fire_classifications"`,
9
+ * `"environmental_product_declaration"`) OR the virtual
10
+ * `"certification_programme"` (see `CERTIFICATION_PROGRAMME_ATTRIBUTE_ID`),
11
+ * which matches a programme — GREENGUARD, FloorScore, LEED, an EPD — wherever it
12
+ * landed across the four certification buckets. Presence (`hasValue`) is NOT
13
+ * supported for the virtual programme attribute. The typed `spec-types` mirror
14
+ * and the `specifications` builders produce values assignable to this type.
15
+ */
16
+ /**
17
+ * Optional per-filter TRUST bar the matching value must clear. `minConfidence` is
18
+ * an explicit confidence floor; `sourceBasis` demands an EXACT source basis (closed
19
+ * enum — an unknown basis is rejected); `meetsRequirement` folds the attribute's
20
+ * registry confidence requirement server-side.
21
+ */
22
+ export interface SearchFilterTrust {
23
+ minConfidence?: number;
24
+ sourceBasis?: "catalog_feed" | "declared" | "derived" | "tested";
25
+ meetsRequirement?: true;
26
+ }
4
27
  export type SearchFilter = {
5
28
  attributeId: string;
6
29
  term: string;
30
+ trust?: SearchFilterTrust;
7
31
  } | {
8
32
  attributeId: string;
9
33
  hasValue: true;
34
+ trust?: SearchFilterTrust;
10
35
  };
11
36
  /** One structured refinement returned with a search response. */
12
37
  export interface SearchSuggestion {
@@ -0,0 +1,27 @@
1
+ import { type ValidationFailure } from "./validation.js";
2
+ /** `verdictStateSchema` — the six live cell states. */
3
+ export declare const VERDICT_STATES: readonly ["matched", "deviated", "failed", "unverified", "missing", "not_evaluated"];
4
+ /** `attributeVerdictSchema.strength`. */
5
+ export declare const VERDICT_STRENGTHS: readonly ["required", "preferred"];
6
+ /** `verdictBaselineSchema` discriminant — the four arms of what a cell was judged against. */
7
+ export declare const VERDICT_BASELINE_KINDS: readonly ["term", "presence", "seed", "image"];
8
+ /** The `image` baseline arm's own `source` enum. */
9
+ export declare const VERDICT_IMAGE_SOURCES: readonly ["variant", "vector"];
10
+ /** `verdictDeviationSchema.direction`. */
11
+ export declare const VERDICT_DEVIATION_DIRECTIONS: readonly ["above", "below", "other"];
12
+ /** `verdictTrustGapSchema` — why a real value did not clear the trust bar. */
13
+ export declare const VERDICT_TRUST_GAPS: readonly ["below_confidence", "basis_unmet", "proposed"];
14
+ /** `verdictGradeSchema` — the rolled-up card grade. */
15
+ export declare const VERDICT_GRADES: readonly ["meets_all", "close", "hard_no"];
16
+ /** `visualMeaningSchema.basis` — where a meaning came from. */
17
+ export declare const VISUAL_MEANING_BASES: readonly ["canonical_attribute", "image_colour_analysis"];
18
+ /**
19
+ * Prove one wire value is a {@link SearchResultItem}, raising through `fail` at
20
+ * the first field that does not match `searchResultItemSchema`. Checks the shape
21
+ * only — unknown keys ride through untouched, per the SDK's pass-through
22
+ * contract (nothing on the emit path parses, so stripping would invent a
23
+ * normalisation the wire never had).
24
+ */
25
+ export declare function checkSearchResultItem(value: unknown, what: string, fail: ValidationFailure): void;
26
+ /** Prove every entry of a `results[]` array is a {@link SearchResultItem}. */
27
+ export declare function checkSearchResultItems(items: readonly unknown[], what: string, fail: ValidationFailure): void;
@@ -0,0 +1,174 @@
1
+ import { requireArray, requireBoolean, requireEnum, requireNonEmptyString, requireNonEmptyStringArray, requireNullableBoundedNumber, requireNullableEnum, requireNullableNonEmptyString, requireNullableNumber, requireNullableString, requireNumber, requireOptionalBoolean, requireOptionalNumber, requireOptionalString, requireRecord, requireString, requireIntegerAtLeast, } from "./validation.js";
2
+ // ── Closed enum members (canonical `z.enum` mirrors) ─────────────────────────
3
+ /** `verdictStateSchema` — the six live cell states. */
4
+ export const VERDICT_STATES = [
5
+ "matched",
6
+ "deviated",
7
+ "failed",
8
+ "unverified",
9
+ "missing",
10
+ "not_evaluated",
11
+ ];
12
+ /** `attributeVerdictSchema.strength`. */
13
+ export const VERDICT_STRENGTHS = ["required", "preferred"];
14
+ /** `verdictBaselineSchema` discriminant — the four arms of what a cell was judged against. */
15
+ export const VERDICT_BASELINE_KINDS = ["term", "presence", "seed", "image"];
16
+ /** The `image` baseline arm's own `source` enum. */
17
+ export const VERDICT_IMAGE_SOURCES = ["variant", "vector"];
18
+ /** `verdictDeviationSchema.direction`. */
19
+ export const VERDICT_DEVIATION_DIRECTIONS = ["above", "below", "other"];
20
+ /** `verdictTrustGapSchema` — why a real value did not clear the trust bar. */
21
+ export const VERDICT_TRUST_GAPS = ["below_confidence", "basis_unmet", "proposed"];
22
+ /** `verdictGradeSchema` — the rolled-up card grade. */
23
+ export const VERDICT_GRADES = ["meets_all", "close", "hard_no"];
24
+ /** `visualMeaningSchema.basis` — where a meaning came from. */
25
+ export const VISUAL_MEANING_BASES = ["canonical_attribute", "image_colour_analysis"];
26
+ // ── Leaf checks ──────────────────────────────────────────────────────────────
27
+ /** `palette[]` — `{ hex, proportion, name: string.nullable().optional() }`. */
28
+ function checkPaletteSwatch(value, what, fail) {
29
+ const record = requireRecord(value, what, fail);
30
+ requireString(record.hex, `${what}.hex`, fail);
31
+ requireNumber(record.proportion, `${what}.proportion`, fail);
32
+ // `.nullable().optional()` — absent OR null OR a string.
33
+ if (record.name !== undefined)
34
+ requireNullableString(record.name, `${what}.name`, fail);
35
+ }
36
+ /** `colourMeta` — three independently optional strings. */
37
+ function checkColourMeta(value, what, fail) {
38
+ const record = requireRecord(value, what, fail);
39
+ requireOptionalString(record.mood, `${what}.mood`, fail);
40
+ requireOptionalString(record.dominantFamily, `${what}.dominantFamily`, fail);
41
+ requireOptionalString(record.dominantHex, `${what}.dominantHex`, fail);
42
+ }
43
+ /** `searchResultSignalsSchema` — five nullable numbers, all required keys. */
44
+ function checkSignals(value, what, fail) {
45
+ const record = requireRecord(value, what, fail);
46
+ for (const signal of ["image", "category", "colour", "lexical", "text"]) {
47
+ requireNullableNumber(record[signal], `${what}.${signal}`, fail);
48
+ }
49
+ }
50
+ /** `visualMeaningSchema` — note the NON-EMPTY `values` array and the closed `basis`. */
51
+ function checkVisualMeaning(value, what, fail) {
52
+ const record = requireRecord(value, what, fail);
53
+ requireString(record.id, `${what}.id`, fail);
54
+ requireString(record.label, `${what}.label`, fail);
55
+ requireNonEmptyStringArray(record.values, `${what}.values`, fail);
56
+ requireEnum(record.basis, VISUAL_MEANING_BASES, `${what}.basis`, fail);
57
+ requireOptionalNumber(record.confidence, `${what}.confidence`, fail);
58
+ requireOptionalString(record.sourceBasis, `${what}.sourceBasis`, fail);
59
+ requireOptionalString(record.version, `${what}.version`, fail);
60
+ requireOptionalString(record.appliedAt, `${what}.appliedAt`, fail);
61
+ }
62
+ // ── Verdict subtree ──────────────────────────────────────────────────────────
63
+ /**
64
+ * `verdictBaselineSchema` — a discriminated union on `kind`. An unknown
65
+ * discriminant is rejected, exactly as `z.discriminatedUnion` rejects it, and
66
+ * each arm carries only the fields it needs.
67
+ */
68
+ function checkVerdictBaseline(value, what, fail) {
69
+ const record = requireRecord(value, what, fail);
70
+ const kind = requireEnum(record.kind, VERDICT_BASELINE_KINDS, `${what}.kind`, fail);
71
+ switch (kind) {
72
+ case "term":
73
+ requireNonEmptyString(record.term, `${what}.term`, fail);
74
+ return;
75
+ case "presence":
76
+ return;
77
+ case "seed":
78
+ requireNonEmptyString(record.variantId, `${what}.variantId`, fail);
79
+ return;
80
+ default:
81
+ // `image` — the like-this-picture arm.
82
+ requireEnum(record.source, VERDICT_IMAGE_SOURCES, `${what}.source`, fail);
83
+ requireNullableNonEmptyString(record.variantId, `${what}.variantId`, fail);
84
+ }
85
+ }
86
+ /** `verdictDeviationSchema` — a named near-miss. */
87
+ function checkVerdictDeviation(value, what, fail) {
88
+ const record = requireRecord(value, what, fail);
89
+ requireEnum(record.direction, VERDICT_DEVIATION_DIRECTIONS, `${what}.direction`, fail);
90
+ requireNullableNumber(record.delta, `${what}.delta`, fail);
91
+ requireNullableString(record.unit, `${what}.unit`, fail);
92
+ requireNonEmptyString(record.label, `${what}.label`, fail);
93
+ }
94
+ /**
95
+ * `attributeVerdictSchema` — nine fields plus the TWO cross-field invariants its
96
+ * `.superRefine()` enforces. Those are relational rather than field-wise, which
97
+ * is no obstacle to a hand-rolled validator: they are two conditionals, and the
98
+ * schema's own doc calls them out as the reason a malformed cell can never reach
99
+ * a consumer.
100
+ */
101
+ function checkAttributeVerdict(value, what, fail) {
102
+ const record = requireRecord(value, what, fail);
103
+ requireNonEmptyString(record.attributeId, `${what}.attributeId`, fail);
104
+ const state = requireEnum(record.state, VERDICT_STATES, `${what}.state`, fail);
105
+ requireEnum(record.strength, VERDICT_STRENGTHS, `${what}.strength`, fail);
106
+ checkVerdictBaseline(record.baseline, `${what}.baseline`, fail);
107
+ requireNullableString(record.value, `${what}.value`, fail);
108
+ requireNullableBoundedNumber(record.confidence, `${what}.confidence`, fail, 0, 1);
109
+ requireNullableString(record.sourceBasis, `${what}.sourceBasis`, fail);
110
+ const deviation = record.deviation;
111
+ if (deviation !== null)
112
+ checkVerdictDeviation(deviation, `${what}.deviation`, fail);
113
+ const trustGap = requireNullableEnum(record.trustGap, VERDICT_TRUST_GAPS, `${what}.trustGap`, fail);
114
+ // superRefine invariant 1 — a deviation only describes a `deviated` cell.
115
+ if (deviation !== null && state !== "deviated") {
116
+ throw fail(`${what}.deviation is set on a cell whose state is "${state}", not "deviated"`);
117
+ }
118
+ // superRefine invariant 2 — a trust gap only explains an `unverified` cell.
119
+ if (trustGap !== null && state !== "unverified") {
120
+ throw fail(`${what}.trustGap is set on a cell whose state is "${state}", not "unverified"`);
121
+ }
122
+ }
123
+ /** `candidateVerdictSchema` — the whole report card. */
124
+ function checkCandidateVerdict(value, what, fail) {
125
+ const record = requireRecord(value, what, fail);
126
+ const cells = requireArray(record.cells, `${what}.cells`, fail);
127
+ cells.forEach((cell, index) => checkAttributeVerdict(cell, `${what}.cells[${index}]`, fail));
128
+ requireEnum(record.grade, VERDICT_GRADES, `${what}.grade`, fail);
129
+ requireNullableBoundedNumber(record.minConfidence, `${what}.minConfidence`, fail, 0, 1);
130
+ }
131
+ // ── The item ─────────────────────────────────────────────────────────────────
132
+ /**
133
+ * Prove one wire value is a {@link SearchResultItem}, raising through `fail` at
134
+ * the first field that does not match `searchResultItemSchema`. Checks the shape
135
+ * only — unknown keys ride through untouched, per the SDK's pass-through
136
+ * contract (nothing on the emit path parses, so stripping would invent a
137
+ * normalisation the wire never had).
138
+ */
139
+ export function checkSearchResultItem(value, what, fail) {
140
+ const record = requireRecord(value, what, fail);
141
+ requireString(record.variantId, `${what}.variantId`, fail);
142
+ requireString(record.brand, `${what}.brand`, fail);
143
+ requireString(record.product, `${what}.product`, fail);
144
+ requireNullableString(record.brandSlug, `${what}.brandSlug`, fail);
145
+ requireNullableString(record.productId, `${what}.productId`, fail);
146
+ requireNullableString(record.color, `${what}.color`, fail);
147
+ requireNullableString(record.variantName, `${what}.variantName`, fail);
148
+ requireNullableString(record.imageUrl, `${what}.imageUrl`, fail);
149
+ requireBoolean(record.hasImage, `${what}.hasImage`, fail);
150
+ requireNumber(record.score, `${what}.score`, fail);
151
+ const palette = requireArray(record.palette, `${what}.palette`, fail);
152
+ palette.forEach((swatch, index) => checkPaletteSwatch(swatch, `${what}.palette[${index}]`, fail));
153
+ if (record.colourMeta !== undefined) {
154
+ checkColourMeta(record.colourMeta, `${what}.colourMeta`, fail);
155
+ }
156
+ if (record.signals !== undefined)
157
+ checkSignals(record.signals, `${what}.signals`, fail);
158
+ requireOptionalBoolean(record.retired, `${what}.retired`, fail);
159
+ requireOptionalBoolean(record.belowFloor, `${what}.belowFloor`, fail);
160
+ if (record.siblingCount !== undefined) {
161
+ requireIntegerAtLeast(record.siblingCount, `${what}.siblingCount`, fail, 1);
162
+ }
163
+ if (record.visualMeanings !== undefined) {
164
+ const meanings = requireArray(record.visualMeanings, `${what}.visualMeanings`, fail);
165
+ meanings.forEach((meaning, index) => checkVisualMeaning(meaning, `${what}.visualMeanings[${index}]`, fail));
166
+ }
167
+ if (record.verdict !== undefined) {
168
+ checkCandidateVerdict(record.verdict, `${what}.verdict`, fail);
169
+ }
170
+ }
171
+ /** Prove every entry of a `results[]` array is a {@link SearchResultItem}. */
172
+ export function checkSearchResultItems(items, what, fail) {
173
+ items.forEach((item, index) => checkSearchResultItem(item, `${what}[${index}]`, fail));
174
+ }
@@ -0,0 +1,3 @@
1
+ import type { SourceMaterialBatchDetail } from "./run-types.js";
2
+ /** Validate a batch-detail body, returning it typed, or throw explaining why. */
3
+ export declare function checkSourceMaterialBatchDetail(value: unknown, batchId: string): SourceMaterialBatchDetail;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * The runtime validator for `GET /api/v1/source-material/{batchId}` — the
3
+ * hand-rolled, zero-runtime-dependency mirror of
4
+ * `sourceMaterialBatchDetailSchema` and its transitive closure
5
+ * (`sourceMaterialBatchDetailFileSchema`, `sourceImageAnalysisSchema`,
6
+ * `sourceImageAnalysisResultSchema`) in
7
+ * `packages/sources/src/source-material/contract.ts`.
8
+ *
9
+ * WHY IT EXISTS. `contract-parity.test.ts` proves the SDK's STATIC types match
10
+ * the canonical schemas; that is a compile-time proof about shapes and validates
11
+ * nothing at run time. This is the run-time half, and it must ACCEPT and REJECT
12
+ * exactly what the schema does — pinned by `source-material-parity.test.ts`.
13
+ *
14
+ * TWO CONTRACT PROPERTIES IT ENFORCES, both easy to lose in a refactor:
15
+ * 1. `analyses` is REQUIRED and an array on every file. `[]` means "nothing
16
+ * computed yet"; an absent field is a broken response, not an empty one.
17
+ * Preprocessing is manual today and automatic later — only the trigger
18
+ * changes, never this contract.
19
+ * 2. `result` is a JSON record all the way down (`z.json()`), not `unknown`.
20
+ * See `requireJsonValue` for why the schema is that strict and why this
21
+ * mirror cannot actually fire over the wire.
22
+ *
23
+ * ENUMS ARE CLOSED here, matching the canonical `z.enum(…)` members, while the
24
+ * published types are `OpenEnum` so a newer server value keeps type-checking.
25
+ * The validator follows the SCHEMA: the type stays wider than what is proven,
26
+ * which is imprecise but never a lie — the same choice
27
+ * `search-result-validation.ts` makes for `basis`.
28
+ *
29
+ * Package-internal (not re-exported from `index.ts`): a published symbol is a
30
+ * permanent contract, and promoting it later is additive.
31
+ */
32
+ import { MaterialGraphError } from "./errors.js";
33
+ import { requireArray, requireEnum, requireSafeInteger, requireJsonRecord, requireNonEmptyString, requireRecord, requireString, } from "./validation.js";
34
+ /** `sourceImageAnalysisKindSchema` members, verbatim. */
35
+ const ANALYSIS_KINDS = ["scene_match"];
36
+ /** `sourceImageAnalysisStatusSchema` members, verbatim. */
37
+ const ANALYSIS_STATUSES = ["ok", "failed"];
38
+ /** `sourceMaterialRoleSchema` members, verbatim. */
39
+ const SOURCE_ROLES = [
40
+ "brief",
41
+ "reference_image",
42
+ "document",
43
+ "document_image",
44
+ "inspiration_image",
45
+ "unclassified",
46
+ ];
47
+ function detailFailure(batchId) {
48
+ return (reason) => MaterialGraphError.unexpected(`GET /api/v1/source-material/${batchId} returned a malformed body: ${reason}`);
49
+ }
50
+ function checkAnalysis(value, what, fail) {
51
+ const record = requireRecord(value, what, fail);
52
+ // `model` and `analysedAt` ARE `.min(1)` canonically — fields this endpoint
53
+ // introduces, where an empty value is meaningless and `model` already cannot
54
+ // be written empty. Parity here is achieved by the schema being strict, never
55
+ // by this mirror freelancing past it.
56
+ return {
57
+ kind: requireEnum(record.kind, ANALYSIS_KINDS, `${what}.kind`, fail),
58
+ model: requireNonEmptyString(record.model, `${what}.model`, fail),
59
+ status: requireEnum(record.status, ANALYSIS_STATUSES, `${what}.status`, fail),
60
+ result: requireJsonRecord(record.result, `${what}.result`, fail),
61
+ analysedAt: requireNonEmptyString(record.analysedAt, `${what}.analysedAt`, fail),
62
+ };
63
+ }
64
+ function checkFile(value, what, fail) {
65
+ const record = requireRecord(value, what, fail);
66
+ // NOT `requireDefaultedArray`: the schema has no `.default([])`, so an absent
67
+ // `analyses` is a malformed body rather than an empty one. Keeping that
68
+ // distinction is the whole point of the always-an-array contract.
69
+ const analyses = requireArray(record.analyses, `${what}.analyses`, fail);
70
+ // `requireString`, NOT `requireNonEmptyString`, for the fields this endpoint
71
+ // INHERITS from the shared source-material contract (`sourceId`, `sha256`,
72
+ // `url` — plus `batchId`/`createdAt` below). Those are plain `z.string()`
73
+ // there and shared with the upload path; a client that rejected `""` would
74
+ // refuse a response the server can legitimately produce, and tightening them
75
+ // is a change to a contract this endpoint does not own.
76
+ return {
77
+ sourceId: requireString(record.sourceId, `${what}.sourceId`, fail),
78
+ role: requireEnum(record.role, SOURCE_ROLES, `${what}.role`, fail),
79
+ filename: requireString(record.filename, `${what}.filename`, fail),
80
+ mediaType: requireString(record.mediaType, `${what}.mediaType`, fail),
81
+ sha256: requireString(record.sha256, `${what}.sha256`, fail),
82
+ // `requireSafeInteger`, NOT `requireIntegerAtLeast(…, 0)`. Canonically these
83
+ // are a bare `z.number().int()` on the INHERITED
84
+ // `sourceMaterialFileReceiptSchema`, which carries no lower bound — so a
85
+ // negative is accepted there and must be accepted here.
86
+ //
87
+ // A negative `bytes` is nonsense and a negative `pages` more so, and this is
88
+ // still not the place to say so: a client validator holding an opinion the
89
+ // contract does not is how the two drift, and how a response the server may
90
+ // legitimately produce starts throwing at the boundary. Tightening those
91
+ // fields is a canonical change to a schema shared with the upload path, and
92
+ // belongs in its own ticket with this mirror following after.
93
+ bytes: requireSafeInteger(record.bytes, `${what}.bytes`, fail),
94
+ pages: record.pages === null ? null : requireSafeInteger(record.pages, `${what}.pages`, fail),
95
+ url: requireString(record.url, `${what}.url`, fail),
96
+ analyses: analyses.map((analysis, index) => checkAnalysis(analysis, `${what}.analyses[${index}]`, fail)),
97
+ };
98
+ }
99
+ /** Validate a batch-detail body, returning it typed, or throw explaining why. */
100
+ export function checkSourceMaterialBatchDetail(value, batchId) {
101
+ const fail = detailFailure(batchId);
102
+ const record = requireRecord(value, "body", fail);
103
+ const sources = requireArray(record.sources, "body.sources", fail);
104
+ return {
105
+ batchId: requireString(record.batchId, "body.batchId", fail),
106
+ createdAt: requireString(record.createdAt, "body.createdAt", fail),
107
+ sources: sources.map((file, index) => checkFile(file, `body.sources[${index}]`, fail)),
108
+ };
109
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The specification surface — the typed vocabulary for asking a material
3
+ * "does it meet this spec?": certifications (GREENGUARD, FloorScore, Declare),
4
+ * VOC / emissions testing (CDPH v1.2), sustainability programmes (LEED, EPDs),
5
+ * and fire / abrasion / slip classifications.
6
+ *
7
+ * Every shape here is a hand-authored, zero-dependency mirror of the server's
8
+ * spec filter contract (`specFilterSchema` in
9
+ * `@materialgraph/products/search/schema`). It exists so a caller answering a
10
+ * question like "find me a GREENGUARD-certified alternative" or "the same fire
11
+ * class, but with an EPD on record" reaches for a NAMED, typed criterion instead
12
+ * of hand-assembling a magic `attribute:value` string.
13
+ *
14
+ * These criteria drive two operations equally:
15
+ * - `client.search({ filters: [...] })` — spec gates layered onto a ranked
16
+ * (colour / visual / lexical) search.
17
+ * - `client.bySpec(...)` — the value-anchored, spec-only primitive, which takes
18
+ * the same criteria as `attribute:value` strings (build them with the helpers
19
+ * in `./specifications`).
20
+ */
21
+ /**
22
+ * The virtual attribute id that matches a certification PROGRAMME wherever it
23
+ * landed at ingest.
24
+ *
25
+ * Certifications are not stored under one attribute: each record is an object
26
+ * (`{ programme, name, status, … }`) split across FOUR real object-array
27
+ * attributes by intent — general `certifications`,
28
+ * `sustainability_certifications`, `material_health_certifications`, and
29
+ * `voc_emissions_testing_certificate` (GREENGUARD Gold / FloorScore / CDPH land
30
+ * in the last). A caller shouldn't have to know which bucket a programme fell
31
+ * into, so this single virtual id matches the `programme` term across all four.
32
+ *
33
+ * The `term` may be a display name or alias ("GREENGUARD Gold", "LEED"), resolved
34
+ * server-side to its canonical programme token, or a canonical token directly
35
+ * (e.g. `greenguard_certificate`, `has_greenguard_gold_certification`).
36
+ *
37
+ * @example
38
+ * // "find a GREENGUARD-certified alternative"
39
+ * client.search({
40
+ * similarTo: { variantId: seed },
41
+ * filters: [{ attributeId: CERTIFICATION_PROGRAMME_ATTRIBUTE_ID, term: "GREENGUARD Gold" }],
42
+ * });
43
+ */
44
+ export declare const CERTIFICATION_PROGRAMME_ATTRIBUTE_ID = "certification_programme";
45
+ /** The literal type of {@link CERTIFICATION_PROGRAMME_ATTRIBUTE_ID}. */
46
+ export type CertificationProgrammeAttributeId = typeof CERTIFICATION_PROGRAMME_ATTRIBUTE_ID;
47
+ /**
48
+ * A term-equality spec criterion: the attribute must carry this canonical value,
49
+ * e.g. `{ attributeId: "fire_classifications", term: "astm_e84_class_a" }`.
50
+ * Discover valid `attributeId`/`term` pairs via `client.specificationFacets()`
51
+ * and `client.attributeValues()`, or resolve free text with
52
+ * `client.resolveAttribute()`.
53
+ */
54
+ export interface SpecTermFilter {
55
+ attributeId: string;
56
+ term: string;
57
+ }
58
+ /**
59
+ * A presence spec criterion: the attribute must have ANY accepted value —
60
+ * "has an EPD on record", "has a measured slip figure at all". Not supported for
61
+ * {@link CERTIFICATION_PROGRAMME_ATTRIBUTE_ID} (a programme is matched by name,
62
+ * never by bare presence).
63
+ */
64
+ export interface SpecPresenceFilter {
65
+ attributeId: string;
66
+ hasValue: true;
67
+ }
68
+ /**
69
+ * A certification-programme criterion — a {@link SpecTermFilter} pinned to the
70
+ * virtual {@link CERTIFICATION_PROGRAMME_ATTRIBUTE_ID}. Structurally assignable
71
+ * to {@link SpecFilter}, so it drops straight into `search({ filters })`.
72
+ */
73
+ export interface CertificationProgrammeFilter {
74
+ attributeId: CertificationProgrammeAttributeId;
75
+ term: string;
76
+ }
77
+ /**
78
+ * A hard, canonical-attribute spec criterion — either a term match or a
79
+ * presence check. The union the search engine applies as a restriction BEFORE
80
+ * ranking. Identical in shape to the SDK's `SearchFilter`.
81
+ */
82
+ export type SpecFilter = SpecTermFilter | SpecPresenceFilter;
package/spec-types.js ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The specification surface — the typed vocabulary for asking a material
3
+ * "does it meet this spec?": certifications (GREENGUARD, FloorScore, Declare),
4
+ * VOC / emissions testing (CDPH v1.2), sustainability programmes (LEED, EPDs),
5
+ * and fire / abrasion / slip classifications.
6
+ *
7
+ * Every shape here is a hand-authored, zero-dependency mirror of the server's
8
+ * spec filter contract (`specFilterSchema` in
9
+ * `@materialgraph/products/search/schema`). It exists so a caller answering a
10
+ * question like "find me a GREENGUARD-certified alternative" or "the same fire
11
+ * class, but with an EPD on record" reaches for a NAMED, typed criterion instead
12
+ * of hand-assembling a magic `attribute:value` string.
13
+ *
14
+ * These criteria drive two operations equally:
15
+ * - `client.search({ filters: [...] })` — spec gates layered onto a ranked
16
+ * (colour / visual / lexical) search.
17
+ * - `client.bySpec(...)` — the value-anchored, spec-only primitive, which takes
18
+ * the same criteria as `attribute:value` strings (build them with the helpers
19
+ * in `./specifications`).
20
+ */
21
+ /**
22
+ * The virtual attribute id that matches a certification PROGRAMME wherever it
23
+ * landed at ingest.
24
+ *
25
+ * Certifications are not stored under one attribute: each record is an object
26
+ * (`{ programme, name, status, … }`) split across FOUR real object-array
27
+ * attributes by intent — general `certifications`,
28
+ * `sustainability_certifications`, `material_health_certifications`, and
29
+ * `voc_emissions_testing_certificate` (GREENGUARD Gold / FloorScore / CDPH land
30
+ * in the last). A caller shouldn't have to know which bucket a programme fell
31
+ * into, so this single virtual id matches the `programme` term across all four.
32
+ *
33
+ * The `term` may be a display name or alias ("GREENGUARD Gold", "LEED"), resolved
34
+ * server-side to its canonical programme token, or a canonical token directly
35
+ * (e.g. `greenguard_certificate`, `has_greenguard_gold_certification`).
36
+ *
37
+ * @example
38
+ * // "find a GREENGUARD-certified alternative"
39
+ * client.search({
40
+ * similarTo: { variantId: seed },
41
+ * filters: [{ attributeId: CERTIFICATION_PROGRAMME_ATTRIBUTE_ID, term: "GREENGUARD Gold" }],
42
+ * });
43
+ */
44
+ export const CERTIFICATION_PROGRAMME_ATTRIBUTE_ID = "certification_programme";
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Zero-dependency builders for the specification surface. Turn a plain-language
3
+ * spec intent into a typed criterion the SDK's two spec operations accept:
4
+ *
5
+ * - `search({ filters })` takes {@link SpecFilter} objects — a spec gate layered
6
+ * onto a ranked (colour / visual / lexical) search. Build them with
7
+ * {@link specTerm}, {@link hasSpec}, and {@link certificationProgramme}.
8
+ * - `bySpec(specs)` takes `attribute:value` strings — the value-anchored,
9
+ * spec-only primitive. Build them with {@link specCriterion} and
10
+ * {@link certificationProgrammeCriterion}.
11
+ *
12
+ * The point of these helpers: Desk (and any spec-driven caller) never has to
13
+ * remember that a certification programme lives under a VIRTUAL attribute, nor
14
+ * hand-format the `attribute:value` string the `bySpec` route splits on its
15
+ * first `:`.
16
+ */
17
+ import { type CertificationProgrammeFilter, type SpecPresenceFilter, type SpecTermFilter } from "./spec-types.js";
18
+ /**
19
+ * A term-equality criterion for `search({ filters })` — the attribute must
20
+ * carry this canonical value.
21
+ *
22
+ * @example
23
+ * // "same fire class as the seed"
24
+ * client.search({ similarTo: { variantId }, filters: [specTerm("fire_classifications", "astm_e84_class_a")] });
25
+ */
26
+ export declare function specTerm(attributeId: string, term: string): SpecTermFilter;
27
+ /**
28
+ * A presence criterion for `search({ filters })` — the attribute must have ANY
29
+ * accepted value.
30
+ *
31
+ * @example
32
+ * // "…and it must have an EPD on record"
33
+ * client.search({ similarTo: { variantId }, filters: [hasSpec("environmental_product_declaration")] });
34
+ */
35
+ export declare function hasSpec(attributeId: string): SpecPresenceFilter;
36
+ /**
37
+ * A certification-programme criterion for `search({ filters })`, pinned to the
38
+ * virtual attribute so a programme matches wherever it landed at ingest
39
+ * (compliance / sustainability / material-health / VOC buckets).
40
+ *
41
+ * The value may be a display name or alias ("GREENGUARD Gold", "LEED") — the
42
+ * server resolves it to its canonical programme token before matching — or a
43
+ * canonical token directly (e.g. `greenguard_certificate`,
44
+ * `has_greenguard_gold_certification`).
45
+ *
46
+ * @example
47
+ * // "find a GREENGUARD-certified alternative"
48
+ * client.search({ similarTo: { variantId: seed }, filters: [certificationProgramme("GREENGUARD Gold")] });
49
+ */
50
+ export declare function certificationProgramme(programme: string): CertificationProgrammeFilter;
51
+ /**
52
+ * An `attribute:value` criterion string for `bySpec`. The `bySpec` route splits
53
+ * on the FIRST `:`, so a value may itself contain colons; the attribute id may
54
+ * not.
55
+ *
56
+ * @example
57
+ * client.bySpec([specCriterion("primary_material", "textile"), specCriterion("fire_classifications", "astm_e84_class_a")]);
58
+ */
59
+ export declare function specCriterion(attributeId: string, value: string): string;
60
+ /**
61
+ * An `attribute:value` criterion string for `bySpec` that filters by
62
+ * certification PROGRAMME across all four certification buckets.
63
+ *
64
+ * The value may be a display name or alias ("GREENGUARD Gold", "LEED"), resolved
65
+ * server-side to its canonical programme token, or a canonical token directly
66
+ * (e.g. `greenguard_certificate`, `has_greenguard_gold_certification`).
67
+ *
68
+ * @example
69
+ * // every brand's GREENGUARD-Gold materials
70
+ * client.bySpec([certificationProgrammeCriterion("GREENGUARD Gold")]);
71
+ */
72
+ export declare function certificationProgrammeCriterion(programme: string): string;