@instruments/taxonomy 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,6 +6,10 @@ The canonical materials taxonomy for the Material Instruments ecosystem: the gov
6
6
  is the single publishable home of the Material Semantics vocabularies that were previously vendored
7
7
  per-repo as drifting local copies.
8
8
 
9
+ It also publishes appearance concepts and a portable taste-assertion contract. Applications own
10
+ their evidence stores and ranking policy; this package owns shared meaning, source crosswalk
11
+ verdicts, and the envelope that lets evidence travel without turning a source label into fact.
12
+
9
13
  ## Install
10
14
 
11
15
  ```sh
@@ -51,6 +55,77 @@ miuFamilyToCanonical("greenery_planting"); // null (declines to classify)
51
55
  MIU_SURFACE_APPLICATION_TO_CANONICAL.railing; // "stair_railing"
52
56
  ```
53
57
 
58
+ ### Canonical bundles and match semantics
59
+
60
+ Consumers compose qualified assertions into canonical bundles rather than minting app-specific
61
+ compound tags. Every constitutive assertion is required; matched constitutive assertions score 2
62
+ and matched accents score 1. Unroled assertions default to accent, extra candidate annotations are
63
+ not penalised, and the package publishes residential and commercial test vectors so another
64
+ language can reproduce the ranking exactly.
65
+
66
+ ```ts
67
+ import { defineBundle, matchBundle } from "@instruments/taxonomy";
68
+
69
+ const quietKitchen = defineBundle([
70
+ { assertion: "space.kitchen", role: "constitutive" },
71
+ { assertion: "mood.calm" },
72
+ { assertion: "style.minimalism" },
73
+ ]);
74
+
75
+ matchBundle(quietKitchen, ["space.kitchen", "mood.calm"]);
76
+ // { eligible: true, score: 0.75, ... }
77
+ ```
78
+
79
+ `defineAlternatives` preserves a source term known to have one of several readings without
80
+ guessing. The DesignShop and materia `Modern` crosswalk rows now carry an explicit any-of between
81
+ `style.modernism` and `style.contemporary` instead of disappearing from downstream joins.
82
+
83
+ ### Appearance axes
84
+
85
+ Product category, specific material, pattern, finish, format and construction are independent
86
+ axes. That separation is deliberate: plain porcelain, geometric porcelain and stone-effect
87
+ porcelain share `material.porcelain` while carrying different pattern concepts. A commercial
88
+ carpet and residential wallcovering use the same pattern semantics; there is no “commercial
89
+ style” branch.
90
+
91
+ Requirement concepts name dimensions such as slip resistance, availability and fire performance.
92
+ The assertion envelope carries the operator and required value separately, because a taxonomy can
93
+ name the question but cannot assert that a particular product passes it.
94
+
95
+ ### `@instruments/taxonomy/crosswalks/designshop`
96
+
97
+ The frozen DesignShop vocabulary resolves through a versioned four-state crosswalk:
98
+
99
+ ```ts
100
+ import { canonicaliseDesignShop } from "@instruments/taxonomy/crosswalks/designshop";
101
+
102
+ canonicaliseDesignShop({
103
+ provenance: {
104
+ sourceRecordId: "scheme:123",
105
+ sourceSnapshotVersion: "designshop-estate-1",
106
+ sourceSystem: "designshop",
107
+ },
108
+ sourceField: "pattern",
109
+ sourceValue: "Solid",
110
+ });
111
+ // resolved -> pattern.plain
112
+ ```
113
+
114
+ `resolved`, `ambiguous`, `unresolved` and `declined` are different results. Unknown input remains
115
+ unresolved with its raw value intact; only a curated row can decline. `Modern` remains an ambiguous
116
+ choice between contemporary and modernism until another source or a person confirms one reading.
117
+ `DESIGNSHOP_APPEARANCE_TEST_VECTORS` is the executable consumer contract.
118
+
119
+ ### `@instruments/taxonomy/taste`
120
+
121
+ `PortableTasteAssertion` separates actor, taste subject and project; taste, context and
122
+ requirement roles; canonical and unresolved meaning; scope, authority, polarity, time and
123
+ provenance. It intentionally contains no boost, rank or serving weight. Those are application
124
+ policy, not portable meaning. Requirement assertions additionally carry a typed constraint.
125
+
126
+ `PORTABLE_ASSERTION_TEST_VECTORS` gives TypeScript and non-TypeScript consumers stable fixtures for
127
+ serialization and contract tests.
128
+
54
129
  ## Provenance and the re-sync ritual
