@cascivo/mcp 0.1.0 → 0.1.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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # @cascivo/mcp
4
4
 
5
- > MCP server exposing the cascade component registry to AI agents
5
+ > MCP server exposing the cascivo component registry to AI agents
6
6
 
7
7
  [cascivo.com](https://cascivo.com) · [Docs](https://docs.cascivo.com) · [Storybook](https://storybook.cascivo.com) · [GitHub](https://github.com/urbanisierung/cascivo)
8
8
 
@@ -25,17 +25,40 @@ The server speaks the MCP stdio transport. By default it reads the `registry.jso
25
25
 
26
26
  ## Tools
27
27
 
28
- | Tool | Input | Returns |
29
- | ------------------- | ------------------------------------- | -------------------------------------------------------- |
30
- | `list_components` | `{ category? }` | All component manifests, optionally filtered by category |
31
- | `get_component` | `{ name }` | The full manifest for one component |
32
- | `search_components` | `{ query }` | Components matching name, tags, or description |
33
- | `add_to_project` | `{ name, outputDir? }` | Runs `cascivo add <name>` as a child process |
34
- | `create_theme` | `{ primary, neutral, accent, name? }` | A custom theme as CSS (semantic token layer) |
35
- | `scaffold_page` | `{ description, components? }` | A JSX page layout string |
28
+ | Tool | Input | Returns |
29
+ | ------------------- | ------------------------------------- | ------------------------------------------------------------------------------------ |
30
+ | `list_components` | `{ category? }` | All component manifests, optionally filtered by category |
31
+ | `get_component` | `{ name }` | The full manifest for one component |
32
+ | `search_components` | `{ query }` | Components matching name, tags, or description |
33
+ | `add_to_project` | `{ name, outputDir? }` | Runs `cascivo add <name>` as a child process |
34
+ | `create_theme` | `{ primary, neutral, accent, name? }` | A custom theme as CSS (semantic token layer) |
35
+ | `scaffold_page` | `{ description, components? }` | A JSX page layout string |
36
+ | `scaffold_view` | `{ description, components? }` | A validated starter `ViewConfig` + the bound-vocabulary `grammar` for its components |
37
+ | `validate_view` | `{ config }` | Validation errors (component, prop type/enum, refs) with exact paths |
38
+ | `get_view_grammar` | `{ components? }` | Bound-vocabulary grammar + generation prompt for valid `ViewConfig` JSON |
36
39
 
37
40
  `category` is one of `inputs`, `display`, `overlay`, `navigation`, `feedback`.
38
41
 
42
+ ### Bound-vocabulary generation (anti-hallucination)
43
+
44
+ `get_view_grammar` derives — from the `component.meta.ts` manifests — a **system
45
+ prompt** plus a compact **allowed-vocabulary grammar** (each component → its
46
+ props → enum/size/variant values) for emitting valid `ViewConfig` JSON rendered
47
+ by `@cascivo/render` `<CascadeView />`. This is [OpenUI](https://openui.com)'s
48
+ "generate the system prompt from the component library" mechanism (see
49
+ [`docs/ROADMAP-V40.md`](../../docs/ROADMAP-V40.md)): because the grammar is
50
+ **derived**, not authored, an LLM is structurally prevented from inventing
51
+ components, props, or enum values, and the grammar can never drift from the
52
+ components. Pair it with `validate_view` (which now also checks prop types/enums)
53
+ as the enforcement backstop.
54
+
55
+ ```ts
56
+ import { buildGenerationPrompt, loadRegistry } from '@cascivo/mcp'
57
+
58
+ const prompt = buildGenerationPrompt(loadRegistry(), { components: ['Badge', 'Button'] })
59
+ // → use as the system prompt; the model can only emit Badge/Button with their real props.
60
+ ```
61
+
39
62
  ## Programmatic use
40
63
 
41
64
  ```ts
package/dist/index.d.mts CHANGED
@@ -21,7 +21,7 @@ interface ThemeColors {
21
21
  accent: string;
22
22
  }
23
23
  /**
24
- * Generate a custom cascade theme as CSS. Maps the three input colors onto the
24
+ * Generate a custom cascivo theme as CSS. Maps the three input colors onto the
25
25
  * semantic token layer; component tokens inherit automatically.
26
26
  */
27
27
  declare function generateThemeCss(colors: ThemeColors, name?: string): string;
@@ -91,10 +91,56 @@ declare function getComponent(registry: Registry, name: string): ComponentManife
91
91
  /** Fuzzy search over name, tags, and description. */
92
92
  declare function searchComponents(registry: Registry, query: string): ComponentManifest[];
93
93
  //#endregion
94
+ //#region src/grammar.d.ts
95
+ interface GrammarProp {
96
+ name: string;
97
+ required: boolean;
98
+ /** Raw TypeScript type string from the manifest. */
99
+ type: string;
100
+ /** Allowed values, when `type` is a string-literal union (e.g. `'sm' | 'md'`). */
101
+ enum?: string[];
102
+ }
103
+ interface GrammarComponent {
104
+ name: string;
105
+ category: string;
106
+ props: GrammarProp[];
107
+ variants: string[];
108
+ sizes: string[];
109
+ }
110
+ interface ViewGrammar {
111
+ components: GrammarComponent[];
112
+ }
113
+ /**
114
+ * Parse a string-literal union type (`'a' | 'b' | "c"`) into its allowed
115
+ * values. Returns `undefined` for any non-enum type (primitives, functions,
116
+ * named types, object shapes) so the grammar only constrains what it can.
117
+ */
118
+ declare function parseEnum(type: string): string[] | undefined;
119
+ declare function buildGrammar(registry: Registry, subset?: string[]): ViewGrammar;
120
+ /**
121
+ * Render the grammar as a compact, model-friendly table: one line per
122
+ * component. Terser than verbose JSON; a `*` marks a required prop.
123
+ *
124
+ * Badge(size: sm|md, variant: default|secondary|success|warning|destructive|outline)
125
+ */
126
+ declare function formatGrammar(grammar: ViewGrammar): string;
127
+ //#endregion
128
+ //#region src/prompt.d.ts
129
+ interface GenerationPromptOptions {
130
+ /** Scope the vocabulary to a subset of component names. Defaults to all. */
131
+ components?: string[];
132
+ }
133
+ /**
134
+ * Build the bound-vocabulary system prompt for emitting `ViewConfig` JSON.
135
+ * Parameterizable by an optional component subset (defaults to the full
136
+ * registry).
137
+ */
138
+ declare function buildGenerationPrompt(registry: Registry, options?: GenerationPromptOptions): string;
139
+ //#endregion
94
140
  //#region src/index.d.ts
95
141
  declare const VERSION = "0.0.0";
96
142
  /** Start the MCP server over stdio. */
97
143
  declare function main(): Promise<void>;
98
144
  //#endregion
99
- export { type ComponentManifest, type Registry, type RegistryComponent, type ScaffoldOptions, type ServerOptions, type ThemeColors, VERSION, createServer, generateThemeCss, getComponent, listComponents, loadRegistry, main, scaffoldPage, searchComponents };
145
+ export { type ComponentManifest, type GenerationPromptOptions, type GrammarComponent, type GrammarProp, type Registry, type RegistryComponent, type ScaffoldOptions, type ServerOptions, type ThemeColors, VERSION, type ViewGrammar, buildGenerationPrompt, buildGrammar, createServer, formatGrammar, generateThemeCss, getComponent, listComponents, loadRegistry, main, parseEnum, scaffoldPage, searchComponents };
100
146
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/server.ts","../src/theme.ts","../src/scaffold.ts","../src/registry.ts","../src/index.ts"],"mappings":";;;KAsBK,OAAA,IAAW,GAAA,UAAa,IAAA,GAAO,WAAA,KAAgB,OAAA,CAAQ,QAAA;AAAA,UAE3C,aAAA;EACf,YAAA;EACA,OAAA;;EAEA,OAAA,GAAU,OAAO;AAAA;;iBAeH,YAAA,CAAa,OAAA,GAAS,aAAA,GAAqB,SAAS;;;UC3CnD,WAAA;;EAEf,OAAA;EDoBG;EClBH,OAAA;;EAEA,MAAA;AAAA;;;;;iBAiBc,gBAAA,CAAiB,MAAA,EAAQ,WAAW,EAAE,IAAA;;;UCvBrC,eAAA;EACf,WAAA;EACA,UAAU;AAAA;;;;;iBAgCI,YAAA,CAAa,OAAwB,EAAf,eAAe;;;UC9BpC,YAAA;EACf,IAAA;EACA,IAAA;EACA,QAAA;EACA,OAAA;EACA,WAAA;AAAA;AAAA,UAGe,iBAAA;EACf,IAAA;EACA,WAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA;EACA,KAAA;EACA,KAAA,EAAO,YAAY;EACnB,MAAA;EACA,aAAA;IAAiB,IAAA;IAAc,IAAA;IAAc,QAAA;EAAA;EAC7C,QAAA;IAAY,KAAA;IAAe,IAAA;IAAc,WAAA;EAAA;EACzC,YAAA;EACA,IAAA;AAAA;AAAA,UAGe,iBAAA;EACf,IAAA;EACA,IAAA;EACA,WAAA;EACA,QAAA;EACA,OAAA;EACA,KAAA;EACA,YAAA;EACA,IAAA;EACA,IAAA,EAAM,iBAAiB;AAAA;AAAA,UAGR,QAAA;EACf,OAAA;EACA,WAAA;EACA,UAAA,EAAY,iBAAiB;AAAA;AAAA,iBAwBf,YAAA,CAAa,IAAA,YAAgB,QAAQ;;iBAUrC,cAAA,CACd,QAAA,EAAU,QAAA,EACV,QAAA,WACA,IAAA,YACC,iBAAiB;;iBAQJ,YAAA,CAAa,QAAA,EAAU,QAAA,EAAU,IAAA,WAAe,iBAAiB;;iBAoGjE,gBAAA,CAAiB,QAAA,EAAU,QAAA,EAAU,KAAA,WAAgB,iBAAiB;;;cCvLzE,OAAA;;iBAiBS,IAAA,IAAQ,OAAO"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/server.ts","../src/theme.ts","../src/scaffold.ts","../src/registry.ts","../src/grammar.ts","../src/prompt.ts","../src/index.ts"],"mappings":";;;KAwBK,OAAA,IAAW,GAAA,UAAa,IAAA,GAAO,WAAA,KAAgB,OAAA,CAAQ,QAAA;AAAA,UAE3C,aAAA;EACf,YAAA;EACA,OAAA;;EAEA,OAAA,GAAU,OAAO;AAAA;;iBAeH,YAAA,CAAa,OAAA,GAAS,aAAA,GAAqB,SAAS;;;UC7CnD,WAAA;;EAEf,OAAA;EDsBG;ECpBH,OAAA;;EAEA,MAAA;AAAA;;;;;iBAiBc,gBAAA,CAAiB,MAAA,EAAQ,WAAW,EAAE,IAAA;;;UCvBrC,eAAA;EACf,WAAA;EACA,UAAU;AAAA;;;;;iBAgCI,YAAA,CAAa,OAAwB,EAAf,eAAe;;;UC9BpC,YAAA;EACf,IAAA;EACA,IAAA;EACA,QAAA;EACA,OAAA;EACA,WAAA;AAAA;AAAA,UAGe,iBAAA;EACf,IAAA;EACA,WAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA;EACA,KAAA;EACA,KAAA,EAAO,YAAY;EACnB,MAAA;EACA,aAAA;IAAiB,IAAA;IAAc,IAAA;IAAc,QAAA;EAAA;EAC7C,QAAA;IAAY,KAAA;IAAe,IAAA;IAAc,WAAA;EAAA;EACzC,YAAA;EACA,IAAA;AAAA;AAAA,UAGe,iBAAA;EACf,IAAA;EACA,IAAA;EACA,WAAA;EACA,QAAA;EACA,OAAA;EACA,KAAA;EACA,YAAA;EACA,IAAA;EACA,IAAA,EAAM,iBAAiB;AAAA;AAAA,UAGR,QAAA;EACf,OAAA;EACA,WAAA;EACA,UAAA,EAAY,iBAAiB;AAAA;AAAA,iBAwBf,YAAA,CAAa,IAAA,YAAgB,QAAQ;;iBAUrC,cAAA,CACd,QAAA,EAAU,QAAA,EACV,QAAA,WACA,IAAA,YACC,iBAAiB;;iBAQJ,YAAA,CAAa,QAAA,EAAU,QAAA,EAAU,IAAA,WAAe,iBAAiB;;iBAoGjE,gBAAA,CAAiB,QAAA,EAAU,QAAA,EAAU,KAAA,WAAgB,iBAAiB;;;UChLrE,WAAA;EACf,IAAA;EACA,QAAA;EJUU;EIRV,IAAA;EJQkC;EINlC,IAAA;AAAA;AAAA,UAGe,gBAAA;EACf,IAAA;EACA,QAAA;EACA,KAAA,EAAO,WAAW;EAClB,QAAA;EACA,KAAA;AAAA;AAAA,UAGe,WAAA;EACf,UAAA,EAAY,gBAAgB;AAAA;;;;;;iBAQd,SAAA,CAAU,IAAY;AAAA,iBA8BtB,YAAA,CAAa,QAAA,EAAU,QAAA,EAAU,MAAA,cAAoB,WAAW;;AJtC7D;AAenB;;;;iBI8EgB,aAAA,CAAc,OAAoB,EAAX,WAAW;;;UChHjC,uBAAA;;EAEf,UAAU;AAAA;;;;;;iBAsCI,qBAAA,CACd,QAAA,EAAU,QAAA,EACV,OAAA,GAAS,uBAA4B;;;cChD1B,OAAA;;iBA0BS,IAAA,IAAQ,OAAO"}
package/dist/index.mjs CHANGED
@@ -120,14 +120,14 @@ function lighten(color, amount) {
120
120
  return `color-mix(in oklab, ${color}, white ${amount}%)`;
121
121
  }
