@nanisoft/prism-mcp-server 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +66 -0
  2. package/THIRD-PARTY-NOTICES.md +48 -0
  3. package/dist/descriptions.d.ts +22 -0
  4. package/dist/descriptions.js +33 -0
  5. package/dist/generated/built.d.ts +2 -0
  6. package/dist/generated/built.js +6 -0
  7. package/dist/generated/server-version.d.ts +1 -0
  8. package/dist/generated/server-version.js +5 -0
  9. package/dist/index.d.ts +11 -10
  10. package/dist/index.js +10 -9
  11. package/dist/render.d.ts +69 -44
  12. package/dist/render.js +422 -145
  13. package/dist/rules.d.ts +12 -0
  14. package/dist/rules.js +14 -0
  15. package/dist/schemas.d.ts +104 -0
  16. package/dist/schemas.js +102 -0
  17. package/dist/server.d.ts +22 -0
  18. package/dist/server.js +75 -0
  19. package/dist/version.d.ts +1 -10
  20. package/dist/version.js +10 -7
  21. package/dist/vocab.d.ts +13 -0
  22. package/dist/vocab.js +19 -0
  23. package/package.json +32 -23
  24. package/dist/factory.d.ts +0 -31
  25. package/dist/factory.d.ts.map +0 -1
  26. package/dist/factory.js +0 -33
  27. package/dist/factory.js.map +0 -1
  28. package/dist/index.d.ts.map +0 -1
  29. package/dist/index.js.map +0 -1
  30. package/dist/lookup.d.ts +0 -68
  31. package/dist/lookup.d.ts.map +0 -1
  32. package/dist/lookup.js +0 -113
  33. package/dist/lookup.js.map +0 -1
  34. package/dist/render.d.ts.map +0 -1
  35. package/dist/render.js.map +0 -1
  36. package/dist/search.d.ts +0 -34
  37. package/dist/search.d.ts.map +0 -1
  38. package/dist/search.js +0 -68
  39. package/dist/search.js.map +0 -1
  40. package/dist/store.d.ts +0 -62
  41. package/dist/store.d.ts.map +0 -1
  42. package/dist/store.js +0 -80
  43. package/dist/store.js.map +0 -1
  44. package/dist/tools.d.ts +0 -22
  45. package/dist/tools.d.ts.map +0 -1
  46. package/dist/tools.js +0 -120
  47. package/dist/tools.js.map +0 -1
  48. package/dist/version.d.ts.map +0 -1
  49. package/dist/version.js.map +0 -1