55
130
 
56
131
  The dictionaries are lifted **verbatim** from materialgraph —
@@ -0,0 +1,151 @@
1
+ // src/match.ts
2
+ var ROLE_WEIGHT = {
3
+ accent: 1,
4
+ constitutive: 2
5
+ };
6
+ var MATCH_WEIGHTS = ROLE_WEIGHT;
7
+ var DEFAULT_BUNDLE_ROLE = "accent";
8
+ var checkedAssertion = (assertion) => {
9
+ const normalized = assertion.trim();
10
+ if (normalized.length === 0) {
11
+ throw new Error("a canonical assertion cannot be empty");
12
+ }
13
+ return normalized;
14
+ };
15
+ var defineBundle = (inputs) => {
16
+ if (inputs.length === 0) {
17
+ throw new Error("a canonical bundle must contain at least one assertion");
18
+ }
19
+ const byAssertion = /* @__PURE__ */ new Map();
20
+ for (const input of inputs) {
21
+ const assertion = checkedAssertion(input.assertion);
22
+ const role = input.role ?? DEFAULT_BUNDLE_ROLE;
23
+ const previous = byAssertion.get(assertion);
24
+ byAssertion.set(assertion, previous === "constitutive" ? previous : role);
25
+ }
26
+ const members = [...byAssertion].map(([assertion, role]) => ({ assertion, role })).toSorted((a, b) => a.assertion.localeCompare(b.assertion) || a.role.localeCompare(b.role));
27
+ const serialization = members.map(({ assertion, role }) => `${role}:${assertion}`).join("|");
28
+ return {
29
+ id: `bundle:${encodeURIComponent(serialization)}`,
30
+ kind: "all-of",
31
+ members,
32
+ serialization
33
+ };
34
+ };
35
+ var defineAlternatives = (inputs, reason) => {
36
+ const bySerialization = /* @__PURE__ */ new Map();
37
+ for (const input of inputs) {
38
+ const bundle = "kind" in input ? input : defineBundle(input);
39
+ bySerialization.set(bundle.serialization, bundle);
40
+ }
41
+ const alternatives = [...bySerialization.values()].toSorted(
42
+ (a, b) => a.serialization.localeCompare(b.serialization)
43
+ );
44
+ if (alternatives.length < 2) {
45
+ throw new Error("canonical alternatives must contain at least two distinct bundles");
46
+ }
47
+ const serialization = alternatives.map(({ id }) => id).join("||");
48
+ return {
49
+ alternatives,
50
+ id: `alternatives:${encodeURIComponent(serialization)}`,
51
+ kind: "any-of",
52
+ reason,
53
+ serialization
54
+ };
55
+ };
56
+ var matchBundle = (bundle, candidateAssertions) => {
57
+ const candidate = new Set(candidateAssertions);
58
+ const matched = bundle.members.filter(({ assertion }) => candidate.has(assertion));
59
+ const missing = bundle.members.filter(({ assertion }) => !candidate.has(assertion));
60
+ const missingConstitutive = missing.filter(({ role }) => role === "constitutive").map(({ assertion }) => assertion);
61
+ const missingAccents = missing.filter(({ role }) => role === "accent").map(({ assertion }) => assertion);
62
+ const eligible = missingConstitutive.length === 0;
63
+ const availableWeight = bundle.members.reduce((sum, { role }) => sum + ROLE_WEIGHT[role], 0);
64
+ const matchedWeight = matched.reduce((sum, { role }) => sum + ROLE_WEIGHT[role], 0);
65
+ return {
66
+ eligible,
67
+ matched: matched.map(({ assertion }) => assertion),
68
+ missingAccents,
69
+ missingConstitutive,
70
+ score: eligible ? matchedWeight / availableWeight : 0
71
+ };
72
+ };
73
+ var matchAlternatives = (alternatives, candidateAssertions) => {
74
+ const assertions = [...candidateAssertions];
75
+ const ranked = alternatives.alternatives.map((alternative) => ({ alternative, match: matchBundle(alternative, assertions) })).toSorted(
76
+ (a, b) => Number(b.match.eligible) - Number(a.match.eligible) || b.match.score - a.match.score || a.alternative.id.localeCompare(b.alternative.id)
77
+ );
78
+ const [strongest] = ranked;
79
+ if (strongest === void 0) {
80
+ throw new Error("canonical alternatives contain no bundles");
81
+ }
82
+ return strongest;
83
+ };
84
+ var COTTAGE = "evokes(typology.cottage)";
85
+ var COTSWOLDS = "evokes(place.region.cotswolds)";
86
+ var SHELTERING = "mood.sheltering";
87
+ var GREEN = "palette.hue.green";
88
+ var RESTAURANT = "space.restaurant";
89
+ var INDUSTRIAL = "style.industrial";
90
+ var MOODY = "mood.moody";
91
+ var COOL = "palette.temperature.cool";
92
+ var MATCH_TEST_VECTORS = [
93
+ {
94
+ candidates: [
95
+ {
96
+ assertions: [COTTAGE, COTSWOLDS, SHELTERING, GREEN],
97
+ id: "residential-exact"
98
+ },
99
+ {
100
+ assertions: [COTTAGE, SHELTERING, GREEN],
101
+ id: "residential-near"
102
+ },
103
+ {
104
+ assertions: [COTSWOLDS, SHELTERING, GREEN],
105
+ id: "residential-wrong-building"
106
+ }
107
+ ],
108
+ expectedEligibleOrder: ["residential-exact", "residential-near"],
109
+ id: "cotswolds-green-cosy-cottage",
110
+ query: defineBundle([
111
+ { assertion: COTTAGE, role: "constitutive" },
112
+ { assertion: COTSWOLDS },
113
+ { assertion: SHELTERING },
114
+ { assertion: GREEN }
115
+ ])
116
+ },
117
+ {
118
+ candidates: [
119
+ {
120
+ assertions: [RESTAURANT, INDUSTRIAL, MOODY, COOL],
121
+ id: "commercial-exact"
122
+ },
123
+ {
124
+ assertions: [RESTAURANT, INDUSTRIAL, MOODY],
125
+ id: "commercial-near"
126
+ },
127
+ {
128
+ assertions: ["typology.restaurant", INDUSTRIAL, MOODY],
129
+ id: "commercial-wrong-scale"
130
+ }
131
+ ],
132
+ expectedEligibleOrder: ["commercial-exact", "commercial-near"],
133
+ id: "moody-industrial-restaurant",
134
+ query: defineBundle([
135
+ { assertion: RESTAURANT, role: "constitutive" },
136
+ { assertion: INDUSTRIAL, role: "constitutive" },
137
+ { assertion: MOODY },
138
+ { assertion: COOL }
139
+ ])
140
+ }
141
+ ];
142
+
143
+ export {
144
+ MATCH_WEIGHTS,
145
+ DEFAULT_BUNDLE_ROLE,
146
+ defineBundle,
147
+ defineAlternatives,
148
+ matchBundle,
149
+ matchAlternatives,
150
+ MATCH_TEST_VECTORS
151
+ };
@@ -0,0 +1,23 @@
1
+ // src/canonicalisation.ts
2
+ var assertionMeaningFrom = (verdict) => {
3
+ const source = {
4
+ crosswalkVersion: verdict.crosswalkVersion,
5
+ sourceField: verdict.sourceField,
6
+ sourceValue: verdict.sourceValue,
7
+ taxonomySnapshotVersion: verdict.taxonomySnapshotVersion
8
+ };
9
+ if (verdict.status === "resolved") {
10
+ return { ...source, canonical: verdict.canonical, kind: "resolved" };
11
+ }
12
+ if (verdict.status === "ambiguous") {
13
+ return { ...source, alternatives: verdict.alternatives, kind: "ambiguous" };
14
+ }
15
+ if (verdict.status === "unresolved") {
16
+ return { ...source, kind: "unresolved" };
17
+ }
18
+ return null;
19
+ };
20
+
21
+ export {
22
+ assertionMeaningFrom
23
+ };
@@ -0,0 +1,94 @@
1
+ // src/taste-assertion.ts
2
+ var TASTE_ASSERTION_CONTRACT_VERSION = "1.0.0";
3
+ var TAXONOMY_SNAPSHOT_VERSION = "2026-08-25";
4
+ var FIXTURE_ACTOR_ID = "person:designer-1";
5
+ var FIXTURE_PROJECT_ID = "project:hotel-1";
6
+ var FIXTURE_SUBJECT_ID = "client:hotel-operator-1";
7
+ var RESOLVED = "resolved";
8
+ var baseFixture = {
9
+ actorId: FIXTURE_ACTOR_ID,
10
+ confirmedAt: "2026-08-25T09:00:10.000Z",
11
+ contractVersion: TASTE_ASSERTION_CONTRACT_VERSION,
12
+ observedAt: "2026-08-25T09:00:00.000Z",
13
+ projectId: FIXTURE_PROJECT_ID,
14
+ provenance: {
15
+ observationId: "observation:1",
16
+ sourceSnapshotVersion: "demo-estate-1",
17
+ sourceSystem: "designshop"
18
+ },
19
+ subjectId: FIXTURE_SUBJECT_ID
20
+ };
21
+ var PORTABLE_ASSERTION_TEST_VECTORS = [
22
+ {
23
+ assertion: {
24
+ ...baseFixture,
25
+ authority: "explicit",
26
+ id: "assertion:taste-plain-porcelain",
27
+ meaning: {
28
+ canonical: {
29
+ id: "bundle:constitutive%3Amaterial.porcelain%7Caccent%3Apattern.plain",
30
+ kind: "all-of",
31
+ members: [
32
+ { assertion: "material.porcelain", role: "constitutive" },
33
+ { assertion: "pattern.plain", role: "accent" }
34
+ ],
35
+ serialization: "constitutive:material.porcelain|accent:pattern.plain"
36
+ },
37
+ crosswalkVersion: "designshop-2026-08-25.1",
38
+ kind: RESOLVED,
39
+ sourceField: "confirmed_correction",
40
+ sourceValue: "porcelain stayed; pattern did not",
41
+ taxonomySnapshotVersion: TAXONOMY_SNAPSHOT_VERSION
42
+ },
43
+ polarity: "positive",
44
+ role: "taste",
45
+ scope: { kind: "room", roomId: "room:kitchen" }
46
+ },
47
+ expected: {
48
+ actorId: FIXTURE_ACTOR_ID,
49
+ meaningKind: RESOLVED,
50
+ projectId: FIXTURE_PROJECT_ID,
51
+ role: "taste",
52
+ subjectId: FIXTURE_SUBJECT_ID
53
+ },
54
+ id: "client-taste-does-not-belong-to-the-designer"
55
+ },
56
+ {
57
+ assertion: {
58
+ ...baseFixture,
59
+ authority: "explicit",
60
+ constraint: { operator: "at_least", unit: "PTV", value: 36 },
61
+ id: "assertion:requirement-slip",
62
+ meaning: {
63
+ canonical: {
64
+ id: "bundle:constitutive%3Arequirement.slip-resistance",
65
+ kind: "all-of",
66
+ members: [{ assertion: "requirement.slip-resistance", role: "constitutive" }],
67
+ serialization: "constitutive:requirement.slip-resistance"
68
+ },
69
+ crosswalkVersion: "designshop-2026-08-25.1",
70
+ kind: RESOLVED,
71
+ sourceField: "requirement",
72
+ sourceValue: "Slip resistance",
73
+ taxonomySnapshotVersion: TAXONOMY_SNAPSHOT_VERSION
74
+ },
75
+ polarity: "positive",
76
+ role: "requirement",
77
+ scope: { kind: "surface", roomId: "room:lobby", surfaceId: "surface:floor" }
78
+ },
79
+ expected: {
80
+ actorId: FIXTURE_ACTOR_ID,
81
+ meaningKind: RESOLVED,
82
+ projectId: FIXTURE_PROJECT_ID,
83
+ role: "requirement",
84
+ subjectId: FIXTURE_SUBJECT_ID
85
+ },
86
+ id: "commercial-requirement-uses-the-same-envelope"
87
+ }
88
+ ];
89
+
90
+ export {
91
+ TASTE_ASSERTION_CONTRACT_VERSION,
92
+ TAXONOMY_SNAPSHOT_VERSION,
93
+ PORTABLE_ASSERTION_TEST_VECTORS
94
+ };
@@ -0,0 +1,41 @@
1
+ import { CanonicalisationInput, CanonicalisationVerdict } from '../taste.js';
2
+ import { B as BundleMemberInput } from '../match-BDI6Evro.js';
3
+
4
+ declare const DESIGNSHOP_CROSSWALK_VERSION: "designshop-2026-08-25.1";
5
+ declare const DESIGNSHOP_SOURCE_SYSTEM: "designshop";
6
+ declare const DESIGNSHOP_SOURCE_FIELDS: readonly ["product_category", "material", "pattern", "finish", "format", "construction", "scheme_style", "product_style", "mood", "space", "requirement"];
7
+ type DesignShopSourceField = (typeof DESIGNSHOP_SOURCE_FIELDS)[number];
8
+ type Mapping = {
9
+ status: "resolved";
10
+ assertions: readonly BundleMemberInput[];
11
+ } | {
12
+ status: "ambiguous";
13
+ alternatives: readonly (readonly BundleMemberInput[])[];
14
+ } | {
15
+ status: "declined";
16
+ reason: string;
17
+ };
18
+ /**
19
+ * Source-specific rows. Source fields are deliberately retained: DesignShop's `material:Mosaic`
20
+ * resolves to a format, and pretending the source field was already canonical would preserve its
21
+ * category error instead of its evidence.
22
+ */
23
+ declare const DESIGNSHOP_CROSSWALK: Readonly<Record<DesignShopSourceField, Readonly<Record<string, Mapping>>>>;
24
+ interface DesignShopCanonicalisationInput extends Omit<CanonicalisationInput, "sourceSystem" | "sourceField"> {
25
+ sourceField: DesignShopSourceField;
26
+ }
27
+ /** Unknown values are unresolved, never declined. Declines require an explicit curated row. */
28
+ declare const canonicaliseDesignShop: (input: DesignShopCanonicalisationInput) => CanonicalisationVerdict;
29
+ interface DesignShopAppearanceTestVector {
30
+ expectedAssertions: readonly string[];
31
+ facets: readonly {
32
+ sourceField: DesignShopSourceField;
33
+ sourceValue: string;
34
+ }[];
35
+ id: string;
36
+ market: "residential" | "commercial";
37
+ }
38
+ /** Shared fixtures prove axes compose instead of welding material identity to appearance. */
39
+ declare const DESIGNSHOP_APPEARANCE_TEST_VECTORS: readonly DesignShopAppearanceTestVector[];
40
+
41
+ export { DESIGNSHOP_APPEARANCE_TEST_VECTORS, DESIGNSHOP_CROSSWALK, DESIGNSHOP_CROSSWALK_VERSION, DESIGNSHOP_SOURCE_FIELDS, DESIGNSHOP_SOURCE_SYSTEM, type DesignShopAppearanceTestVector, type DesignShopCanonicalisationInput, type DesignShopSourceField, canonicaliseDesignShop };
@@ -0,0 +1,281 @@
1
+ import {
2
+ TAXONOMY_SNAPSHOT_VERSION
3
+ } from "../chunk-XLUE3IC2.js";
4
+ import {
5
+ defineAlternatives,
6
+ defineBundle
7
+ } from "../chunk-C2ZPDTOU.js";
8
+
9
+ // src/crosswalks/designshop.ts
10
+ var DESIGNSHOP_CROSSWALK_VERSION = "designshop-2026-08-25.1";
11
+ var DESIGNSHOP_SOURCE_SYSTEM = "designshop";
12
+ var DESIGNSHOP_SOURCE_FIELDS = [
13
+ "product_category",
14
+ "material",
15
+ "pattern",
16
+ "finish",
17
+ "format",
18
+ "construction",
19
+ "scheme_style",
20
+ "product_style",
21
+ "mood",
22
+ "space",
23
+ "requirement"
24
+ ];
25
+ var resolved = (...assertions) => ({
26
+ assertions: assertions.map((assertion) => ({ assertion, role: "constitutive" })),
27
+ status: "resolved"
28
+ });
29
+ var constitutive = (assertion) => ({
30
+ assertions: [{ assertion, role: "constitutive" }],
31
+ status: "resolved"
32
+ });
33
+ var ambiguous = (...assertions) => ({
34
+ alternatives: assertions.map((assertion) => [{ assertion, role: "constitutive" }]),
35
+ status: "ambiguous"
36
+ });
37
+ var DESIGNSHOP_CROSSWALK = {
38
+ /* oxlint-disable sonarjs/no-duplicate-string -- canonical ids repeat across deliberately
39
+ independent source rows; hiding them behind aliases would make the crosswalk harder to audit. */
40
+ construction: {
41
+ Encaustic: constitutive("construction.encaustic"),
42
+ "Engineered Hardwood": constitutive("construction.engineered-hardwood"),
43
+ Handcrafted: constitutive("construction.handcrafted"),
44
+ Handmade: constitutive("construction.handmade"),
45
+ Handwoven: constitutive("construction.handwoven"),
46
+ "High Pressure Laminate": constitutive("construction.high-pressure-laminate"),
47
+ Laminated: constitutive("construction.laminated"),
48
+ "Loop Pile": constitutive("construction.loop-pile"),
49
+ "Luxury Vinyl": constitutive("construction.luxury-vinyl"),
50
+ "Non-Woven": constitutive("construction.non-woven"),
51
+ "Pre-Cast / Molded": constitutive("construction.precast-moulded"),
52
+ Printed: constitutive("construction.printed"),
53
+ "Red Clay Body": constitutive("construction.red-clay-body"),
54
+ "Rigid Core Luxury Vinyl": constitutive("construction.rigid-core-luxury-vinyl"),
55
+ Sintered: constitutive("construction.sintered"),
56
+ "Through Body": constitutive("construction.through-body"),
57
+ Woven: constitutive("construction.woven")
58
+ },
59
+ finish: {
60
+ Glazed: constitutive("finish.glazed"),
61
+ Gloss: constitutive("finish.gloss"),
62
+ Honed: constitutive("finish.honed"),
63
+ Matte: constitutive("finish.matte"),
64
+ Natural: constitutive("finish.natural"),
65
+ Polished: constitutive("finish.polished")
66
+ },
67
+ format: {
68
+ "Area Rug": constitutive("format.area-rug"),
69
+ Hide: constitutive("format.hide"),
70
+ Liquid: constitutive("format.liquid"),
71
+ Modular: constitutive("format.modular"),
72
+ Mural: constitutive("format.mural"),
73
+ Panel: constitutive("format.panel"),
74
+ Plank: constitutive("format.plank"),
75
+ Roll: constitutive("format.roll"),
76
+ Slab: constitutive("format.slab"),
77
+ Tile: constitutive("format.tile"),
78
+ "Wide Plank": constitutive("format.wide-plank")
79
+ },
80
+ material: {
81
+ "Carpet Tiles": resolved("product-category.carpet-and-rug", "format.modular"),
82
+ Ceramic: constitutive("material.ceramic"),
83
+ "Engineered & Composite": constitutive("material.composite"),
84
+ Interior: {
85
+ reason: "A catalogue location masquerading as a material value.",
86
+ status: "declined"
87
+ },
88
+ Laminate: constitutive("material.laminate"),
89
+ Mosaic: constitutive("format.modular"),
90
+ Natural: constitutive("material.natural-fibre"),
91
+ Paper: constitutive("material.paper"),
92
+ Porcelain: constitutive("material.porcelain"),
93
+ "Solid Surface": constitutive("material.solid-surface"),
94
+ Stone: constitutive("material.stone"),
95
+ Vinyl: constitutive("material.vinyl")
96
+ },
97
+ mood: {
98
+ Airy: constitutive("mood.airy"),
99
+ Calm: constitutive("mood.calm"),
100
+ Dramatic: constitutive("mood.dramatic"),
101
+ Sheltering: constitutive("mood.sheltering")
102
+ },
103
+ pattern: {
104
+ "Abstract / Organic": constitutive("pattern.abstract-organic"),
105
+ "Animal Print": constitutive("pattern.animal-print"),
106
+ Botanical: constitutive("pattern.botanical"),
107
+ "Brocade / Damask / Moire": constitutive("pattern.damask"),
108
+ "Concrete Effect": constitutive("pattern.concrete-effect"),
109
+ "Cultural Heritage": constitutive("pattern.cultural-heritage"),
110
+ Geometric: constitutive("pattern.geometric"),
111
+ "Herringbone / Chevron": constitutive("pattern.herringbone-chevron"),
112
+ "Leather Effect": constitutive("pattern.leather-effect"),
113
+ Marbleized: constitutive("pattern.marbleised"),
114
+ "Metal Effect": constitutive("pattern.metal-effect"),
115
+ Natural: constitutive("pattern.natural-motif"),
116
+ Novelty: constitutive("pattern.novelty"),
117
+ "Plaid / Check / Houndstooth": constitutive("pattern.plaid-check"),
118
+ Scenic: constitutive("pattern.scenic"),
119
+ Solid: constitutive("pattern.plain"),
120
+ "Stone Effect": constitutive("pattern.stone-effect"),
121
+ "Strie / Ombre / Gradient": constitutive("pattern.gradient"),
122
+ Stripe: constitutive("pattern.stripe"),
123
+ Texture: constitutive("pattern.texture"),
124
+ Toile: constitutive("pattern.toile"),
125
+ "Whimsy & Character": constitutive("pattern.character"),
126
+ "Woodgrain Effect": constitutive("pattern.woodgrain-effect"),
127
+ "Woven Effect": constitutive("pattern.woven-effect")
128
+ },
129
+ product_category: {
130
+ "Carpets & Rugs": constitutive("product-category.carpet-and-rug"),
131
+ Countertops: constitutive("product-category.countertop"),
132
+ Flooring: constitutive("product-category.flooring"),
133
+ Tile: constitutive("product-category.tile"),
134
+ Wallcovering: constitutive("product-category.wallcovering")
135
+ },
136
+ product_style: {
137
+ "Art Deco": constitutive("style.art-deco"),
138
+ "Classic/ Traditional": constitutive("style.traditional"),
139
+ Coastal: constitutive("evokes(place.landscape.coastal)"),
140
+ Contemporary: constitutive("style.contemporary"),
141
+ "Country/ Cottage": resolved("evokes(typology.cottage)", "style.traditional"),
142
+ Eclectic: constitutive("style.eclectic"),
143
+ Farmhouse: resolved("evokes(typology.farmhouse)", "style.rustic"),
144
+ Industrial: constitutive("style.industrial"),
145
+ "Mid-Century Modern": constitutive("style.mid-century-modern"),
146
+ Minimalist: constitutive("style.minimalism"),
147
+ Modern: ambiguous("style.modernism", "style.contemporary"),
148
+ Rustic: constitutive("style.rustic"),
149
+ Scandinavian: constitutive("evokes(place.region.nordic)"),
150
+ Transitional: constitutive("style.transitional")
151
+ },
152
+ requirement: {
153
+ "Acoustic performance": constitutive("requirement.acoustic-performance"),
154
+ Availability: constitutive("requirement.availability"),
155
+ Budget: constitutive("requirement.budget"),
156
+ "Fire performance": constitutive("requirement.fire-performance"),
157
+ "Indoor air quality": constitutive("requirement.indoor-air-quality"),
158
+ "Installation method": constitutive("requirement.installation-method"),
159
+ "Lead time": constitutive("requirement.lead-time"),
160
+ Maintenance: constitutive("requirement.maintenance"),
161
+ "Slip resistance": constitutive("requirement.slip-resistance"),
162
+ "Wet-area suitability": constitutive("requirement.wet-area-suitability")
163
+ },
164
+ scheme_style: {
165
+ Coastal: constitutive("evokes(place.landscape.coastal)"),
166
+ Contemporary: constitutive("style.contemporary"),
167
+ Country: ambiguous("evokes(place.region.english-country)", "evokes(typology.farmhouse)"),
168
+ Eclectic: constitutive("style.eclectic"),
169
+ Farmhouse: resolved("evokes(typology.farmhouse)", "style.rustic"),
170
+ Minimalist: constitutive("style.minimalism"),
171
+ Modern: ambiguous("style.modernism", "style.contemporary"),
172
+ Traditional: constitutive("style.traditional"),
173
+ Transitional: constitutive("style.transitional")
174
+ },
175
+ space: {
176
+ Bathroom: constitutive("room.bathroom"),
177
+ Kitchen: constitutive("room.kitchen"),
178
+ Lobby: constitutive("space.lobby"),
179
+ Restaurant: constitutive("room.restaurant")
180
+ }
181
+ /* oxlint-enable sonarjs/no-duplicate-string */
182
+ };
183
+ var canonicaliseDesignShop = (input) => {
184
+ const base = {
185
+ crosswalkVersion: DESIGNSHOP_CROSSWALK_VERSION,
186
+ provenance: input.provenance,
187
+ sourceField: input.sourceField,
188
+ sourceSystem: DESIGNSHOP_SOURCE_SYSTEM,
189
+ sourceValue: input.sourceValue,
190
+ taxonomySnapshotVersion: TAXONOMY_SNAPSHOT_VERSION
191
+ };
192
+ const mapping = DESIGNSHOP_CROSSWALK[input.sourceField][input.sourceValue];
193
+ if (mapping === void 0) {
194
+ return { ...base, status: "unresolved" };
195
+ }
196
+ if (mapping.status === "declined") {
197
+ return { ...base, reason: mapping.reason, status: "declined" };
198
+ }
199
+ if (mapping.status === "ambiguous") {
200
+ return {
201
+ ...base,
202
+ alternatives: defineAlternatives(mapping.alternatives, "source-ambiguous"),
203
+ status: "ambiguous"
204
+ };
205
+ }
206
+ return { ...base, canonical: defineBundle(mapping.assertions), status: "resolved" };
207
+ };
208
+ var DESIGNSHOP_APPEARANCE_TEST_VECTORS = [
209
+ {
210
+ expectedAssertions: ["material.porcelain", "pattern.plain", "product-category.tile"],
211
+ facets: [
212
+ { sourceField: "product_category", sourceValue: "Tile" },
213
+ { sourceField: "material", sourceValue: "Porcelain" },
214
+ { sourceField: "pattern", sourceValue: "Solid" }
215
+ ],
216
+ id: "residential-plain-porcelain",
217
+ market: "residential"
218
+ },
219
+ {
220
+ expectedAssertions: ["material.porcelain", "pattern.geometric", "product-category.tile"],
221
+ facets: [
222
+ { sourceField: "product_category", sourceValue: "Tile" },
223
+ { sourceField: "material", sourceValue: "Porcelain" },
224
+ { sourceField: "pattern", sourceValue: "Geometric" }
225
+ ],
226
+ id: "residential-patterned-porcelain",
227
+ market: "residential"
228
+ },
229
+ {
230
+ expectedAssertions: ["material.porcelain", "pattern.stone-effect", "product-category.tile"],
231
+ facets: [
232
+ { sourceField: "product_category", sourceValue: "Tile" },
233
+ { sourceField: "material", sourceValue: "Porcelain" },
234
+ { sourceField: "pattern", sourceValue: "Stone Effect" }
235
+ ],
236
+ id: "residential-stone-effect-porcelain",
237
+ market: "residential"
238
+ },
239
+ {
240
+ expectedAssertions: [
241
+ "construction.loop-pile",
242
+ "format.modular",
243
+ "pattern.geometric",
244
+ "product-category.carpet-and-rug"
245
+ ],
246
+ facets: [
247
+ { sourceField: "product_category", sourceValue: "Carpets & Rugs" },
248
+ { sourceField: "format", sourceValue: "Modular" },
249
+ { sourceField: "construction", sourceValue: "Loop Pile" },
250
+ { sourceField: "pattern", sourceValue: "Geometric" }
251
+ ],
252
+ id: "commercial-patterned-carpet",
253
+ market: "commercial"
254
+ },
255
+ {
256
+ expectedAssertions: [
257
+ "construction.non-woven",
258
+ "format.roll",
259
+ "material.paper",
260
+ "pattern.botanical",
261
+ "product-category.wallcovering"
262
+ ],
263
+ facets: [
264
+ { sourceField: "product_category", sourceValue: "Wallcovering" },
265
+ { sourceField: "material", sourceValue: "Paper" },
266
+ { sourceField: "format", sourceValue: "Roll" },
267
+ { sourceField: "construction", sourceValue: "Non-Woven" },
268
+ { sourceField: "pattern", sourceValue: "Botanical" }
269
+ ],
270
+ id: "residential-patterned-wallcovering",
271
+ market: "residential"
272
+ }
273
+ ];
274
+ export {
275
+ DESIGNSHOP_APPEARANCE_TEST_VECTORS,
276
+ DESIGNSHOP_CROSSWALK,
277
+ DESIGNSHOP_CROSSWALK_VERSION,
278
+ DESIGNSHOP_SOURCE_FIELDS,
279
+ DESIGNSHOP_SOURCE_SYSTEM,
280
+ canonicaliseDesignShop
281
+ };