122
122
  /**
123
- * Generate a custom cascade theme as CSS. Maps the three input colors onto the
123
+ * Generate a custom cascivo theme as CSS. Maps the three input colors onto the
124
124
  * semantic token layer; component tokens inherit automatically.
125
125
  */
126
126
  function generateThemeCss(colors, name = "custom") {
127
127
  const { primary, neutral, accent } = colors;
128
- return `/* Cascade — Generated theme: ${name} */
128
+ return `/* cascivo — Generated theme: ${name} */
129
129
 
130
- @layer cascade.theme {
130
+ @layer cascivo.theme {
131
131
  [data-theme='${name}'] {
132
132
  color-scheme: light;
133
133
 
@@ -323,6 +323,78 @@ function validateView(config, componentNames) {
323
323
  };
324
324
  }
325
325
  //#endregion
326
+ //#region src/grammar.ts
327
+ /**
328
+ * Parse a string-literal union type (`'a' | 'b' | "c"`) into its allowed
329
+ * values. Returns `undefined` for any non-enum type (primitives, functions,
330
+ * named types, object shapes) so the grammar only constrains what it can.
331
+ */
332
+ function parseEnum(type) {
333
+ const branches = type.split("|").map((s) => s.trim()).filter((s) => s.length > 0);
334
+ if (branches.length === 0) return void 0;
335
+ const values = [];
336
+ for (const branch of branches) {
337
+ const quoted = branch.match(/^['"](.+)['"]$/);
338
+ if (!quoted?.[1]) return void 0;
339
+ values.push(quoted[1]);
340
+ }
341
+ return values;
342
+ }
343
+ /**
344
+ * Build the bound-vocabulary descriptor from the registry, optionally scoped to
345
+ * a subset of component names (case-insensitive). Output is deterministic
346
+ * (components and props sorted by name) for reproducible prompts and tests.
347
+ */
348
+ const TYPE_RANK = {
349
+ component: 0,
350
+ block: 1,
351
+ chart: 2,
352
+ section: 3,
353
+ layout: 4
354
+ };
355
+ function buildGrammar(registry, subset) {
356
+ const wanted = subset && subset.length > 0 ? new Set(subset.map((n) => n.toLowerCase())) : null;
357
+ const canonical = /* @__PURE__ */ new Map();
358
+ for (const c of registry.components) {
359
+ const key = c.meta.name.toLowerCase();
360
+ const current = canonical.get(key);
361
+ if (!current || (TYPE_RANK[c.type ?? ""] ?? 9) < (TYPE_RANK[current.type ?? ""] ?? 9)) canonical.set(key, c);
362
+ }
363
+ return { components: [...canonical.values()].filter((c) => !wanted || wanted.has(c.meta.name.toLowerCase()) || wanted.has(c.name.toLowerCase())).map((c) => {
364
+ const props = (c.meta.props ?? []).map((p) => {
365
+ const values = parseEnum(p.type);
366
+ return {
367
+ name: p.name,
368
+ required: p.required === true,
369
+ type: p.type,
370
+ ...values ? { enum: values } : {}
371
+ };
372
+ }).sort((a, b) => a.name.localeCompare(b.name));
373
+ return {
374
+ name: c.meta.name,
375
+ category: c.category,
376
+ props,
377
+ variants: c.meta.variants ?? [],
378
+ sizes: c.meta.sizes ?? []
379
+ };
380
+ }).sort((a, b) => a.name.localeCompare(b.name)) };
381
+ }
382
+ /** Render one prop as `name`, `name*` (required), with enum choices or its type. */
383
+ function formatProp(prop) {
384
+ const mark = prop.required ? "*" : "";
385
+ const value = prop.enum ? prop.enum.join("|") : prop.type;
386
+ return `${prop.name}${mark}: ${value}`;
387
+ }
388
+ /**
389
+ * Render the grammar as a compact, model-friendly table: one line per
390
+ * component. Terser than verbose JSON; a `*` marks a required prop.
391
+ *
392
+ * Badge(size: sm|md, variant: default|secondary|success|warning|destructive|outline)
393
+ */
394
+ function formatGrammar(grammar) {
395
+ return grammar.components.map((c) => `${c.name}(${c.props.map(formatProp).join(", ")})`).join("\n");
396
+ }
397
+ //#endregion
326
398
  //#region src/scaffold-view.ts
327
399
  /** Simple keyword matcher — returns region layout name based on description keywords. */
328
400
  function pickLayout(description) {
@@ -374,10 +446,55 @@ function scaffoldView(input, registry) {
374
446
  const { errors } = validateView(config, new Set(registry.components.map((c) => c.meta.name)));
375
447
  return {
376
448
  config,
377
- errors
449
+ errors,
450
+ grammar: formatGrammar(buildGrammar(registry, componentNames))
378
451
  };
379
452
  }
380
453
  //#endregion
454
+ //#region src/prompt.ts
455
+ const SCHEMA_DOC = `A ViewConfig is a JSON object:
456
+
457
+ {
458
+ "version": 1,
459
+ "view": {
460
+ "layout": "<optional layout name, e.g. dashboard | settings | auth>",
461
+ "regions": {
462
+ "<regionName>": [ <ComponentNode>, ... ]
463
+ }
464
+ }
465
+ }
466
+
467
+ A ComponentNode is:
468
+
469
+ {
470
+ "component": "<one of the registered component names below>",
471
+ "props": { "<propName>": <value> },
472
+ "bind": { "<propName>": "$data.<path>" },
473
+ "events": { "<eventPropName>": "$actions.<name>" },
474
+ "children": <ComponentNode[] | string | { "$t": "<i18n.key>" }>
475
+ }
476
+
477
+ Rules:
478
+ - "component" MUST be one of the registered components listed below — never invent one.
479
+ - Only use props listed for that component; enum props MUST use one of the listed values.
480
+ - "bind" values are host-data references and MUST start with "$data.".
481
+ - "events" values are host-action references and MUST start with "$actions.".
482
+ - "children" is an array of nodes, a plain string, or an i18n ref { "$t": "key" }.
483
+ - Output ONLY the JSON ViewConfig — no prose, no markdown fences.`;
484
+ /**
485
+ * Build the bound-vocabulary system prompt for emitting `ViewConfig` JSON.
486
+ * Parameterizable by an optional component subset (defaults to the full
487
+ * registry).
488
+ */
489
+ function buildGenerationPrompt(registry, options = {}) {
490
+ return [
491
+ "You generate UI as a cascivo ViewConfig — a JSON description rendered by @cascivo/render <CascadeView />.",
492
+ SCHEMA_DOC,
493
+ "Registered components (allowed vocabulary — `name*` marks a required prop):",
494
+ formatGrammar(buildGrammar(registry, options.components))
495
+ ].join("\n\n");
496
+ }
497
+ //#endregion
381
498
  //#region src/tokens.ts
382
499
  const HERE$1 = dirname(fileURLToPath(import.meta.url));
383
500
  const CATALOG_BASE_URL = "https://cascivo.com";
@@ -445,6 +562,7 @@ function selectComponent(need, components) {
445
562
  name: c.name,
446
563
  score,
447
564
  why,
565
+ whenNotToUse: c.intent.whenNotToUse ?? [],
448
566
  related: c.intent.related ?? []
449
567
  };
450
568
  });
@@ -457,7 +575,9 @@ function selectComponent(need, components) {
457
575
  return scored.map((r) => ({
458
576
  name: r.name,
459
577
  score: r.score + (relatedBonus.get(r.name) ?? 0),
460
- why: r.why
578
+ why: r.why,
579
+ whenNotToUse: r.whenNotToUse,
580
+ related: r.related
461
581
  })).filter((r) => r.score >= 0).sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).slice(0, 5);
462
582
  }
463
583
  //#endregion
@@ -490,8 +610,7 @@ function createServer(options = {}) {
490
610
  description: "List available component registries: the cascade directory plus any configured via CASCADE_REGISTRIES env.",
491
611
  inputSchema: {}
492
612
  }, async () => {
493
- const [directory] = await Promise.all([fetchDirectory(fetchFn)]);
494
- return json(mergeRegistries(directory, getEnvRegistries()).map(({ namespace, name, description, verified, homepage }) => ({
613
+ return json(mergeRegistries(await fetchDirectory(fetchFn), getEnvRegistries()).map(({ namespace, name, description, verified, homepage }) => ({
495
614
  namespace,
496
615
  name,
497
616
  description,
@@ -627,18 +746,32 @@ function createServer(options = {}) {
627
746
  components: z.array(z.string()).optional().describe("Component names to include")
628
747
  }
629
748
  }, ({ description, components }) => {
630
- const { config, errors } = scaffoldView({
749
+ const { config, errors, grammar } = scaffoldView({
631
750
  description,
632
751
  ...components ? { components } : {}
633
752
  }, registry);
634
753
  if (errors.length > 0) return json({
635
754
  valid: false,
636
755
  errors,
637
- config
756
+ config,
757
+ grammar
638
758
  });
639
759
  return json({
640
760
  valid: true,
641
- config
761
+ config,
762
+ grammar
763
+ });
764
+ });
765
+ server.registerTool("get_view_grammar", {
766
+ title: "Get view grammar",
767
+ description: "Get the bound-vocabulary grammar + system prompt for generating valid ViewConfig JSON, derived from the component manifests. Use this to constrain an LLM to cascivo's real components, props, and enum values (anti-hallucination). Optionally scope to a subset of components.",
768
+ inputSchema: { components: z.array(z.string()).optional().describe("Scope the vocabulary to these component names. Omit for the full registry.") }
769
+ }, ({ components }) => {
770
+ const grammar = buildGrammar(registry, components);
771
+ return json({
772
+ grammar: formatGrammar(grammar),
773
+ prompt: buildGenerationPrompt(registry, components ? { components } : {}),
774
+ components: grammar.components
642
775
  });
643
776
  });
644
777
  server.registerTool("get_tokens", {
@@ -716,6 +849,6 @@ if (argv[1] !== void 0 && import.meta.url === pathToFileURL(argv[1]).href) main(
716
849
  process.exitCode = 1;
717
850
  });
718
851
  //#endregion
719
- export { VERSION, createServer, generateThemeCss, getComponent, listComponents, loadRegistry, main, scaffoldPage, searchComponents };
852
+ export { VERSION, buildGenerationPrompt, buildGrammar, createServer, formatGrammar, generateThemeCss, getComponent, listComponents, loadRegistry, main, parseEnum, scaffoldPage, searchComponents };
720
853
 
721
854
  //# sourceMappingURL=index.mjs.map