package/dist/render.js CHANGED
@@ -1,156 +1,433 @@
1
- /**
2
- * The markdown every tool answers with — the corpus is Markdown, so JSON
3
- * envelopes would only add tokens and quoting (ADR-0004 §2). All of these are
4
- * pure string builders over already-resolved data; lookup/search logic lives
5
- * elsewhere. Result text always prints the next call to make (§7).
6
- */
7
- import { ITEM_KINDS } from './lookup.js';
8
- /** The one import rule every description and every miss carries. */
9
- export const IMPORT_RULE = "Always import from '@nanisoft/prism-ui', never from antd directly.";
10
- const KIND_LABEL = { component: 'component', block: 'block', page: 'page' };
11
- const KIND_HEADING = { component: 'Components', block: 'Blocks', page: 'Pages' };
12
- const plural = (count, noun) => (count === 1 ? `1 ${noun}` : `${count} ${noun}s`);
13
- /**
14
- * `list_items` — the cold-start catalog. The header line carries the corpus
15
- * version (mismatch detection) and, when the bundler supplies one, the build
16
- * date; pass-throughs are marked with their antd base so the delegation seam
17
- * is visible before any per-item call.
18
- */
19
- export function catalogMarkdown(docs, kind, built) {
20
- const counts = new Map(ITEM_KINDS.map((entry) => [entry, docs.items.filter((item) => item.kind === entry).length]));
21
- const summary = ITEM_KINDS.map((entry) => plural(counts.get(entry) ?? 0, KIND_LABEL[entry])).join(', ');
22
- const lines = [`Prism ${docs.prismVersion} — ${summary}${built ? ` (built ${built})` : ''}`, ''];
1
+ import { STORE_SECTION_TITLES, STORE_SECTIONS, } from '@nanisoft/prism-llms';
2
+ import { IMPORT_RULE, NO_INVENTION_RULE, SURFACE_RULE } from './rules.js';
3
+ import { TOKEN_GROUPS } from './vocab.js';
4
+ /* -------------------------------------------------------------------------- */
5
+ /* Result helpers */
6
+ /* -------------------------------------------------------------------------- */
7
+ const text = (value) => ({ content: [{ type: 'text', text: value }] });
8
+ const error = (value) => ({
9
+ content: [{ type: 'text', text: value }],
10
+ isError: true,
11
+ });
12
+ function fence(code, language = 'tsx') {
13
+ return `\`\`\`${language}\n${code.replace(/\r\n/g, '\n').replace(/\n$/, '')}\n\`\`\``;
14
+ }
15
+ function kindLabel(kind) {
16
+ if (kind === 'component')
17
+ return 'Component';
18
+ if (kind === 'block')
19
+ return 'Block';
20
+ return 'Page';
21
+ }
22
+ function countKinds(items) {
23
+ const counts = { component: 0, block: 0, page: 0 };
24
+ for (const item of items)
25
+ counts[item.kind] += 1;
26
+ return counts;
27
+ }
28
+ /* -------------------------------------------------------------------------- */
29
+ /* Lookup */
30
+ /* -------------------------------------------------------------------------- */
31
+ /** Every exact name or slug match in the optional kind, case-insensitively. */
32
+ export function matchesFor(store, name, kind) {
33
+ const needle = name.trim().toLowerCase();
34
+ const pool = store.items.filter((item) => !kind || item.kind === kind);
35
+ const byName = pool.filter((item) => item.name.toLowerCase() === needle);
36
+ if (byName.length > 0)
37
+ return byName;
38
+ return pool.filter((item) => item.slug.toLowerCase() === needle);
39
+ }
40
+ function suggestItems(store, name, kind) {
41
+ const needle = name.trim().toLowerCase();
42
+ const pool = store.items.filter((item) => !kind || item.kind === kind);
43
+ return pool
44
+ .map((item) => {
45
+ const candidate = `${item.name} ${item.slug}`.toLowerCase();
46
+ let score = candidate.includes(needle) ? 3 : 0;
47
+ let common = 0;
48
+ while (common < item.name.length && common < needle.length && item.name[common]?.toLowerCase() === needle[common]) {
49
+ common += 1;
50
+ }
51
+ score += common;
52
+ return { item, score };
53
+ })
54
+ .filter((entry) => entry.score > 0)
55
+ .sort((a, b) => b.score - a.score || a.item.name.localeCompare(b.item.name))
56
+ .slice(0, 3)
57
+ .map((entry) => entry.item);
58
+ }
59
+ function itemMiss(store, name, kind) {
60
+ const suggestions = suggestItems(store, name, kind);
61
+ const did = suggestions.length > 0
62
+ ? ` Did you mean ${suggestions.map((item) => `\`${item.name}\` (${item.kind})`).join(', ')}?`
63
+ : '';
64
+ return error(`No Prism item named \`${name}\`${kind ? ` of kind \`${kind}\`` : ''}.${did} Browse the catalogue with \`list_items\` or search with \`search_docs\`.`);
65
+ }
66
+ function resolveItem(store, name, kind) {
67
+ const matches = matchesFor(store, name, kind);
68
+ if (matches.length === 0)
69
+ return { ok: false, result: itemMiss(store, name, kind) };
70
+ if (matches.length > 1) {
71
+ const kinds = matches.map((item) => `\`${item.kind}\``).join(', ');
72
+ return {
73
+ ok: false,
74
+ result: error(`\`${name}\` matches more than one kind: ${kinds}. Pass \`kind\` to disambiguate.`),
75
+ };
76
+ }
77
+ const item = matches[0];
78
+ if (!item)
79
+ return { ok: false, result: itemMiss(store, name, kind) };
80
+ return { ok: true, item };
81
+ }
82
+ function renderListItem(item) {
83
+ const category = item.kind === 'component' && item.category ? ` _(${item.category})_` : '';
84
+ return `- **${item.name}** - ${item.description}${category}`;
85
+ }
86
+ export function renderListItems(store, args, built) {
87
+ const { kind, category } = args;
88
+ if (category && kind && kind !== 'component') {
89
+ return error(`\`category\` is a Component-only field. Kind \`${kind}\` has no categories; omit \`category\` or pass \`kind: "component"\`.`);
90
+ }
91
+ const counts = countKinds(store.items);
92
+ const stamp = built ? ` (built ${built})` : '';
93
+ const header = `# Prism ${store.version} - ${counts.component} components, ${counts.block} blocks, ${counts.page} pages${stamp}`;
94
+ const filters = [];
23
95
  if (kind)
24
- lines.push(`Filtered to ${KIND_LABEL[kind]}s — omit \`kind\` for the whole catalog.`, '');
25
- for (const entry of ITEM_KINDS) {
26
- if (kind && entry !== kind)
27
- continue;
28
- const inKind = docs.items.filter((item) => item.kind === entry);
29
- lines.push(`## ${KIND_HEADING[entry]}`, '');
30
- if (inKind.length === 0) {
31
- lines.push(`No ${KIND_LABEL[entry]}s in this build.`, '');
96
+ filters.push(`kind = \`${kind}\``);
97
+ if (category)
98
+ filters.push(`category = \`${category}\``);
99
+ const note = filters.length > 0 ? `\n\nFiltered to ${filters.join(', ')}.` : '';
100
+ let items = store.items;
101
+ if (kind)
102
+ items = items.filter((item) => item.kind === kind);
103
+ if (category) {
104
+ items = items.filter((item) => item.kind === 'component' && item.category === category);
105
+ }
106
+ const groups = [
107
+ { kind: 'component', heading: 'Components' },
108
+ { kind: 'block', heading: 'Blocks' },
109
+ { kind: 'page', heading: 'Pages' },
110
+ ];
111
+ const sections = [];
112
+ for (const group of groups) {
113
+ const groupItems = items.filter((item) => item.kind === group.kind);
114
+ if (groupItems.length === 0)
32
115
  continue;
116
+ sections.push(`## ${group.heading}\n\n${groupItems.map(renderListItem).join('\n')}`);
117
+ }
118
+ if (sections.length === 0) {
119
+ const label = kind ? `${groupPlural(kind)}` : 'items';
120
+ sections.push(`No ${label} in this build.`);
121
+ }
122
+ const footer = `Use \`get_item_doc\`, \`get_item_props\` or \`search_docs\` to read an item.\n\nImport rule: ${IMPORT_RULE}\n\nSurface rule: ${SURFACE_RULE}`;
123
+ return text([`${header}${note}`, ...sections, footer].join('\n\n'));
124
+ }
125
+ function groupPlural(kind) {
126
+ if (kind === 'component')
127
+ return 'components';
128
+ if (kind === 'block')
129
+ return 'blocks';
130
+ return 'pages';
131
+ }
132
+ export function renderItemDoc(store, args) {
133
+ const resolved = resolveItem(store, args.name, args.kind);
134
+ if (!resolved.ok)
135
+ return resolved.result;
136
+ const item = resolved.item;
137
+ return text(`${item.doc.trimEnd()}\n\n---\n\nPublic import: \`${item.importLine}\`\n\nImport rule: ${IMPORT_RULE}`);
138
+ }
139
+ /* -------------------------------------------------------------------------- */
140
+ /* get_item_props */
141
+ /* -------------------------------------------------------------------------- */
142
+ export function renderItemProps(store, args) {
143
+ const resolved = resolveItem(store, args.name, args.kind);
144
+ if (!resolved.ok)
145
+ return resolved.result;
146
+ const item = resolved.item;
147
+ // Both sections, when both are recorded. A Block and a Page publish an
148
+ // interface section AND a composition section, and reading one while calling
149
+ // the other "no own props" is the type-level lie the published surface exists
150
+ // to prevent: the store carried both and the tool reported one and denied the
151
+ // other.
152
+ if (item.props && item.composition) {
153
+ return text(`# ${item.name} - props and composition\n\n## Props\n\n${item.props}\n\n## Composition\n\n${item.composition}\n\n` +
154
+ `This is the complete public Prism API for \`${item.name}\`; internal primitive props are not a consumer contract.\n\n` +
155
+ `Import rule: ${IMPORT_RULE}`);
156
+ }
157
+ if (item.props) {
158
+ return text(`# ${item.name} - props\n\n${item.props}\n\nThis is the complete public Prism API for \`${item.name}\`; internal primitive props are not a consumer contract.\n\nImport rule: ${IMPORT_RULE}`);
159
+ }
160
+ if (item.composition) {
161
+ return text(`# ${item.name} - composition\n\nThe public surface of this ${kindLabel(item.kind)} is the composition below.\n\n${item.composition}\n\nImport rule: ${IMPORT_RULE}`);
162
+ }
163
+ return error(`No props or composition are recorded for \`${item.name}\`. Use \`get_item_doc\` for its documentation.`);
164
+ }
165
+ /* -------------------------------------------------------------------------- */
166
+ /* get_item_source */
167
+ /* -------------------------------------------------------------------------- */
168
+ export function renderItemSource(store, args) {
169
+ const resolved = resolveItem(store, args.name, args.kind);
170
+ if (!resolved.ok)
171
+ return resolved.result;
172
+ const item = resolved.item;
173
+ if (!item.example) {
174
+ return error(`No demo source is recorded for \`${item.name}\`. Use \`get_item_doc\` for its documentation.`);
175
+ }
176
+ return text([
177
+ `# ${item.name} - example source`,
178
+ `Public import:\n\n${fence(item.importLine)}`,
179
+ `${item.example.title}\n\n${fence(item.example.code)}`,
180
+ 'This demo is self-contained: it imports only `@nanisoft/prism-ui/*` or `react`, and exposes one default export.',
181
+ `Import rule: ${IMPORT_RULE}`,
182
+ ].join('\n\n'));
183
+ }
184
+ export function renderThemeDoc(store, args) {
185
+ const pack = args.pack ?? 'default';
186
+ const mode = args.mode ?? 'light';
187
+ const group = args.group;
188
+ if (!store.tokens.packs.includes(pack)) {
189
+ return error(`Unknown pack \`${pack}\`. Valid packs: ${store.tokens.packs.join(', ')}.`);
190
+ }
191
+ if (!store.tokens.modes.includes(mode)) {
192
+ return error(`Unknown mode \`${mode}\`. Valid modes: ${store.tokens.modes.join(', ')}.`);
193
+ }
194
+ if (group !== undefined && !TOKEN_GROUPS.includes(group)) {
195
+ return error(`Unknown group \`${group}\`. Valid groups: ${TOKEN_GROUPS.join(', ')}.`);
196
+ }
197
+ const preamble = [
198
+ `Packs: ${store.tokens.packs.join(', ')}.`,
199
+ `Modes: ${store.tokens.modes.join(', ')}.`,
200
+ `Queryable groups: ${TOKEN_GROUPS.join(', ')}.`,
201
+ // The attribute is not root-only, and the correction is in this string
202
+ // because this string is the agent-facing surface: a tool that prints the
203
+ // pack's radius below and tells the reader the attribute belongs on `<html>`
204
+ // is worse than a tool that says nothing, because the reader then places a
205
+ // boundary where a boundary is not needed and believes a subtree cannot carry
206
+ // one. A boundary moves BOTH axes beneath it, and radius is the axis people
207
+ // forget.
208
+ 'Selection: `data-pack="<id>"` on any element makes that element and its subtree a theme boundary. ' +
209
+ 'It wears the mode of the nearest ancestor carrying `.dark`, and carries that class itself only to hold a fixed mode. ' +
210
+ 'A boundary also moves the pack corner radius, which is why a boundary belongs on a fully-rounded element, on one carrying no radius utility, or on a shape with no radius concept.',
211
+ ].join('\n\n');
212
+ const footer = `Import rule: ${IMPORT_RULE}\n\n${NO_INVENTION_RULE}`;
213
+ if (group === undefined || group === 'semantic') {
214
+ const theme = store.tokens.themes.find((entry) => entry.pack === pack && entry.mode === mode);
215
+ if (!theme) {
216
+ return error(`No resolved values are recorded for pack \`${pack}\` in mode \`${mode}\`.`);
217
+ }
218
+ const lines = theme.semantic.map((token) => `--${token.token}: ${token.value}`).join('\n');
219
+ return text([`# Prism theme: ${pack} / ${mode}`, preamble, lines, footer].join('\n\n'));
220
+ }
221
+ const scale = store.tokens.scales[group];
222
+ const lines = scale
223
+ .map((token) => token.binding
224
+ ? `--${token.token}: ${token.value} (binding: --${token.binding})`
225
+ : `--${token.token}: ${token.value}`)
226
+ .join('\n');
227
+ const close = group === 'breakpoint'
228
+ ? '\n\nThe scale closes with `--breakpoint-xl: initial` and `--breakpoint-2xl: initial`.'
229
+ : '';
230
+ return text([`# Prism token scale: ${group}`, preamble, `${lines}${close}`, footer].join('\n\n'));
231
+ }
232
+ /* -------------------------------------------------------------------------- */
233
+ /* list_pages / get_page */
234
+ /* -------------------------------------------------------------------------- */
235
+ export function renderListPages(store) {
236
+ // The Sections and the label each is read under are both the Store's own, so a
237
+ // Section added to the content tree appears here from the same edit that added
238
+ // it there. A hand-kept list of the three prose Sections is what would leave
239
+ // the Changelogs out of the one tool that exists to list pages.
240
+ const groups = STORE_SECTIONS.map((section) => ({
241
+ section,
242
+ heading: STORE_SECTION_TITLES[section],
243
+ pages: store.pages.filter((page) => page.section === section),
244
+ }));
245
+ const counted = groups.map((group) => `${group.pages.length} ${group.heading.toLowerCase()}`);
246
+ const header = `# Prism pages - ${store.pages.length} pages (${counted.join(', ')})`;
247
+ const sections = groups
248
+ .filter((group) => group.pages.length > 0)
249
+ .map((group) => `## ${group.heading}\n\n${group.pages
250
+ .map((page) => `- **${page.title}** - ${page.description} (\`${page.url}\`)`)
251
+ .join('\n')}`);
252
+ const footer = `Use \`get_page({ url })\` to read one, or \`get_changelog\` for a published package's changes.\n\nImport rule: ${IMPORT_RULE}`;
253
+ return text([header, ...sections, footer].join('\n\n'));
254
+ }
255
+ function suggestPages(store, normalized) {
256
+ const needle = normalized.replace(/^\//, '').toLowerCase();
257
+ return store.pages
258
+ .map((page) => {
259
+ const candidate = `${page.section}/${page.slug}`.toLowerCase();
260
+ let score = candidate.includes(needle) || needle.includes(candidate) ? 3 : 0;
261
+ let common = 0;
262
+ while (common < candidate.length && common < needle.length && candidate[common] === needle[common]) {
263
+ common += 1;
264
+ }
265
+ score += common;
266
+ return { page, score };
267
+ })
268
+ .filter((entry) => entry.score > 0)
269
+ .sort((a, b) => b.score - a.score || a.page.title.localeCompare(b.page.title))
270
+ .slice(0, 3)
271
+ .map((entry) => entry.page);
272
+ }
273
+ export function renderPage(store, args) {
274
+ const raw = args.url.trim();
275
+ const withoutMd = raw.endsWith('.md') ? raw.slice(0, -3) : raw;
276
+ const normalized = withoutMd.startsWith('/') ? withoutMd : `/${withoutMd}`;
277
+ if (/^\/(components|blocks|pages)\//.test(normalized)) {
278
+ return error(`\`${args.url}\` is a catalogue item URL, not a documentation page. Use \`get_item_doc\` or \`get_item_source\` for an item.`);
279
+ }
280
+ const page = store.pages.find((entry) => entry.url === normalized || entry.mirror === raw || entry.url === withoutMd);
281
+ if (!page) {
282
+ const suggestions = suggestPages(store, normalized);
283
+ const did = suggestions.length > 0
284
+ ? ` Did you mean ${suggestions.map((entry) => `\`${entry.url}\``).join(', ')}?`
285
+ : '';
286
+ return error(`No Prism page at \`${args.url}\`.${did} List the pages with \`list_pages\`.`);
287
+ }
288
+ return text(`${page.markdown.trimEnd()}\n\n---\n\nMirror: \`${page.mirror}\`\n\nImport rule: ${IMPORT_RULE}`);
289
+ }
290
+ export function searchCandidates(store, kind) {
291
+ const candidates = [];
292
+ if (kind === undefined || kind !== 'doc') {
293
+ for (const item of store.items) {
294
+ if (kind !== undefined && item.kind !== kind)
295
+ continue;
296
+ candidates.push({
297
+ title: item.name,
298
+ kind: item.kind,
299
+ url: item.url,
300
+ snippet: item.description,
301
+ content: item.doc,
302
+ followUp: `get_item_doc({ name: "${item.name}", kind: "${item.kind}" })`,
303
+ });
304
+ }
305
+ }
306
+ if (kind === undefined || kind === 'doc') {
307
+ for (const page of store.pages) {
308
+ candidates.push({
309
+ title: page.title,
310
+ kind: 'doc',
311
+ url: page.url,
312
+ snippet: page.description,
313
+ content: page.markdown,
314
+ followUp: `get_page({ url: "${page.url}" })`,
315
+ });
33
316
  }
34
- for (const item of inKind)
35
- lines.push(catalogEntry(item));
36
- lines.push('');
37
317
  }
38
- lines.push('---', '', `Next: \`get_item_doc\` for an item's full doc, \`get_item_props\` for its Prism-added props, \`search_docs\` to search everything. Items marked _(extends antd …)_ are pass-throughs — their inherited antd props live in the antd MCP. ${IMPORT_RULE}`);
39
- return lines.join('\n');
318
+ return candidates;
40
319
  }
