@makitt.io/mds-mcp-server 0.1.3 → 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 +135 -34
- package/dist/catalog.d.ts +16 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +383 -0
- package/dist/catalog.js.map +1 -0
- package/dist/data/catalog.json +41643 -4127
- package/dist/data/playbook/ai-fill.md +61 -48
- package/dist/data/playbook/anti-patterns.md +112 -110
- package/dist/data/playbook/array-input.md +94 -49
- package/dist/data/playbook/async-states.md +71 -61
- package/dist/data/playbook/data-grid.md +118 -101
- package/dist/data/playbook/feedback.md +103 -84
- package/dist/data/playbook/form.md +164 -134
- package/dist/data/playbook/overlay.md +97 -88
- package/dist/data/playbook/page-layout.md +95 -76
- package/dist/data/playbook/responsive-tokens.md +77 -58
- package/dist/data/recipes/admin-list-page.md +86 -0
- package/dist/data/recipes/async-state-section.md +60 -0
- package/dist/data/recipes/dashboard-overview.md +65 -0
- package/dist/data/recipes/detail-drawer.md +69 -0
- package/dist/data/recipes/modal-form.md +67 -0
- package/dist/data/recipes/settings-form-page.md +79 -0
- package/dist/index.d.ts +4 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -339
- package/dist/index.js.map +1 -1
- package/dist/loaders.d.ts +8 -0
- package/dist/loaders.d.ts.map +1 -0
- package/dist/loaders.js +120 -0
- package/dist/loaders.js.map +1 -0
- package/dist/recipes.d.ts +13 -0
- package/dist/recipes.d.ts.map +1 -0
- package/dist/recipes.js +82 -0
- package/dist/recipes.js.map +1 -0
- package/dist/responses.d.ts +8 -0
- package/dist/responses.d.ts.map +1 -0
- package/dist/responses.js +25 -0
- package/dist/responses.js.map +1 -0
- package/dist/text.d.ts +4 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +20 -0
- package/dist/text.js.map +1 -0
- package/dist/tool-definitions.d.ts +3 -0
- package/dist/tool-definitions.d.ts.map +1 -0
- package/dist/tool-definitions.js +199 -0
- package/dist/tool-definitions.js.map +1 -0
- package/dist/tools.d.ts +4 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +233 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +107 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/package.json +20 -16
package/README.md
CHANGED
|
@@ -1,29 +1,72 @@
|
|
|
1
1
|
# @makitt.io/mds-mcp-server
|
|
2
2
|
|
|
3
3
|
MCP (Model Context Protocol) server for `@makitt/mds`. AI 가 mds 의 catalog +
|
|
4
|
-
playbook
|
|
4
|
+
playbook + recipe 를 자동 query — fabrication 0 보장의 최종 layer.
|
|
5
5
|
|
|
6
|
-
> **Registry**: npmjs.org (표준 public npm registry). `.npmrc` 별도 setup 없이
|
|
6
|
+
> **Registry**: npmjs.org (표준 public npm registry). `.npmrc` 별도 setup 없이
|
|
7
|
+
> install 가능.
|
|
7
8
|
|
|
8
9
|
## 무엇을 하는가
|
|
9
10
|
|
|
10
11
|
Claude Code / Cursor 같은 AI 도구가 다음을 자동 query:
|
|
11
12
|
|
|
12
|
-
- **컴포넌트
|
|
13
|
-
|
|
13
|
+
- **컴포넌트 contract** — public import / props / enum values / type shapes /
|
|
14
|
+
JSDoc / Storybook URLs / story source
|
|
15
|
+
- **Playbook 영역** — form / feedback / data-grid / overlay / page-layout /
|
|
16
|
+
array-input / async-states / ai-fill / anti-patterns
|
|
17
|
+
- **MDS-only composition recipes** — admin list page / settings form page /
|
|
18
|
+
modal form / detail drawer / dashboard overview / async state section
|
|
14
19
|
- **결정 표 검색** — "modal-form vs page-form 어떻게 선택?" → playbook 안 lookup
|
|
15
|
-
- **안티패턴
|
|
20
|
+
- **안티패턴 조회/가드레일** — playbook 기반으로 사용 전 금지 패턴 확인
|
|
16
21
|
|
|
17
22
|
## Tools
|
|
18
23
|
|
|
19
|
-
| Tool
|
|
20
|
-
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
23
|
-
| `
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
24
|
+
| Tool | 용도 |
|
|
25
|
+
| ---------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
26
|
+
| `mds_codegen_plan` | 새 페이지/기능 codegen 시작점. task → recipe/playbook/component 후속 조회 계획 + guardrail 반환 |
|
|
27
|
+
| `mds_components_list` | public JSX component / compound part list. 기본 compact, helper/type/internal 숨김 |
|
|
28
|
+
| `mds_components_get` | 특정 컴포넌트의 full contract (import / props / enum values / type shapes / stories / examples) |
|
|
29
|
+
| `mds_components_search` | alias / dot name / prop / enum value / type / story normalized search |
|
|
30
|
+
| `mds_component_examples_get` | Storybook URL / iframe URL / story source / JSDoc examples |
|
|
31
|
+
| `mds_playbook_list` | Playbook 영역 list |
|
|
32
|
+
| `mds_playbook_get` | 특정 Playbook 의 full content (markdown) |
|
|
33
|
+
| `mds_playbook_search` | 모든 Playbook 의 decision matrix / 안티패턴 text search |
|
|
34
|
+
| `mds_recipes_list` | MDS-only composition recipe list |
|
|
35
|
+
| `mds_recipes_get` | 특정 recipe 의 full MDS composition contract |
|
|
36
|
+
| `mds_recipes_search` | recipe content text search |
|
|
37
|
+
|
|
38
|
+
## Agent Codegen Contract
|
|
39
|
+
|
|
40
|
+
MCP 응답은 AI agent 가 MDS UI 를 바로 작성할 수 있게 아래 정보를 제공한다.
|
|
41
|
+
|
|
42
|
+
- `kind` — `component`, `compoundPart`, `hook`, `helper`, `type`, `store`,
|
|
43
|
+
`internal`
|
|
44
|
+
- `status` — `implemented`, `planned`, `deprecated`, `internal`
|
|
45
|
+
- `import.statement` — 실제 코드에서 써야 하는 public import
|
|
46
|
+
- `props[].values` — union / enum literal values
|
|
47
|
+
- `types` — `ColumnDef<T>`, `SortState`, `TableDensity` 같은 참조 타입 shape
|
|
48
|
+
- `storybook[].storyUrl` / `iframeUrl` — browser agent 가 직접 열 visual oracle
|
|
49
|
+
- `storybook[].source` — 해당 story 의 source snippet
|
|
50
|
+
- `examples` — component JSDoc `@example`
|
|
51
|
+
|
|
52
|
+
기본 `mds_components_list` 는 실제 JSX 로 조립 가능한 public component 와
|
|
53
|
+
compound part 만 반환한다. `insertImage`, `$isImageNode`,
|
|
54
|
+
`NotificationRootProps` 같은 helper / type / internal symbol 은
|
|
55
|
+
`includeInternal: true` 를 명시한 경우에만 탐색 대상이다.
|
|
56
|
+
|
|
57
|
+
Agent 의 표준 사용 순서:
|
|
58
|
+
|
|
59
|
+
0. `mds_codegen_plan` 으로 task 에 맞는 recipe / playbook / component 조회
|
|
60
|
+
순서를 잡는다.
|
|
61
|
+
1. `mds_recipes_search` 또는 `mds_recipes_get` 으로 MDS-only 조립 패턴을
|
|
62
|
+
확인한다.
|
|
63
|
+
2. `mds_playbook_get` 으로 page/form/data/overlay 결정 기준을 확인한다.
|
|
64
|
+
3. `mds_components_search` 로 후보를 찾는다.
|
|
65
|
+
4. `mds_components_get` 으로 import, prop values, type shape 를 확인한다.
|
|
66
|
+
5. `mds_component_examples_get` 의 Storybook `iframeUrl` 을 browser/screenshot
|
|
67
|
+
도구로 열어 실제 렌더링을 확인한다.
|
|
68
|
+
6. 생성한 페이지도 Playwright/Storybook screenshot 으로 desktop/mobile overflow
|
|
69
|
+
와 interaction 을 확인한다.
|
|
27
70
|
|
|
28
71
|
## 설치
|
|
29
72
|
|
|
@@ -33,13 +76,14 @@ Claude Code / Cursor 같은 AI 도구가 다음을 자동 query:
|
|
|
33
76
|
claude mcp add mds -- npx -y @makitt.io/mds-mcp-server@latest
|
|
34
77
|
```
|
|
35
78
|
|
|
36
|
-
→ npmjs.org 에서 자동 fetch + 실행. self-contained (embedded catalog +
|
|
79
|
+
→ npmjs.org 에서 자동 fetch + 실행. self-contained (embedded catalog +
|
|
80
|
+
playbook + recipe). 별도 auth / scope mapping 없음.
|
|
37
81
|
|
|
38
82
|
### B. Local build (mds 개발자 monorepo)
|
|
39
83
|
|
|
40
84
|
```bash
|
|
41
85
|
cd packages/mds && pnpm build # catalog.json 생성
|
|
42
|
-
cd ../mds-mcp-server && pnpm build # tsc +
|
|
86
|
+
cd ../mds-mcp-server && pnpm build # tsc + data/release verify + dogfood + stdio smoke + pack verify
|
|
43
87
|
claude mcp add mds -- node /absolute/path/.../mds-mcp-server/dist/index.js
|
|
44
88
|
```
|
|
45
89
|
|
|
@@ -49,6 +93,7 @@ claude mcp add mds -- node /absolute/path/.../mds-mcp-server/dist/index.js
|
|
|
49
93
|
claude mcp add mds \
|
|
50
94
|
-e MDS_CATALOG_PATH=/path/to/your/catalog.json \
|
|
51
95
|
-e MDS_PLAYBOOK_DIR=/path/to/your/playbook \
|
|
96
|
+
-e MDS_RECIPE_DIR=/path/to/your/recipes \
|
|
52
97
|
-- npx -y @makitt.io/mds-mcp-server@latest
|
|
53
98
|
```
|
|
54
99
|
|
|
@@ -72,20 +117,41 @@ AI: (mds_playbook_get 'form' 자동 호출 → 결정 표 lookup)
|
|
|
72
117
|
|
|
73
118
|
## Path resolution 우선순위
|
|
74
119
|
|
|
75
|
-
1. **env var** (`MDS_CATALOG_PATH` / `MDS_PLAYBOOK_DIR`) —
|
|
120
|
+
1. **env var** (`MDS_CATALOG_PATH` / `MDS_PLAYBOOK_DIR` / `MDS_RECIPE_DIR`) —
|
|
121
|
+
외부 customize
|
|
76
122
|
2. **embedded data** (`dist/data/`) — published package 의 self-contained
|
|
77
123
|
3. **monorepo relative** (`../../mds/`) — dev fallback
|
|
78
124
|
|
|
79
125
|
## Build (data 자동 embed)
|
|
80
126
|
|
|
81
127
|
```bash
|
|
82
|
-
pnpm build # tsc +
|
|
128
|
+
pnpm build # tsc + copy:data + verify:data + dogfood + smoke:stdio + verify:pack
|
|
83
129
|
```
|
|
84
130
|
|
|
85
131
|
- `tsc` — TypeScript compile (`dist/index.js`)
|
|
86
|
-
- `copy-data.mjs` — `../mds/dist/catalog.json` + `../mds/docs/playbook/`
|
|
132
|
+
- `copy-data.mjs` — `../mds/dist/catalog.json` + `../mds/docs/playbook/` +
|
|
133
|
+
`../mds/docs/recipes/` 를 `dist/data/` 로 복사
|
|
134
|
+
- `verify-data.mjs` — embedded catalog/playbook/recipe 누락과 버전 메타 검증
|
|
135
|
+
- `dogfood.mjs` — build 된 MCP API 로 전체 recipe reachability, recipe 참조
|
|
136
|
+
component, compound namespace overview, 대표 agent 시나리오 검증
|
|
137
|
+
- `smoke-stdio.mjs` — MCP SDK client 로 `node dist/index.js` 실제 stdio
|
|
138
|
+
transport 호출 검증
|
|
139
|
+
- `verify-pack.mjs` — `npm pack --dry-run` payload 에 embedded data 포함,
|
|
140
|
+
source/script 누출 없음 검증
|
|
87
141
|
|
|
88
|
-
→ self-contained package. `files: ["dist", "README.md"]` 가 npm publish 에
|
|
142
|
+
→ self-contained package. `files: ["dist", "README.md"]` 가 npm publish 에
|
|
143
|
+
포함됨.
|
|
144
|
+
|
|
145
|
+
## Release policy
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
pnpm verify:release
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- `verify-release.mjs` — Changesets/publish policy, npm files contract,
|
|
152
|
+
README publish guide 검증
|
|
153
|
+
- CI와 `prepublishOnly`에서 실행한다. 앱/로컬 빌드가 루트 `.changeset` 메타데이터에
|
|
154
|
+
묶이지 않도록 `pnpm build`에는 포함하지 않는다.
|
|
89
155
|
|
|
90
156
|
## Transport
|
|
91
157
|
|
|
@@ -97,9 +163,12 @@ npm registry (`registry.npmjs.org`) 에 `@makitt.io/mds-mcp-server` 를 publish.
|
|
|
97
163
|
|
|
98
164
|
### 첫 setup — Granular Access Token (공용 계정 권장)
|
|
99
165
|
|
|
100
|
-
공용 npm 계정 (예: `techtaka-makitt`) publish 흐름을 자동화 — 매번 2FA OTP race
|
|
166
|
+
공용 npm 계정 (예: `techtaka-makitt`) publish 흐름을 자동화 — 매번 2FA OTP race
|
|
167
|
+
에는 부적합. **Granular Access Token + bypass 2FA** 를 1회 setup 하면 이후
|
|
168
|
+
publish 가 자동.
|
|
101
169
|
|
|
102
|
-
1. **token 생성** —
|
|
170
|
+
1. **token 생성** —
|
|
171
|
+
https://www.npmjs.com/settings/<username>/tokens/granular-access-tokens/new
|
|
103
172
|
- **Token name**: `mds-mcp-server publish` (또는 유의미한 라벨)
|
|
104
173
|
- **Expiration**: 1 year (또는 정책 따라)
|
|
105
174
|
- **Allowed IP ranges**: 비워두기 (또는 CI / 사무실 IP)
|
|
@@ -108,35 +177,66 @@ npm registry (`registry.npmjs.org`) 에 `@makitt.io/mds-mcp-server` 를 publish.
|
|
|
108
177
|
- Permissions: **Read and write**
|
|
109
178
|
- **Bypass 2FA**: ✓ enable (publish 시 OTP 요구하지 않음)
|
|
110
179
|
- Generate token → 값 복사 (한 번만 표시)
|
|
111
|
-
2. **token 을 공용 vault 에 share** — 1Password / LastPass / Bitwarden 등 team
|
|
112
|
-
|
|
180
|
+
2. **token 을 공용 vault 에 share** — 1Password / LastPass / Bitwarden 등 team
|
|
181
|
+
공유 entry 에 보관. 채팅 / 코드에 직접 노출 금지
|
|
182
|
+
3. **각 maintainer 의 local setup** — `~/.npmrc` 에 직접 entry 추가 (또는
|
|
183
|
+
`npm config set`):
|
|
113
184
|
```bash
|
|
114
185
|
npm config set //registry.npmjs.org/:_authToken=<token>
|
|
115
186
|
```
|
|
116
187
|
→ `~/.npmrc` 에 추가됨. git ignore 됨 (사용자 home dir).
|
|
117
188
|
|
|
189
|
+
### Version bump — Changesets
|
|
190
|
+
|
|
191
|
+
이 repo 의 package version 은 Changesets 로 올린다. `npm version` 으로 직접 bump
|
|
192
|
+
하지 않는다.
|
|
193
|
+
|
|
194
|
+
- `patch` — MCP answer bugfix, stale embed fix, doc/copy correction
|
|
195
|
+
- `minor` — new MCP tool, new response field, new recipe/playbook surface
|
|
196
|
+
- `major` — tool rename/removal, response shape breaking change, install command
|
|
197
|
+
breaking change
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
pnpm changeset
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
예:
|
|
204
|
+
|
|
205
|
+
```md
|
|
206
|
+
---
|
|
207
|
+
'@makitt.io/mds-mcp-server': minor
|
|
208
|
+
'@makitt/mds': patch
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
Add MCP codegen planning, stdio smoke, and stricter generated catalog contracts.
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
MDS 와 MCP 는 결합되어 있다. MDS component/catalog/playbook/recipe 가 바뀌고 MCP
|
|
215
|
+
embedded answer 가 바뀌면 MCP changeset 도 같이 남긴다.
|
|
216
|
+
|
|
118
217
|
### Publish 흐름
|
|
119
218
|
|
|
219
|
+
maintainer 가 release PR/commit 에서:
|
|
220
|
+
|
|
120
221
|
```bash
|
|
121
|
-
|
|
122
|
-
pnpm
|
|
123
|
-
npm publish --dry-run # tarball 검증 (file list / size / name / version)
|
|
124
|
-
npm publish # 실제 publish (bypass 2FA token 으로 자동 처리)
|
|
222
|
+
pnpm version-packages # changeset version: package.json + changelog 갱신
|
|
223
|
+
pnpm release # packages build + changeset publish
|
|
125
224
|
```
|
|
126
225
|
|
|
127
|
-
|
|
226
|
+
로컬 검증은 publish 전 항상:
|
|
128
227
|
|
|
129
228
|
```bash
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
npm version major # 0.1.0 → 1.0.0 (breaking)
|
|
229
|
+
cd packages/mds-mcp-server
|
|
230
|
+
pnpm build # data/release verify + dogfood + stdio smoke + pack verify
|
|
133
231
|
```
|
|
134
232
|
|
|
135
|
-
|
|
233
|
+
`verify:pack` 이 이미 `npm pack --dry-run --json` payload 를 검사하므로 별도
|
|
234
|
+
수동 `npm publish --dry-run` 은 보조 확인용이다.
|
|
136
235
|
|
|
137
236
|
### 보안
|
|
138
237
|
|
|
139
|
-
- ❌ token 이 채팅 / git commit / chat log 등으로 leak 시 — 즉시 npmjs.com
|
|
238
|
+
- ❌ token 이 채팅 / git commit / chat log 등으로 leak 시 — 즉시 npmjs.com
|
|
239
|
+
settings 에서 **Revoke**
|
|
140
240
|
- ❌ 무한 expiration token — 1 year 이내
|
|
141
241
|
- ❌ "Read and write" 모든 scope 허용 — `@makitt.io` specific scope 만 허용
|
|
142
242
|
- ✓ Bypass 2FA enable — 공용 계정 publish 시 OTP race 방지
|
|
@@ -146,4 +246,5 @@ npm version major # 0.1.0 → 1.0.0 (breaking)
|
|
|
146
246
|
- `mds_lint_check(code)` — 사용자 코드의 mds rule 위반 자동 검출
|
|
147
247
|
- `mds_skills_list` — AI fill 용 skill registry (ai-fill.md)
|
|
148
248
|
- `mds_responsive_check(component, viewport)` — 컴포넌트의 responsive 동작 spec
|
|
149
|
-
- `mds_examples_get(component)` — 컴포넌트 사용 예 (story args + apps/web 실
|
|
249
|
+
- `mds_examples_get(component)` — 컴포넌트 사용 예 (story args + apps/web 실
|
|
250
|
+
사용 모음)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Catalog, CatalogComponent, CompoundOverview } from './types.js';
|
|
2
|
+
export declare function isDefaultListable(component: CatalogComponent): boolean;
|
|
3
|
+
export declare function findComponent(catalog: Catalog, name: string): CatalogComponent | undefined;
|
|
4
|
+
export declare function findExactComponent(catalog: Catalog, name: string): CatalogComponent | undefined;
|
|
5
|
+
export declare function componentMatchesQuery(component: CatalogComponent, queryTokens: string[]): boolean;
|
|
6
|
+
export declare function scoreComponent(component: CatalogComponent, query: string): number;
|
|
7
|
+
export declare function summarizeComponent(component: CatalogComponent, includeDescription: boolean): Record<string, unknown>;
|
|
8
|
+
export declare function componentNamespace(componentName: string): string;
|
|
9
|
+
export declare function compoundNamespaces(catalog: Catalog): string[];
|
|
10
|
+
export declare function collectCompoundParts(catalog: Catalog, namespace: string): CatalogComponent[];
|
|
11
|
+
export declare function findCompoundNamespace(catalog: Catalog, name: string): string | undefined;
|
|
12
|
+
export declare function buildCompoundOverview(catalog: Catalog, namespace: string): CompoundOverview;
|
|
13
|
+
export declare function buildComponentContract(catalog: Catalog, component: CatalogComponent): Record<string, unknown>;
|
|
14
|
+
export declare function componentSuggestions(catalog: Catalog, name: string): string[];
|
|
15
|
+
export declare function summarizeSearchMatch(component: CatalogComponent, queryTokens: string[]): Record<string, unknown>;
|
|
16
|
+
//# sourceMappingURL=catalog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,OAAO,EACP,gBAAgB,EAIhB,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAEpB,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,gBAAgB,GAAG,OAAO,CAQtE;AAgBD,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAK1F;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAU/F;AA0BD,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAIjG;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAcjF;AAED,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAC3B,kBAAkB,EAAE,OAAO,GAC1B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAYzB;AAED,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,CAM7D;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,gBAAgB,EAAE,CA8B5F;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMxF;AAuMD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAgC3F;AAED,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,gBAAgB,GAC1B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUzB;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAY7E;AA6BD,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,gBAAgB,EAC3B,WAAW,EAAE,MAAM,EAAE,GACpB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAsBzB"}
|
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { loadPlaybooks, loadRecipes } from './loaders.js';
|
|
2
|
+
import { normalizeText, uniqueStrings } from './text.js';
|
|
3
|
+
export function isDefaultListable(component) {
|
|
4
|
+
const kind = component.kind ?? 'component';
|
|
5
|
+
const status = component.status ?? 'implemented';
|
|
6
|
+
return (component.public !== false &&
|
|
7
|
+
status !== 'internal' &&
|
|
8
|
+
(kind === 'component' || kind === 'compoundPart'));
|
|
9
|
+
}
|
|
10
|
+
function buildComponentLookup(catalog) {
|
|
11
|
+
const lookup = new Map();
|
|
12
|
+
for (const component of Object.values(catalog.components)) {
|
|
13
|
+
const names = [component.name, component.displayName, ...(component.aliases ?? [])].filter((item) => Boolean(item));
|
|
14
|
+
for (const candidate of names) {
|
|
15
|
+
lookup.set(candidate.toLowerCase(), component);
|
|
16
|
+
lookup.set(normalizeText(candidate), component);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return lookup;
|
|
20
|
+
}
|
|
21
|
+
export function findComponent(catalog, name) {
|
|
22
|
+
const lookup = buildComponentLookup(catalog);
|
|
23
|
+
return (catalog.components[name] ?? lookup.get(name.toLowerCase()) ?? lookup.get(normalizeText(name)));
|
|
24
|
+
}
|
|
25
|
+
export function findExactComponent(catalog, name) {
|
|
26
|
+
const normalizedName = normalizeText(name);
|
|
27
|
+
return (catalog.components[name] ??
|
|
28
|
+
Object.values(catalog.components).find((component) => component.name.toLowerCase() === name.toLowerCase() ||
|
|
29
|
+
normalizeText(component.name) === normalizedName));
|
|
30
|
+
}
|
|
31
|
+
function componentSearchHaystack(component) {
|
|
32
|
+
const parts = [
|
|
33
|
+
component.name,
|
|
34
|
+
component.displayName,
|
|
35
|
+
...(component.aliases ?? []),
|
|
36
|
+
component.layer,
|
|
37
|
+
component.category,
|
|
38
|
+
component.description,
|
|
39
|
+
component.import?.statement,
|
|
40
|
+
...component.props.flatMap((prop) => [
|
|
41
|
+
prop.name,
|
|
42
|
+
prop.description,
|
|
43
|
+
prop.type,
|
|
44
|
+
prop.rawType,
|
|
45
|
+
prop.typeRef,
|
|
46
|
+
...(prop.values ?? []).map(String),
|
|
47
|
+
]),
|
|
48
|
+
...(component.stories ?? []),
|
|
49
|
+
...(component.storybook ?? []).flatMap((story) => [story.name, story.title, story.id]),
|
|
50
|
+
...Object.keys(component.types ?? {}),
|
|
51
|
+
];
|
|
52
|
+
return normalizeText(parts.filter((item) => Boolean(item)).join(' '));
|
|
53
|
+
}
|
|
54
|
+
export function componentMatchesQuery(component, queryTokens) {
|
|
55
|
+
if (queryTokens.length === 0)
|
|
56
|
+
return true;
|
|
57
|
+
const haystack = componentSearchHaystack(component);
|
|
58
|
+
return queryTokens.every((token) => haystack.includes(token));
|
|
59
|
+
}
|
|
60
|
+
export function scoreComponent(component, query) {
|
|
61
|
+
const normalizedQuery = normalizeText(query);
|
|
62
|
+
const normalizedName = normalizeText(component.name);
|
|
63
|
+
if (normalizedName === normalizedQuery)
|
|
64
|
+
return 1000;
|
|
65
|
+
if (normalizedName.includes(normalizedQuery))
|
|
66
|
+
return 800;
|
|
67
|
+
if (normalizedQuery.startsWith(normalizedName))
|
|
68
|
+
return 700;
|
|
69
|
+
if ((component.aliases ?? []).some((alias) => normalizeText(alias) === normalizedQuery)) {
|
|
70
|
+
return 750;
|
|
71
|
+
}
|
|
72
|
+
if ((component.aliases ?? []).some((alias) => normalizeText(alias).includes(normalizedQuery))) {
|
|
73
|
+
return 650;
|
|
74
|
+
}
|
|
75
|
+
if (component.props.some((prop) => normalizeText(prop.name) === normalizedQuery))
|
|
76
|
+
return 550;
|
|
77
|
+
return 100;
|
|
78
|
+
}
|
|
79
|
+
export function summarizeComponent(component, includeDescription) {
|
|
80
|
+
return {
|
|
81
|
+
name: component.name,
|
|
82
|
+
kind: component.kind ?? 'component',
|
|
83
|
+
status: component.status ?? 'implemented',
|
|
84
|
+
layer: component.layer,
|
|
85
|
+
category: component.category,
|
|
86
|
+
import: component.import?.statement,
|
|
87
|
+
hasStories: component.hasStories,
|
|
88
|
+
storyCount: component.storybook?.length ?? component.stories?.length ?? 0,
|
|
89
|
+
...(includeDescription ? { description: component.description } : {}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function componentNamespace(componentName) {
|
|
93
|
+
return componentName.split('.')[0] ?? componentName;
|
|
94
|
+
}
|
|
95
|
+
export function compoundNamespaces(catalog) {
|
|
96
|
+
return uniqueStrings(Object.values(catalog.components)
|
|
97
|
+
.filter((component) => component.name.includes('.'))
|
|
98
|
+
.map((component) => componentNamespace(component.name)));
|
|
99
|
+
}
|
|
100
|
+
export function collectCompoundParts(catalog, namespace) {
|
|
101
|
+
const prefix = `${namespace}.`;
|
|
102
|
+
return Object.values(catalog.components)
|
|
103
|
+
.filter((component) => component.name.startsWith(prefix))
|
|
104
|
+
.filter(isDefaultListable)
|
|
105
|
+
.sort((a, b) => {
|
|
106
|
+
const order = [
|
|
107
|
+
'Root',
|
|
108
|
+
'Trigger',
|
|
109
|
+
'Content',
|
|
110
|
+
'Header',
|
|
111
|
+
'Title',
|
|
112
|
+
'Description',
|
|
113
|
+
'Body',
|
|
114
|
+
'Footer',
|
|
115
|
+
'Close',
|
|
116
|
+
'RawClose',
|
|
117
|
+
];
|
|
118
|
+
const aPart = a.name.slice(prefix.length);
|
|
119
|
+
const bPart = b.name.slice(prefix.length);
|
|
120
|
+
const aIndex = order.indexOf(aPart);
|
|
121
|
+
const bIndex = order.indexOf(bPart);
|
|
122
|
+
if (aIndex !== -1 || bIndex !== -1) {
|
|
123
|
+
return ((aIndex === -1 ? Number.MAX_SAFE_INTEGER : aIndex) -
|
|
124
|
+
(bIndex === -1 ? Number.MAX_SAFE_INTEGER : bIndex));
|
|
125
|
+
}
|
|
126
|
+
return a.name.localeCompare(b.name);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
export function findCompoundNamespace(catalog, name) {
|
|
130
|
+
const normalizedName = normalizeText(name);
|
|
131
|
+
return compoundNamespaces(catalog).find((namespace) => namespace.toLowerCase() === name.toLowerCase() || normalizeText(namespace) === normalizedName);
|
|
132
|
+
}
|
|
133
|
+
function inferRelatedPlaybooks(name, layer) {
|
|
134
|
+
const normalizedName = normalizeText(name);
|
|
135
|
+
const playbooks = new Set();
|
|
136
|
+
if (layer === 'data' ||
|
|
137
|
+
normalizedName.includes('data grid') ||
|
|
138
|
+
normalizedName.includes('table')) {
|
|
139
|
+
playbooks.add('data-grid');
|
|
140
|
+
playbooks.add('async-states');
|
|
141
|
+
}
|
|
142
|
+
if (layer === 'overlays' ||
|
|
143
|
+
/(modal|drawer|sheet|dialog|popover|tooltip|hover card)/.test(normalizedName)) {
|
|
144
|
+
playbooks.add('overlay');
|
|
145
|
+
}
|
|
146
|
+
if (/(field|text field|textarea|select|checkbox|switch|radio|date picker|time picker|file upload|number input)/.test(normalizedName)) {
|
|
147
|
+
playbooks.add('form');
|
|
148
|
+
}
|
|
149
|
+
if (/(toast|notification|banner|empty state|error state|loading state|skeleton)/.test(normalizedName)) {
|
|
150
|
+
playbooks.add('feedback');
|
|
151
|
+
playbooks.add('async-states');
|
|
152
|
+
}
|
|
153
|
+
if (/(page|page header|section|anchor nav|app shell|sidebar|topbar)/.test(normalizedName)) {
|
|
154
|
+
playbooks.add('page-layout');
|
|
155
|
+
}
|
|
156
|
+
if (/(responsive|grid|stack|container)/.test(normalizedName)) {
|
|
157
|
+
playbooks.add('responsive-tokens');
|
|
158
|
+
}
|
|
159
|
+
return Array.from(playbooks).sort();
|
|
160
|
+
}
|
|
161
|
+
function inferRelatedRecipes(name) {
|
|
162
|
+
const normalizedName = normalizeText(name);
|
|
163
|
+
const recipes = new Set();
|
|
164
|
+
if (/(data grid|table|pagination|menu|pill)/.test(normalizedName))
|
|
165
|
+
recipes.add('admin-list-page');
|
|
166
|
+
if (/(modal|field|text field|select|banner)/.test(normalizedName))
|
|
167
|
+
recipes.add('modal-form');
|
|
168
|
+
if (/(drawer|description list|timeline)/.test(normalizedName))
|
|
169
|
+
recipes.add('detail-drawer');
|
|
170
|
+
if (/(page|page header|section|toolbar|textarea|switch|checkbox)/.test(normalizedName)) {
|
|
171
|
+
recipes.add('settings-form-page');
|
|
172
|
+
}
|
|
173
|
+
if (/(stat card|delta|chart|data list)/.test(normalizedName))
|
|
174
|
+
recipes.add('dashboard-overview');
|
|
175
|
+
if (/(loading state|skeleton|empty state|error state|banner)/.test(normalizedName)) {
|
|
176
|
+
recipes.add('async-state-section');
|
|
177
|
+
}
|
|
178
|
+
return Array.from(recipes).sort();
|
|
179
|
+
}
|
|
180
|
+
function availablePlaybooks(ids) {
|
|
181
|
+
try {
|
|
182
|
+
const available = new Set(Object.keys(loadPlaybooks()));
|
|
183
|
+
return ids.filter((id) => available.has(id));
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function availableRecipes(ids) {
|
|
190
|
+
try {
|
|
191
|
+
const available = new Set(Object.keys(loadRecipes()));
|
|
192
|
+
return ids.filter((id) => available.has(id));
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function buildFollowUpTools(name, relatedPlaybooks, relatedRecipes) {
|
|
199
|
+
return [
|
|
200
|
+
...relatedRecipes.slice(0, 2).map((id) => ({
|
|
201
|
+
tool: 'mds_recipes_get',
|
|
202
|
+
args: { id },
|
|
203
|
+
reason: `Confirm the MDS-only composition pattern related to ${name}.`,
|
|
204
|
+
})),
|
|
205
|
+
...relatedPlaybooks.slice(0, 3).map((area) => ({
|
|
206
|
+
tool: 'mds_playbook_get',
|
|
207
|
+
args: { area },
|
|
208
|
+
reason: `Confirm the decision rules and anti-patterns for ${name}.`,
|
|
209
|
+
})),
|
|
210
|
+
{
|
|
211
|
+
tool: 'mds_component_examples_get',
|
|
212
|
+
args: { name },
|
|
213
|
+
reason: 'Open Storybook stories/source as the visual and usage oracle before codegen.',
|
|
214
|
+
},
|
|
215
|
+
];
|
|
216
|
+
}
|
|
217
|
+
function buildComponentAgentGuide(component) {
|
|
218
|
+
const relatedPlaybooks = availablePlaybooks(inferRelatedPlaybooks(component.name, component.layer));
|
|
219
|
+
const relatedRecipes = availableRecipes(inferRelatedRecipes(component.name));
|
|
220
|
+
return {
|
|
221
|
+
intent: `Use ${component.name} from MDS; do not recreate its UI locally.`,
|
|
222
|
+
importStatement: component.import?.statement,
|
|
223
|
+
requiredProps: component.props
|
|
224
|
+
.filter((prop) => prop.required)
|
|
225
|
+
.map((prop) => ({
|
|
226
|
+
name: prop.name,
|
|
227
|
+
type: prop.rawType ?? prop.type,
|
|
228
|
+
description: prop.description,
|
|
229
|
+
})),
|
|
230
|
+
relatedPlaybooks,
|
|
231
|
+
relatedRecipes,
|
|
232
|
+
followUpTools: buildFollowUpTools(component.name, relatedPlaybooks, relatedRecipes),
|
|
233
|
+
checklist: [
|
|
234
|
+
'Use the returned import statement; prefer the public @makitt/mds entrypoint unless a subpath is explicitly better.',
|
|
235
|
+
'Use only documented props and literal values from props[].values.',
|
|
236
|
+
'Check Storybook iframe/source before final UI code when the component affects layout or interaction.',
|
|
237
|
+
'Keep app routing, data fetching, permissions, persistence, and platform rules outside this MDS contract.',
|
|
238
|
+
'Verify generated UI in desktop and mobile viewports.',
|
|
239
|
+
],
|
|
240
|
+
pitfalls: [
|
|
241
|
+
'Do not invent local cards, fields, tables, dialogs, sidebars, or visual CSS when an MDS component exists.',
|
|
242
|
+
'Do not use playbook TBD/planned notes as implemented API unless mds_components_get confirms the prop/component.',
|
|
243
|
+
'Do not hardcode visual values; add or use MDS tokens/components.',
|
|
244
|
+
],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function requiredCompoundParts(namespace, parts) {
|
|
248
|
+
return [`${namespace}.Root`, `${namespace}.Content`].filter((partName) => parts.some((part) => part.name === partName));
|
|
249
|
+
}
|
|
250
|
+
function commonCompoundComposition(namespace, parts) {
|
|
251
|
+
const commonPartNames = [
|
|
252
|
+
'Root',
|
|
253
|
+
'Trigger',
|
|
254
|
+
'Content',
|
|
255
|
+
'Header',
|
|
256
|
+
'Title',
|
|
257
|
+
'Description',
|
|
258
|
+
'Body',
|
|
259
|
+
'Footer',
|
|
260
|
+
'Close',
|
|
261
|
+
].map((part) => `${namespace}.${part}`);
|
|
262
|
+
return parts.map((part) => part.name).filter((partName) => commonPartNames.includes(partName));
|
|
263
|
+
}
|
|
264
|
+
function buildCompoundAgentGuide({ namespace, representative, requiredParts, relatedPlaybooks, relatedRecipes, }) {
|
|
265
|
+
return {
|
|
266
|
+
intent: `Use the ${namespace} compound parts together; do not use a one-off local overlay/composition.`,
|
|
267
|
+
importStatement: representative.import?.statement,
|
|
268
|
+
requiredProps: [],
|
|
269
|
+
relatedPlaybooks,
|
|
270
|
+
relatedRecipes,
|
|
271
|
+
followUpTools: [
|
|
272
|
+
...buildFollowUpTools(namespace, relatedPlaybooks, relatedRecipes),
|
|
273
|
+
...requiredParts.map((partName) => ({
|
|
274
|
+
tool: 'mds_components_get',
|
|
275
|
+
args: { name: partName },
|
|
276
|
+
reason: `Confirm exact props for ${partName}.`,
|
|
277
|
+
})),
|
|
278
|
+
],
|
|
279
|
+
checklist: [
|
|
280
|
+
`Import the compound namespace with: ${representative.import?.statement ?? `import { ${namespace} } from '@makitt/mds';`}`,
|
|
281
|
+
'Query each part you render when it accepts props.',
|
|
282
|
+
'Use the related playbook before choosing modal/drawer/sheet/page placement.',
|
|
283
|
+
'Keep platform behavior outside this MDS-only composition contract.',
|
|
284
|
+
],
|
|
285
|
+
pitfalls: [
|
|
286
|
+
`Do not query only "${namespace}" and then fabricate part props; call mds_components_get for concrete parts.`,
|
|
287
|
+
'Do not nest overlays by default; check overlay/form playbooks first.',
|
|
288
|
+
],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
export function buildCompoundOverview(catalog, namespace) {
|
|
292
|
+
const parts = collectCompoundParts(catalog, namespace);
|
|
293
|
+
const representative = parts.find((part) => part.name === `${namespace}.Root`) ??
|
|
294
|
+
parts.find((part) => part.name === `${namespace}.Content`) ??
|
|
295
|
+
parts[0];
|
|
296
|
+
if (!representative)
|
|
297
|
+
throw new Error(`Compound namespace "${namespace}" has no public parts.`);
|
|
298
|
+
const relatedPlaybooks = availablePlaybooks(inferRelatedPlaybooks(namespace, representative.layer));
|
|
299
|
+
const relatedRecipes = availableRecipes(inferRelatedRecipes(namespace));
|
|
300
|
+
const requiredParts = requiredCompoundParts(namespace, parts);
|
|
301
|
+
return {
|
|
302
|
+
name: namespace,
|
|
303
|
+
kind: 'compound',
|
|
304
|
+
status: 'implemented',
|
|
305
|
+
layer: representative.layer,
|
|
306
|
+
import: representative.import,
|
|
307
|
+
description: `${namespace} is an MDS compound namespace. Query individual parts for exact props before writing code.`,
|
|
308
|
+
parts: parts.map((part) => summarizeComponent(part, true)),
|
|
309
|
+
requiredParts,
|
|
310
|
+
commonComposition: commonCompoundComposition(namespace, parts),
|
|
311
|
+
agentGuide: buildCompoundAgentGuide({
|
|
312
|
+
namespace,
|
|
313
|
+
representative,
|
|
314
|
+
requiredParts,
|
|
315
|
+
relatedPlaybooks,
|
|
316
|
+
relatedRecipes,
|
|
317
|
+
}),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
export function buildComponentContract(catalog, component) {
|
|
321
|
+
return {
|
|
322
|
+
...component,
|
|
323
|
+
agentGuide: buildComponentAgentGuide(component),
|
|
324
|
+
catalogMeta: {
|
|
325
|
+
package: catalog.package,
|
|
326
|
+
version: catalog.version,
|
|
327
|
+
generated: catalog.generated,
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
export function componentSuggestions(catalog, name) {
|
|
332
|
+
const queryTokens = normalizeText(name).split(' ').filter(Boolean);
|
|
333
|
+
const componentSuggestions = Object.values(catalog.components)
|
|
334
|
+
.filter((component) => componentMatchesQuery(component, queryTokens))
|
|
335
|
+
.sort((a, b) => scoreComponent(b, name) - scoreComponent(a, name))
|
|
336
|
+
.slice(0, 5)
|
|
337
|
+
.map((component) => component.name);
|
|
338
|
+
const namespaceSuggestions = compoundNamespaces(catalog).filter((namespace) => normalizeText(namespace).includes(normalizeText(name)));
|
|
339
|
+
return uniqueStrings([...namespaceSuggestions, ...componentSuggestions]).slice(0, 5);
|
|
340
|
+
}
|
|
341
|
+
function propMatchesQuery(component, prop, queryTokens) {
|
|
342
|
+
const aliases = [
|
|
343
|
+
prop.type,
|
|
344
|
+
prop.rawType,
|
|
345
|
+
prop.typeRef,
|
|
346
|
+
...(prop.values ?? []).map(String),
|
|
347
|
+
].filter((item) => Boolean(item));
|
|
348
|
+
return componentMatchesQuery({
|
|
349
|
+
...component,
|
|
350
|
+
name: prop.name,
|
|
351
|
+
displayName: prop.name,
|
|
352
|
+
aliases,
|
|
353
|
+
description: prop.description,
|
|
354
|
+
props: [],
|
|
355
|
+
stories: [],
|
|
356
|
+
hasTests: false,
|
|
357
|
+
hasStories: false,
|
|
358
|
+
}, queryTokens);
|
|
359
|
+
}
|
|
360
|
+
export function summarizeSearchMatch(component, queryTokens) {
|
|
361
|
+
return {
|
|
362
|
+
name: component.name,
|
|
363
|
+
kind: component.kind ?? 'component',
|
|
364
|
+
status: component.status ?? 'implemented',
|
|
365
|
+
layer: component.layer,
|
|
366
|
+
import: component.import?.statement,
|
|
367
|
+
description: component.description?.slice(0, 200),
|
|
368
|
+
matchedProps: component.props
|
|
369
|
+
.filter((prop) => propMatchesQuery(component, prop, queryTokens))
|
|
370
|
+
.slice(0, 3)
|
|
371
|
+
.map((prop) => ({
|
|
372
|
+
name: prop.name,
|
|
373
|
+
type: prop.rawType ?? prop.type,
|
|
374
|
+
values: prop.values,
|
|
375
|
+
})),
|
|
376
|
+
stories: component.storybook?.slice(0, 3).map((story) => ({
|
|
377
|
+
name: story.name,
|
|
378
|
+
id: story.id,
|
|
379
|
+
url: story.storyUrl,
|
|
380
|
+
})),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
//# sourceMappingURL=catalog.js.map
|