@vectoriox/iox-ui 5.46.0 → 5.47.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.
@@ -0,0 +1,3 @@
1
+ {
2
+ "module": "../fesm2022/vectoriox-iox-ui-harness-core.mjs"
3
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectoriox/iox-ui",
3
- "version": "5.46.0",
3
+ "version": "5.47.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -47,6 +47,10 @@
47
47
  "types": "./generation-core/index.d.ts",
48
48
  "default": "./fesm2022/vectoriox-iox-ui-generation-core.mjs"
49
49
  },
50
+ "./harness-core": {
51
+ "types": "./harness-core/index.d.ts",
52
+ "default": "./fesm2022/vectoriox-iox-ui-harness-core.mjs"
53
+ },
50
54
  "./media-core": {
51
55
  "types": "./media-core/index.d.ts",
52
56
  "default": "./fesm2022/vectoriox-iox-ui-media-core.mjs"
@@ -17,6 +17,7 @@ export * from './site-plan.model';
17
17
  export * from './patterns';
18
18
  export * from './templates';
19
19
  export * from './serialized-node';
20
+ export * from './shape-check';
20
21
  export * from './validate-site-plan';
21
22
  export * from './compose-site';
22
23
  export * from './nav-shape';
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The ONE way a framework-free gate refuses a document "with a path, never repairs" (W1's site-plan gate, the
3
+ * harness's analysis gate): a documented shape table (key → type text; `key?` optional, `[name]` the object's
4
+ * own field names) whose key lists the gate READS — so the shape a model is told about is the shape refused —
5
+ * and a collector that records each refusal at its dotted path. Generic over each gate's issue-code enum.
6
+ */
7
+
8
+ export type ShapeEntry = Readonly<Record<string, string>>;
9
+ export type PlainObject = Record<string, unknown>;
10
+
11
+ const OPTIONAL_MARK = '?';
12
+ const DYNAMIC_KEY = /^\[[^\]]+\]\??$/;
13
+ const isDynamic = (key: string) => DYNAMIC_KEY.test(key);
14
+
15
+ /** Every fixed key an object of the shape may carry. */
16
+ export function shapeKeys(entry: ShapeEntry): string[] {
17
+ return Object.keys(entry).filter(k => !isDynamic(k)).map(k => (k.endsWith(OPTIONAL_MARK) ? k.slice(0, -1) : k));
18
+ }
19
+
20
+ /** The keys an object of the shape must carry. */
21
+ export function requiredShapeKeys(entry: ShapeEntry): string[] {
22
+ return Object.keys(entry).filter(k => !isDynamic(k) && !k.endsWith(OPTIONAL_MARK));
23
+ }
24
+
25
+ export const isPlainObject = (v: unknown): v is PlainObject => !!v && typeof v === 'object' && !Array.isArray(v);
26
+ export const isNonEmptyString = (v: unknown): v is string => typeof v === 'string' && v.trim() !== '';
27
+ /** `path.key`, or `key` at the root. */
28
+ export const pathAt = (path: string, key: string) => (path ? `${path}.${key}` : key);
29
+
30
+ export interface PathIssue<C extends string> {
31
+ code: C;
32
+ /** Where, as a dotted path into the document (`pages[2].sections[0].binding`). */
33
+ path: string;
34
+ message: string;
35
+ }
36
+
37
+ /** The three refusals every shape gate shares, named in that gate's own code enum. */
38
+ export interface ShapeIssueCodes<C extends string> {
39
+ /** A value of the wrong type, or a required value missing. */
40
+ shape: C;
41
+ /** A key the shape does not have. */
42
+ unknownKey: C;
43
+ /** A name used twice where it must be unique. */
44
+ duplicate: C;
45
+ }
46
+
47
+ export class PathIssueCollector<C extends string> {
48
+ readonly issues: PathIssue<C>[] = [];
49
+
50
+ /** `subject` names the document in a refusal ("… is not part of the plan shape"). */
51
+ constructor(private readonly subject: string, private readonly codes: ShapeIssueCodes<C>) {}
52
+
53
+ add(code: C, path: string, message: string): void {
54
+ this.issues.push({ code, path, message });
55
+ }
56
+
57
+ keys(obj: PlainObject, allowed: ReadonlyArray<string>, path: string): void {
58
+ for (const key of Object.keys(obj)) if (!allowed.includes(key)) this.add(this.codes.unknownKey, pathAt(path, key), `"${key}" is not part of the ${this.subject} shape`);
59
+ }
60
+
61
+ string(obj: PlainObject, key: string, path: string, required = true): string | null {
62
+ const v = obj[key];
63
+ if (v === undefined && !required) return null;
64
+ if (!isNonEmptyString(v)) {
65
+ this.add(this.codes.shape, pathAt(path, key), 'must be a non-empty string');
66
+ return null;
67
+ }
68
+ return v;
69
+ }
70
+
71
+ boolean(obj: PlainObject, key: string, path: string): void {
72
+ if (obj[key] !== undefined && typeof obj[key] !== 'boolean') this.add(this.codes.shape, pathAt(path, key), 'must be a boolean');
73
+ }
74
+
75
+ array(obj: PlainObject, key: string, path: string): unknown[] {
76
+ const v = obj[key];
77
+ if (!Array.isArray(v)) {
78
+ this.add(this.codes.shape, pathAt(path, key), 'must be an array');
79
+ return [];
80
+ }
81
+ return v;
82
+ }
83
+
84
+ unique(values: Array<string | null>, path: (i: number) => string, what: string): void {
85
+ const seen = new Set<string>();
86
+ values.forEach((v, i) => {
87
+ if (v === null) return;
88
+ if (seen.has(v)) this.add(this.codes.duplicate, path(i), `${what} "${v}" is used twice`);
89
+ seen.add(v);
90
+ });
91
+ }
92
+ }
@@ -25,7 +25,7 @@ export const SPEC_SITE_PLAN_SHAPE = {
25
25
  "business": {
26
26
  "name": "string — the brand: the header, the hero, the copyright line",
27
27
  "tagline?": "string — the hero lead and the footer line",
28
- "facts?": "{ [fact]: string } — facts (email, phone, city): only the customer's own words; a blank fact is a publish-blocking placeholder, never invented"
28
+ "facts?": "{ [fact]: string } — facts (email, phone, city, address, hours): only the customer's own words; a blank fact is a publish-blocking placeholder, never invented"
29
29
  },
30
30
  "design": {
31
31
  "flavorId": "string — a v2 flavor pack id",
@@ -104,7 +104,9 @@ export const SPEC_SITE_PLAN_SHAPE = {
104
104
  export const SPEC_SITE_FACTS = {
105
105
  "email": "Email address",
106
106
  "phone": "Phone number",
107
- "city": "City or area"
107
+ "city": "City or area",
108
+ "address": "Street address",
109
+ "hours": "Opening hours"
108
110
  } as const;
109
111
 
110
112
  export const SPEC_SITE_PATTERNS = {
@@ -2052,3 +2054,130 @@ export const SPEC_SITE_TEMPLATES = {
2052
2054
  }
2053
2055
  }
2054
2056
  } as const;
2057
+
2058
+ export const SPEC_SITE_LIBRARY = [
2059
+ {
2060
+ "id": "nav.inline-cta",
2061
+ "family": "nav"
2062
+ },
2063
+ {
2064
+ "id": "nav.centered-logo",
2065
+ "family": "nav"
2066
+ },
2067
+ {
2068
+ "id": "nav.overlay-menu",
2069
+ "family": "nav"
2070
+ },
2071
+ {
2072
+ "id": "hero.split",
2073
+ "family": "hero"
2074
+ },
2075
+ {
2076
+ "id": "hero.centered",
2077
+ "family": "hero"
2078
+ },
2079
+ {
2080
+ "id": "hero.full-bleed-media",
2081
+ "family": "hero"
2082
+ },
2083
+ {
2084
+ "id": "hero.stacked-editorial",
2085
+ "family": "hero"
2086
+ },
2087
+ {
2088
+ "id": "proof.logo-strip",
2089
+ "family": "proof"
2090
+ },
2091
+ {
2092
+ "id": "proof.stat-row",
2093
+ "family": "proof"
2094
+ },
2095
+ {
2096
+ "id": "proof.testimonial-single",
2097
+ "family": "proof"
2098
+ },
2099
+ {
2100
+ "id": "proof.testimonial-carousel",
2101
+ "family": "proof"
2102
+ },
2103
+ {
2104
+ "id": "feature.triad",
2105
+ "family": "feature"
2106
+ },
2107
+ {
2108
+ "id": "feature.alternating-rows",
2109
+ "family": "feature"
2110
+ },
2111
+ {
2112
+ "id": "feature.icon-list",
2113
+ "family": "feature"
2114
+ },
2115
+ {
2116
+ "id": "feature.tabbed",
2117
+ "family": "feature"
2118
+ },
2119
+ {
2120
+ "id": "collection.card-grid",
2121
+ "family": "collection"
2122
+ },
2123
+ {
2124
+ "id": "collection.masonry-gallery",
2125
+ "family": "collection"
2126
+ },
2127
+ {
2128
+ "id": "collection.slider",
2129
+ "family": "collection"
2130
+ },
2131
+ {
2132
+ "id": "collection.list-detail",
2133
+ "family": "collection"
2134
+ },
2135
+ {
2136
+ "id": "story.long-form",
2137
+ "family": "story"
2138
+ },
2139
+ {
2140
+ "id": "story.timeline",
2141
+ "family": "story"
2142
+ },
2143
+ {
2144
+ "id": "story.quote-break",
2145
+ "family": "story"
2146
+ },
2147
+ {
2148
+ "id": "cta.banner",
2149
+ "family": "cta"
2150
+ },
2151
+ {
2152
+ "id": "cta.split-form",
2153
+ "family": "cta"
2154
+ },
2155
+ {
2156
+ "id": "contact.split-map",
2157
+ "family": "contact"
2158
+ },
2159
+ {
2160
+ "id": "contact.simple",
2161
+ "family": "contact"
2162
+ },
2163
+ {
2164
+ "id": "faq.accordion",
2165
+ "family": "faq"
2166
+ },
2167
+ {
2168
+ "id": "pricing.tiers",
2169
+ "family": "pricing"
2170
+ },
2171
+ {
2172
+ "id": "footer.three-column",
2173
+ "family": "footer"
2174
+ },
2175
+ {
2176
+ "id": "footer.mega",
2177
+ "family": "footer"
2178
+ },
2179
+ {
2180
+ "id": "footer.minimal",
2181
+ "family": "footer"
2182
+ }
2183
+ ] as const;
@@ -7,6 +7,9 @@ import {
7
7
  FACT_KEYS, GlobalSlot, PlanDataSourceMode, PlanDataSourceType, RelativeOp, SitePlanSourceKind, SITE_PLAN_VERSION,
8
8
  } from './site-plan.model';
9
9
  import { SPEC_SITE_PLAN_SHAPE } from './site-spec.generated';
10
+ import {
11
+ isNonEmptyString, isPlainObject as isObject, PathIssue, PathIssueCollector, pathAt as at, PlainObject as Obj, requiredShapeKeys, ShapeEntry, shapeKeys,
12
+ } from './shape-check';
10
13
 
11
14
  /**
12
15
  * The Site Plan validator — the gate in front of the composer. The wizard's plan and (later) the AI's
@@ -45,12 +48,7 @@ export enum SitePlanIssueCode {
45
48
  MissingPreviewField = 'missing-preview-field',
46
49
  }
47
50
 
48
- export interface SitePlanIssue {
49
- code: SitePlanIssueCode;
50
- /** Where, as a dotted path into the plan (`pages[2].sections[0].binding`). */
51
- path: string;
52
- message: string;
53
- }
51
+ export type SitePlanIssue = PathIssue<SitePlanIssueCode>;
54
52
 
55
53
  export interface SitePlanValidation {
56
54
  valid: boolean;
@@ -67,22 +65,8 @@ export class SitePlanError extends Error {
67
65
  const RESERVED_PREFIX = '$';
68
66
  const ID_FIELD = '_id';
69
67
 
70
- /** The documented plan shape (contract `site.planShape`): key → type; a key ending in `?` is optional and
71
- * a `[name]` key stands for the object's own field names. The key lists below are READ from it, so a
72
- * key the plan documents is a key the gate validates — and an undocumented one is refused. */
73
- type ShapeEntry = Readonly<Record<string, string>>;
74
- const OPTIONAL_MARK = '?';
75
- const DYNAMIC_KEY = /^\[[^\]]+\]\??$/;
76
- const isDynamic = (key: string) => DYNAMIC_KEY.test(key);
77
- /** Every fixed key an object of the shape may carry. */
78
- export function shapeKeys(entry: ShapeEntry): string[] {
79
- return Object.keys(entry).filter(k => !isDynamic(k)).map(k => (k.endsWith(OPTIONAL_MARK) ? k.slice(0, -1) : k));
80
- }
81
- /** The keys an object of the shape must carry. */
82
- export function requiredShapeKeys(entry: ShapeEntry): string[] {
83
- return Object.keys(entry).filter(k => !isDynamic(k) && !k.endsWith(OPTIONAL_MARK));
84
- }
85
-
68
+ /** The documented plan shape (contract `site.planShape`): its key lists are READ (`shape-check`), so a key
69
+ * the plan documents is a key the gate validates — and an undocumented one is refused. */
86
70
  const SHAPE: Readonly<Record<string, ShapeEntry>> = SPEC_SITE_PLAN_SHAPE;
87
71
  const PLAN_KEYS = shapeKeys(SHAPE['plan']);
88
72
  const TEMPLATE_SOURCE_KEYS = shapeKeys(SHAPE['sourceTemplate']);
@@ -104,53 +88,16 @@ const GLOBAL_POSITION: Readonly<Record<`${GlobalSlot}`, `${PatternGlobalPosition
104
88
  const ROUTE = /^\/(?:(?:[a-z0-9]+(?:-[a-z0-9]+)*|:[a-zA-Z_][a-zA-Z0-9_]*)(?:\/(?:[a-z0-9]+(?:-[a-z0-9]+)*|:[a-zA-Z_][a-zA-Z0-9_]*))*)?$/;
105
89
  const ROUTE_PARAM = /:([a-zA-Z_][a-zA-Z0-9_]*)/g;
106
90
 
107
- type Obj = Record<string, unknown>;
108
-
109
- const isObject = (v: unknown): v is Obj => !!v && typeof v === 'object' && !Array.isArray(v);
110
- const isNonEmptyString = (v: unknown): v is string => typeof v === 'string' && v.trim() !== '';
111
91
  const has = (table: object, key: string) => Object.prototype.hasOwnProperty.call(table, key);
112
- const at = (path: string, key: string) => (path ? `${path}.${key}` : key);
113
92
 
114
93
  /** The `:param` names of a route, in order. */
115
94
  export function routeParams(route: string): string[] {
116
95
  return [...route.matchAll(ROUTE_PARAM)].map(m => m[1]);
117
96
  }
118
97
 
119
- class Collector {
120
- readonly issues: SitePlanIssue[] = [];
121
- add(code: SitePlanIssueCode, path: string, message: string): void {
122
- this.issues.push({ code, path, message });
123
- }
124
- keys(obj: Obj, allowed: ReadonlyArray<string>, path: string): void {
125
- for (const key of Object.keys(obj)) if (!allowed.includes(key)) this.add(SitePlanIssueCode.UnknownKey, at(path, key), `"${key}" is not part of the plan shape`);
126
- }
127
- string(obj: Obj, key: string, path: string, required = true): string | null {
128
- const v = obj[key];
129
- if (v === undefined && !required) return null;
130
- if (!isNonEmptyString(v)) {
131
- this.add(SitePlanIssueCode.Shape, at(path, key), 'must be a non-empty string');
132
- return null;
133
- }
134
- return v;
135
- }
136
- boolean(obj: Obj, key: string, path: string): void {
137
- if (obj[key] !== undefined && typeof obj[key] !== 'boolean') this.add(SitePlanIssueCode.Shape, at(path, key), 'must be a boolean');
138
- }
139
- array(obj: Obj, key: string, path: string): unknown[] {
140
- const v = obj[key];
141
- if (!Array.isArray(v)) {
142
- this.add(SitePlanIssueCode.Shape, at(path, key), 'must be an array');
143
- return [];
144
- }
145
- return v;
146
- }
147
- unique(values: Array<string | null>, path: (i: number) => string, what: string): void {
148
- const seen = new Set<string>();
149
- values.forEach((v, i) => {
150
- if (v === null) return;
151
- if (seen.has(v)) this.add(SitePlanIssueCode.Duplicate, path(i), `${what} "${v}" is used twice`);
152
- seen.add(v);
153
- });
98
+ class Collector extends PathIssueCollector<SitePlanIssueCode> {
99
+ constructor() {
100
+ super('plan', { shape: SitePlanIssueCode.Shape, unknownKey: SitePlanIssueCode.UnknownKey, duplicate: SitePlanIssueCode.Duplicate });
154
101
  }
155
102
  }
156
103
 
@@ -0,0 +1,141 @@
1
+ import type { DensityId, DesignMotion, MoodId, MotionId, TintId } from '@vectoriox/iox-ui/design-core';
2
+ import type { FactKey } from '@vectoriox/iox-ui/generation-core';
3
+
4
+ import {
5
+ SPEC_BUSINESS_TYPES, SPEC_HARNESS_SOURCE, SPEC_NEEDS, SPEC_STRUCTURES, SPEC_USER_AVOIDS,
6
+ } from './harness-spec.generated';
7
+
8
+ /**
9
+ * The design harness's knowledge (AI plan rev 3 §3f, H2): the 30 kinds of business IOX serves, each with the
10
+ * styles that suit it (best first, with the reason), its look rules and content rules (one closed grammar,
11
+ * read by the ranker and the lint), its voice, its motion, the pages it needs and its home sections.
12
+ * Generated 1:1 from the contract (`harness`, gated in build-spec.mjs). Seeded from UI UX Pro Max — the
13
+ * attribution travels with the data (`HARNESS_SOURCE.notice`).
14
+ */
15
+ export const HARNESS_SOURCE = SPEC_HARNESS_SOURCE;
16
+
17
+ /** What a site may need — the closed list an analysis returns; each maps to the archetype a plan writes. */
18
+ export type NeedId = keyof typeof SPEC_NEEDS;
19
+ export const NEED_IDS = Object.keys(SPEC_NEEDS) as ReadonlyArray<NeedId>;
20
+ export interface NeedDef {
21
+ readonly label: string;
22
+ readonly archetype: string;
23
+ }
24
+ export const NEEDS: Readonly<Record<NeedId, NeedDef>> = SPEC_NEEDS;
25
+
26
+ /** What an owner may ask to avoid ("no dark backgrounds") — each one look rule. */
27
+ export type UserAvoidId = keyof typeof SPEC_USER_AVOIDS;
28
+ export const USER_AVOID_IDS = Object.keys(SPEC_USER_AVOIDS) as ReadonlyArray<UserAvoidId>;
29
+
30
+ /** The structures a site is built from (Studio · Practice · Place). */
31
+ export type StructureId = keyof typeof SPEC_STRUCTURES;
32
+ export const STRUCTURES = SPEC_STRUCTURES;
33
+
34
+ /** How much a kind of business needs "book a slot" — the next product bet (off this road). */
35
+ export enum BookingNeed {
36
+ Must = 'must',
37
+ Tables = 'tables',
38
+ Sometimes = 'sometimes',
39
+ None = 'none',
40
+ }
41
+
42
+ /** The content checks of the avoid grammar (a look rule is the other kind). */
43
+ export enum AvoidCheck {
44
+ NeedsFamily = 'needs-family',
45
+ NeedsFact = 'needs-fact',
46
+ ImageHero = 'image-hero',
47
+ NoPattern = 'no-pattern',
48
+ BrandHue = 'brand-hue',
49
+ CopyLength = 'copy-length',
50
+ }
51
+
52
+ /** How much text a site carries, shortest first. */
53
+ export enum CopyLength {
54
+ Short = 'short',
55
+ Standard = 'standard',
56
+ Rich = 'rich',
57
+ }
58
+ export const COPY_LENGTH_ORDER: ReadonlyArray<`${CopyLength}`> = [CopyLength.Short, CopyLength.Standard, CopyLength.Rich];
59
+
60
+ /** A look a business should not wear: each named axis lists the values that match (all named axes must). */
61
+ export interface LookCondition {
62
+ readonly flavor?: ReadonlyArray<string>;
63
+ readonly mode?: ReadonlyArray<string>;
64
+ readonly motion?: ReadonlyArray<MotionId>;
65
+ readonly moods?: ReadonlyArray<MoodId>;
66
+ readonly tint?: ReadonlyArray<TintId>;
67
+ readonly density?: ReadonlyArray<DensityId>;
68
+ readonly scroll?: ReadonlyArray<string>;
69
+ readonly animate?: ReadonlyArray<string>;
70
+ readonly between?: ReadonlyArray<string>;
71
+ }
72
+
73
+ export interface LookRule {
74
+ readonly when: LookCondition;
75
+ readonly say: string;
76
+ }
77
+
78
+ export type CheckRule =
79
+ | { readonly check: `${AvoidCheck.NeedsFamily}`; readonly family: string; readonly say: string }
80
+ | { readonly check: `${AvoidCheck.NeedsFact}`; readonly fact: FactKey; readonly say: string }
81
+ | { readonly check: `${AvoidCheck.ImageHero}`; readonly say: string }
82
+ | { readonly check: `${AvoidCheck.NoPattern}`; readonly pattern: string; readonly say: string }
83
+ | { readonly check: `${AvoidCheck.BrandHue}`; readonly from: number; readonly to: number; readonly minChroma: number; readonly say: string }
84
+ | { readonly check: `${AvoidCheck.CopyLength}`; readonly max: `${CopyLength}`; readonly say: string };
85
+
86
+ export type AvoidRule = LookRule | CheckRule;
87
+
88
+ export const isLookRule = (rule: AvoidRule): rule is LookRule => 'when' in rule;
89
+
90
+ export interface GoodFit {
91
+ readonly preset: string;
92
+ readonly reason: string;
93
+ }
94
+
95
+ export interface BusinessType {
96
+ readonly id: string;
97
+ readonly label: string;
98
+ readonly he: string;
99
+ readonly group: string;
100
+ readonly uupm: string;
101
+ /** Keywords (English + Hebrew): the eval's expected answer and the no-model fallback. */
102
+ readonly match: ReadonlyArray<string>;
103
+ /** The styles that suit it, best first, each with why. */
104
+ readonly good: ReadonlyArray<GoodFit>;
105
+ readonly colour: { readonly mood: string; readonly swatches: ReadonlyArray<string> };
106
+ readonly fonts: string;
107
+ readonly tone: string;
108
+ /** The motion TIMING it suits (the design axis — hover lift, durations). */
109
+ readonly motion: MotionId;
110
+ /** How its site usually scrolls, how animated it is and what plays between pages (rev 3.1). */
111
+ readonly moves: Readonly<DesignMotion>;
112
+ readonly imagery: string;
113
+ readonly pages: ReadonlyArray<string>;
114
+ /** The needs its page set implies, in page order (derived from `pages` at contract build). */
115
+ readonly needs: ReadonlyArray<NeedId>;
116
+ /** The home page's sections, library ids. */
117
+ readonly home: ReadonlyArray<string>;
118
+ readonly avoid: ReadonlyArray<AvoidRule>;
119
+ /** Words its copy must not use. */
120
+ readonly words: ReadonlyArray<string>;
121
+ readonly structure: StructureId;
122
+ /** In the M1-alpha slice. */
123
+ readonly slice: boolean;
124
+ /** Stock photos may fill its pictures (never the work itself). */
125
+ readonly stock: boolean;
126
+ readonly booking: `${BookingNeed}`;
127
+ }
128
+
129
+ /** The 30, in the board's order. Assignment here proves the generated data fits the type. */
130
+ export const BUSINESS_TYPES: ReadonlyArray<BusinessType> = SPEC_BUSINESS_TYPES;
131
+
132
+ export function businessTypeById(id: string | null | undefined): BusinessType | undefined {
133
+ return id ? BUSINESS_TYPES.find(t => t.id === id) : undefined;
134
+ }
135
+
136
+ export const BUSINESS_TYPE_IDS: ReadonlyArray<string> = BUSINESS_TYPES.map(t => t.id);
137
+
138
+ /** The user-avoid rule for an id, or undefined. */
139
+ export function userAvoidRule(id: string): LookRule | undefined {
140
+ return (SPEC_USER_AVOIDS as Readonly<Record<string, LookRule>>)[id];
141
+ }