41
- function catalogEntry(item) {
42
- const base = item.antdBase ? ` _(extends antd ${item.antdBase})_` : '';
43
- return `- **${item.name}** — ${item.description}${base}`;
320
+ export function searchDocs(store, args) {
321
+ const limit = Math.min(Math.max(args.limit ?? 5, 1), 10);
322
+ const terms = args.query
323
+ .toLowerCase()
324
+ .split(/\s+/)
325
+ .filter(Boolean);
326
+ const hits = [];
327
+ for (const candidate of searchCandidates(store, args.kind)) {
328
+ const name = candidate.title.toLowerCase();
329
+ const description = candidate.snippet.toLowerCase();
330
+ const content = candidate.content.toLowerCase();
331
+ const matched = new Set();
332
+ let score = 0;
333
+ let matchesAll = true;
334
+ for (const term of terms) {
335
+ let termScore = 0;
336
+ if (name.includes(term)) {
337
+ matched.add('name');
338
+ termScore += 5;
339
+ }
340
+ if (description.includes(term)) {
341
+ matched.add('description');
342
+ termScore += 3;
343
+ }
344
+ if (content.includes(term)) {
345
+ matched.add('content');
346
+ termScore += 1;
347
+ }
348
+ if (termScore === 0) {
349
+ matchesAll = false;
350
+ break;
351
+ }
352
+ score += termScore;
353
+ }
354
+ if (!matchesAll)
355
+ continue;
356
+ hits.push({ ...candidate, matched: [...matched], score });
357
+ }
358
+ hits.sort((a, b) => b.score - a.score || a.title.localeCompare(b.title));
359
+ const top = hits.slice(0, limit);
360
+ if (top.length === 0) {
361
+ return text(`No matches for \`${args.query}\`${args.kind ? ` in kind \`${args.kind}\`` : ''}. It searches item names, descriptions and page bodies. Try a shorter query, or browse with \`list_items\` and \`list_pages\`.`);
362
+ }
363
+ const header = `# Search: ${args.query}\n\n${top.length} hit${top.length === 1 ? '' : 's'}${args.kind ? ` in kind \`${args.kind}\`` : ''}.`;
364
+ const lines = top.map((hit) => `- **${hit.title}** (${hit.kind}) - ${hit.snippet} [matched: ${hit.matched.join(', ')}] -> \`${hit.followUp}\``);
365
+ const footer = `Read the body with the follow-up call, or repeat a narrower search.\n\nImport rule: ${IMPORT_RULE}`;
366
+ return text([header, lines.join('\n'), footer].join('\n\n'));
44
367
  }
45
368
  /**
46
- * `get_item_doc` — the generator's page verbatim (usage rules, props, example,
47
- * the `> Extends:` seam), plus at most one steering footer built from the data.
369
+ * Every published package that ships a changelog, named and routed, for a miss.
370
+ *
371
+ * The miss carries the whole set rather than a "did you mean", because the
372
+ * argument is a free string rather than a closed enum: an agent that guessed a
373
+ * package name gets the real names in one round trip instead of guessing again.
48
374
  */
49
- export function itemDocMarkdown(item) {
50
- const pointers = [];
51
- if (item.props)
52
- pointers.push(`Prism-added props only: \`get_item_props { "name": "${item.name}" }\``);
53
- if ((item.examples?.length ?? 0) > 0)
54
- pointers.push(`Copyable example source: \`get_item_source { "name": "${item.name}" }\``);
55
- if (item.antdBase)
56
- pointers.push(`Inherited antd props and demos: antd MCP \`antd_info ${item.antdBase}\``);
57
- return pointers.length > 0 ? `${item.doc}\n\n---\n\n${pointers.join(' · ')}` : item.doc;
375
+ function changelogMiss(store, wanted) {
376
+ const available = store.changelogs.length === 0
377
+ ? 'This build carries no package changelogs.'
378
+ : `Prism publishes a changelog for: ${store.changelogs
379
+ .map((entry) => `\`${entry.package}\` (\`${entry.route}\`)`)
380
+ .join(', ')}.`;
381
+ return error(`No Prism changelog for \`${wanted}\`. ${available} Read one with \`get_changelog({ package })\`.`);
382
+ }
383
+ /** The entry a package name, unscoped name or route segment resolves to. */
384
+ function changelogFor(store, wanted) {
385
+ const needle = wanted.trim().toLowerCase();
386
+ return store.changelogs.find((entry) => entry.package.toLowerCase() === needle || entry.slug.toLowerCase() === needle);
58
387
  }
59
388
  /**
60
- * `get_item_props` — only the Prism-added delta. A pass-through's answer *is*
61
- * the routing instruction (ADR-0004 §4: plasma's `_No additional props…_` line
62
- * is the switch-servers signal).
389
+ * One published package's changelog, or one version entry of it.
390
+ *
391
+ * The text is the package's own file, byte for byte, because the Store carries
392
+ * the bytes rather than a summary of them. A generated summary would drift the
393
+ * moment a release was published, which is the one thing a changelog cannot do:
394
+ * a breaking change in a published package has to be discoverable by an agent
395
+ * that never saw the release notes anywhere else. The reference design system
396
+ * shipped a breaking module-system change that no agent using it could find, and
397
+ * the reason is precisely that its corpus had no changelog and no tool to return
398
+ * one.
399
+ *
400
+ * The footers state where the text is published, so an agent holding an older
401
+ * corpus can still fetch the same bytes over HTTP, and which versions exist, so
402
+ * one call can pick the next `version` to ask for.
63
403
  */
64
- export function itemPropsMarkdown(item) {
65
- if (item.props) {
66
- return item.antdBase
67
- ? `${item.props}\n\n---\n\nBeyond these Prism-added props, the inherited antd surface lives in the antd MCP (\`antd_info ${item.antdBase}\`). ${IMPORT_RULE}`
68
- : item.props;
69
- }
70
- if (item.antdBase) {
71
- return [
72
- `## ${item.name} — props`,
73
- '',
74
- `${item.name} — antd ${item.antdBase}, unchanged. Props: use the antd MCP (\`antd_info ${item.antdBase}\`). ${IMPORT_RULE}`,
75
- '',
76
- '_No additional props beyond the antd base component._',
77
- ].join('\n');
78
- }
79
- return [
80
- `## ${item.name} — props`,
81
- '',
82
- `\`${item.name}\` documents no Prism-added props in this build. Its usage rules and example live in the full doc: \`get_item_doc { "name": "${item.name}" }\`.`,
83
- ].join('\n');
84
- }
85
- /** `get_item_source` — the documented example, verbatim, in one fenced block. */
86
- export function itemSourceMarkdown(item, example) {
87
- const labelled = example.title ? `${example.title} (\`${example.slug}\`)` : `\`${example.slug}\``;
88
- const others = (item.examples ?? []).filter((entry) => entry.slug !== example.slug).map((entry) => `\`${entry.slug}\``);
89
- const lines = [
90
- `# ${item.name} — example source: ${labelled}`,
91
- '',
92
- '```tsx',
93
- example.code,
94
- '```',
95
- '',
96
- `Verbatim from the docs demo — self-contained (imports only \`@nanisoft/prism-ui\` and \`react\`). ${IMPORT_RULE}`,
97
- ];
98
- if (others.length > 0)
99
- lines.push('', `Other examples: ${others.join(', ')} — pass \`example\` to pick one.`);
100
- return lines.join('\n');
101
- }
102
- /** `get_theme_doc` — the pack × mode atom verbatim, plus the sibling slugs. */
103
- export function themeMarkdown(theme, all) {
104
- const others = all.filter((entry) => entry.slug !== theme.slug).map((entry) => `\`${entry.slug}\``);
105
- const footer = others.length > 0 ? `\n\n---\n\nOther theme atoms: ${others.join(', ')} — pass \`pack\`/\`mode\` to switch.` : '';
106
- return `${theme.markdown}${footer}`;
107
- }
108
- /** `list_pages` — the site's llms.txt body as an index. */
109
- export function pagesIndexMarkdown(pages, baseUrl) {
110
- const lines = ['# Prism docs pages', ''];
111
- if (pages.length === 0) {
112
- lines.push('This build carries no docs pages — the catalog is the whole corpus for now.');
113
- }
114
- else {
115
- lines.push(`Found ${pages.length}:`, '');
116
- for (const page of pages) {
117
- lines.push(`- **${page.title}** — \`${page.url}\`${page.description ? ` — ${page.description}` : ''}`);
118
- lines.push(` → \`get_page { "url": "${page.url}" }\``);
119
- }
120
- lines.push('');
121
- }
122
- lines.push('---', '', `Full site index: ${baseUrl}/llms.txt · Catalog: \`list_items\`.`);
123
- return lines.join('\n');
124
- }
125
- /** `get_page` — the prerendered page verbatim, cited back to the site. */
126
- export function pageMarkdown(page, baseUrl) {
127
- return `${page.markdown}\n\n---\n\nSource: ${baseUrl}${page.url} · More: \`list_pages\`.`;
128
- }
129
- /** `search_docs` — ranked hits, each printing its own next call. */
130
- export function searchResultsMarkdown(query, hits) {
131
- const lines = [`# Search results for "${query}"`, ''];
132
- if (hits.length === 0) {
133
- lines.push('No matches.', '', 'Try broader terms — or browse: `list_items` for the catalog, `list_pages` for docs pages.');
134
- return lines.join('\n');
135
- }
136
- lines.push(`Found ${hits.length} result(s):`, '');
137
- hits.forEach((hit, index) => {
138
- if (index > 0)
139
- lines.push('---', '');
140
- lines.push(`## ${hit.title} (${hit.kind})`, hit.snippet, `→ \`${hit.followUp}\`${hit.followUp.startsWith('get_item_source') ? ' for the copyable source.' : ''}`);
141
- });
142
- return lines.join('\n');
143
- }
144
- /** A lookup miss: the closest names plus the browse pointer, as an error. */
145
- export function missMarkdown(target, suggestions, browseHint) {
146
- const lines = [`\`${target}\` not found.`];
147
- if (suggestions.length > 0)
148
- lines.push('', `Did you mean: ${suggestions.map((name) => `**${name}**`).join(', ')}?`);
149
- lines.push('', browseHint);
150
- return lines.join('\n');
151
- }
152
- /** A name hit whose `kind` argument pointed somewhere else. */
153
- export function kindMismatchMarkdown(item, requested) {
154
- return `\`${item.name}\` is a ${item.kind}, not a ${requested}. Retry with \`kind\` omitted — or with \`"${item.kind}"\`.`;
155
- }
156
- //# sourceMappingURL=render.js.map
404
+ export function renderChangelog(store, args) {
405
+ const changelog = changelogFor(store, args.package);
406
+ if (changelog === undefined)
407
+ return changelogMiss(store, args.package);
408
+ const versions = changelog.versions.join(', ');
409
+ const published = `Published at \`${changelog.route}\` and its mirror \`${changelog.route}.md\`, byte for byte from the package.`;
410
+ if (args.version === undefined) {
411
+ return text([
412
+ `# ${changelog.package} - changelog`,
413
+ `Versions, newest first as the file records them: ${versions}.`,
414
+ changelog.text.trimEnd(),
415
+ '---',
416
+ `${published} Ask for one entry with \`get_changelog({ package: "${changelog.package}", version: "<version>" })\`.`,
417
+ `Import rule: ${IMPORT_RULE}`,
418
+ ].join('\n\n'));
419
+ }
420
+ const wanted = args.version.trim();
421
+ const release = changelog.releases.find((entry) => entry.version === wanted);
422
+ if (release === undefined) {
423
+ return error(`\`${wanted}\` is not a version \`${changelog.package}\` records. It records: ${versions}. ` +
424
+ `Read the whole changelog with \`get_changelog({ package: "${changelog.package}" })\`.`);
425
+ }
426
+ return text([
427
+ `# ${changelog.package} - ${release.version}`,
428
+ release.body,
429
+ '---',
430
+ `${published} Every version: ${versions}. Read the whole changelog with \`get_changelog({ package: "${changelog.package}" })\`.`,
431
+ `Import rule: ${IMPORT_RULE}`,
432
+ ].join('\n\n'));
433
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The three rules every tool description carries.
3
+ *
4
+ * Ticket 13 requires each description to state the import rule and the surface
5
+ * rule, so a client that reads only `tools/list` learns the contract without a
6
+ * round-trip. The no-invention rule is repeated by `get_theme_doc`.
7
+ */
8
+ export declare const IMPORT_RULE = "Import from `@nanisoft/prism-ui/components/<slug>`, `./blocks/<slug>` or `./pages/<slug>`. Base UI and internal paths are never a consumer import.";
9
+ export declare const SURFACE_RULE = "Prism answers its complete owned surface; Base UI and native elements are internal implementation details.";
10
+ export declare const NO_INVENTION_RULE = "Motion and typography are named by token, never by a millisecond or a `cubic-bezier` literal.";
11
+ /** Compose a tool description: the intro, then the two standing rules. */
12
+ export declare function describe(intro: string): string;
package/dist/rules.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The three rules every tool description carries.
3
+ *
4
+ * Ticket 13 requires each description to state the import rule and the surface
5
+ * rule, so a client that reads only `tools/list` learns the contract without a
6
+ * round-trip. The no-invention rule is repeated by `get_theme_doc`.
7
+ */
8
+ export const IMPORT_RULE = 'Import from `@nanisoft/prism-ui/components/<slug>`, `./blocks/<slug>` or `./pages/<slug>`. Base UI and internal paths are never a consumer import.';
9
+ export const SURFACE_RULE = 'Prism answers its complete owned surface; Base UI and native elements are internal implementation details.';
10
+ export const NO_INVENTION_RULE = 'Motion and typography are named by token, never by a millisecond or a `cubic-bezier` literal.';
11
+ /** Compose a tool description: the intro, then the two standing rules. */
12
+ export function describe(intro) {
13
+ return `${intro}\n\nImport rule: ${IMPORT_RULE}\n\nSurface rule: ${SURFACE_RULE}`;
14
+ }