wiki-formant 0.63.1 → 0.64.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/dist/blocks.js CHANGED
@@ -145,12 +145,7 @@ export async function mapBlockTreeAsync(blocks, map, shape) {
145
145
  * so a code block inside an infobox shipped unhighlighted.
146
146
  */
147
147
  export function someBlock(blocks, predicate, containers) {
148
- return blocks.some(block => {
149
- const groups = containers(block);
150
- if (!groups)
151
- return predicate(block);
152
- return groups.some(group => someBlock(group, predicate, containers));
153
- });
148
+ return leafBlocks(blocks, containers).some(predicate);
154
149
  }
155
150
  // ---- the shared leaf renderers ----------------------------------------------
156
151
  /**
@@ -58,7 +58,3 @@ export declare function categoryView<P extends FacetablePage & {
58
58
  sortHref: (to: "title" | "recent") => string;
59
59
  sections: CategorySection[];
60
60
  }>;
61
- export type CategoryView<P> = Awaited<ReturnType<typeof categoryView<P & FacetablePage & {
62
- tagPath: string;
63
- slug: string;
64
- }>>>;
package/dist/combobox.js CHANGED
@@ -10,9 +10,7 @@
10
10
  // the attribute that announces the highlighted row as the reader arrows
11
11
  // through it. A combobox without it is a text field that silently changes what
12
12
  // Enter does.
13
- /** The id of one option row. Exported because a caller that scrolls the
14
- * highlighted row into view has to find it by the same id the input points at. */
15
- /** @internal */
13
+ /** The id of one option row, which `aria-activedescendant` names. @internal */
16
14
  export function optionId(baseId, listKey, index) {
17
15
  return `${listId(baseId, listKey)}-opt-${index}`;
18
16
  }
@@ -139,6 +139,8 @@ export declare function agentCardParity(t: Tester): Promise<void>;
139
139
  * will pass is a check that measures nothing.
140
140
  */
141
141
  export declare function conditionalGetChecks(t: Tester, paths: readonly string[]): Promise<void>;
142
+ /** The JSON descriptors every S10 origin serves: the two agent-card paths, the OpenAPI pair, the MCP manifest and the server card. */
143
+ export declare const S10_DESCRIPTORS: readonly string[];
142
144
  /**
143
145
  * A JSON descriptor is revalidatable — an agent card, an OpenAPI document, a
144
146
  * registry manifest, a server card.
@@ -149,8 +151,6 @@ export declare function conditionalGetChecks(t: Tester, paths: readonly string[]
149
151
  * document moving, which is worse than none. The body-derived ETag is the whole
150
152
  * validator, and it is enough.
151
153
  */
152
- /** The JSON descriptors every S10 origin serves: the two agent-card paths, the OpenAPI pair, the MCP manifest and the server card. */
153
- export declare const S10_DESCRIPTORS: readonly string[];
154
154
  export declare function descriptorChecks(t: Tester, paths?: readonly string[]): Promise<void>;
155
155
  /**
156
156
  * Documents that project one corpus at different depths carry different ETags.
@@ -327,6 +327,15 @@ export async function conditionalGetChecks(t, paths) {
327
327
  }
328
328
  }
329
329
  }
330
+ /** The JSON descriptors every S10 origin serves: the two agent-card paths, the OpenAPI pair, the MCP manifest and the server card. */
331
+ export const S10_DESCRIPTORS = [
332
+ '.well-known/agent-card.json',
333
+ '.well-known/agent.json',
334
+ '.well-known/openapi.json',
335
+ 'openapi.json',
336
+ '.well-known/mcp.json',
337
+ 'api/mcp/server-card',
338
+ ];
330
339
  /**
331
340
  * A JSON descriptor is revalidatable — an agent card, an OpenAPI document, a
332
341
  * registry manifest, a server card.
@@ -337,15 +346,6 @@ export async function conditionalGetChecks(t, paths) {
337
346
  * document moving, which is worse than none. The body-derived ETag is the whole
338
347
  * validator, and it is enough.
339
348
  */
340
- /** The JSON descriptors every S10 origin serves: the two agent-card paths, the OpenAPI pair, the MCP manifest and the server card. */
341
- export const S10_DESCRIPTORS = [
342
- '.well-known/agent-card.json',
343
- '.well-known/agent.json',
344
- '.well-known/openapi.json',
345
- 'openapi.json',
346
- '.well-known/mcp.json',
347
- 'api/mcp/server-card',
348
- ];
349
349
  export async function descriptorChecks(t, paths = S10_DESCRIPTORS) {
350
350
  for (const path of paths) {
351
351
  const url = path.startsWith('http') ? path : `${t.base}/${path.replace(/^\//, '')}`;
package/dist/crawlers.js CHANGED
@@ -53,10 +53,6 @@ export function detectAiBot(userAgent) {
53
53
  }
54
54
  return null;
55
55
  }
56
- /** Every token that needs its own robots.txt group — which is all of them. @internal */
57
- export function aiCrawlerTokens() {
58
- return AI_CRAWLERS.map(c => c.token);
59
- }
60
56
  /**
61
57
  * The agent surface an S10 origin serves, allowed in every group. All three
62
58
  * `robots.ts` files listed these by hand, and a path missing from one list is
@@ -110,7 +106,7 @@ export function crawlerRules(opts) {
110
106
  // honestly as itself was told the endpoint was off limits. The roster is
111
107
  // for indexing policy, not for hiding a documented endpoint.
112
108
  { userAgent: '*', allow: [...new Set([...[opts.allow].flat(), ...aiAllow])], disallow },
113
- ...aiCrawlerTokens().map(userAgent => ({ userAgent, allow: aiAllow, disallow })),
109
+ ...AI_CRAWLERS.map(({ token: userAgent }) => ({ userAgent, allow: aiAllow, disallow })),
114
110
  ...SEARCH_ENGINES.map(userAgent => ({ userAgent, allow: '/', disallow: searchDisallow })),
115
111
  ];
116
112
  }
package/dist/headings.js CHANGED
@@ -79,14 +79,7 @@ export function injectHeadingIds(html, options = {}) {
79
79
  * consumers use only the injector above.
80
80
  */
81
81
  export function headingsFrom(html) {
82
- const out = [];
83
- for (const [, tag, attrs, content] of html.matchAll(HEADING)) {
84
- const id = getAttr(attrs ?? '', 'id');
85
- const text = stripTags(content ?? '');
86
- if (id && text)
87
- out.push({ id, text, level: Number(tag[1]) });
88
- }
89
- return out;
82
+ return headingOutline(html).filter(h => h.id);
90
83
  }
91
84
  /**
92
85
  * Every heading in `html`, in document order, whether or not it carries an id —
@@ -3,7 +3,7 @@ export type SourceListItem = string | {
3
3
  name?: string;
4
4
  url?: string;
5
5
  };
6
- export interface SourceEntry {
6
+ interface SourceEntry {
7
7
  lastChecked?: string | null;
8
8
  note?: string;
9
9
  list?: SourceListItem[];
@@ -13,11 +13,11 @@ export type BacklogEntry = string | {
13
13
  item: string;
14
14
  status?: string;
15
15
  };
16
- export type FeedbackEntry = string | {
16
+ type FeedbackEntry = string | {
17
17
  date?: string;
18
18
  note: string;
19
19
  };
20
- export interface RunEntry {
20
+ interface RunEntry {
21
21
  date?: string;
22
22
  category?: string;
23
23
  summary?: string;
@@ -55,3 +55,4 @@ export declare function render(state: LogState, routine: string, newId?: () => s
55
55
  type: "content";
56
56
  text: string;
57
57
  }[];
58
+ export {};
package/dist/maps.js CHANGED
@@ -54,7 +54,13 @@ export function toMapEmbedUrl(url) {
54
54
  if (/google\.[a-z.]+\/maps/.test(url)) {
55
55
  const place = url.match(/\/place\/([^/@]+)/);
56
56
  if (place) {
57
- const q = encodeURIComponent(decodeURIComponent(place[1]).replace(/\+/g, ' '));
57
+ // A stray `%` makes decodeURIComponent throw; the segment is then used as pasted.
58
+ let name = place[1];
59
+ try {
60
+ name = decodeURIComponent(name);
61
+ }
62
+ catch { }
63
+ const q = encodeURIComponent(name.replace(/\+/g, ' '));
58
64
  return `https://maps.google.com/maps?q=${q}&output=embed`;
59
65
  }
60
66
  }
@@ -1,3 +1,8 @@
1
+ /**
2
+ * One pass, so a decoded `&` never starts a second entity (`&#38;lt;` is the
3
+ * text `&lt;`), and a numeric reference past U+10FFFF stays as written rather
4
+ * than throwing out of the twin's route.
5
+ */
1
6
  export declare function decodeEntities(s: string): string;
2
7
  /** Inline-level HTML → markdown. Applied inside cells, list items, headings. */
3
8
  export declare function inlineToMarkdown(html: string): string;
package/dist/markdown.js CHANGED
@@ -37,11 +37,18 @@ const NAMED_ENTITIES = {
37
37
  mu: 'μ', nu: 'ν', pi: 'π', rho: 'ρ', sigma: 'σ', tau: 'τ', phi: 'φ', omega: 'ω',
38
38
  Delta: 'Δ', Sigma: 'Σ', Omega: 'Ω', Lambda: 'Λ', Phi: 'Φ', Pi: 'Π',
39
39
  };
40
+ /**
41
+ * One pass, so a decoded `&` never starts a second entity (`&#38;lt;` is the
42
+ * text `&lt;`), and a numeric reference past U+10FFFF stays as written rather
43
+ * than throwing out of the twin's route.
44
+ */
40
45
  export function decodeEntities(s) {
41
- return s
42
- .replace(/&#x([0-9a-fA-F]{1,6});/g, (_m, hex) => String.fromCodePoint(parseInt(hex, 16)))
43
- .replace(/&#(\d{1,7});/g, (_m, dec) => String.fromCodePoint(Number(dec)))
44
- .replace(/&([a-zA-Z][a-zA-Z0-9]{1,9});/g, (m, name) => NAMED_ENTITIES[name] ?? m);
46
+ return s.replace(/&(?:#x([0-9a-fA-F]{1,6})|#(\d{1,7})|([a-zA-Z][a-zA-Z0-9]{1,9}));/g, (m, hex, dec, name) => {
47
+ if (name)
48
+ return NAMED_ENTITIES[name] ?? m;
49
+ const code = hex ? parseInt(hex, 16) : Number(dec);
50
+ return code <= 0x10ffff ? String.fromCodePoint(code) : m;
51
+ });
45
52
  }
46
53
  /** Inline-level HTML → markdown. Applied inside cells, list items, headings. */
47
54
  export function inlineToMarkdown(html) {
@@ -71,7 +78,7 @@ export function tableToMarkdown(html) {
71
78
  const pad = (r) => [...r, ...Array(width - r.length).fill('')];
72
79
  // A table whose first row is all <th> has a header; otherwise synthesise an
73
80
  // empty one, since GFM has no headerless table form.
74
- const headed = /<th\b/i.test(html.slice(0, html.indexOf('</tr>') + 1));
81
+ const headed = /<th\b/i.test(html.slice(0, html.search(/<\/tr>/i) + 1));
75
82
  const [head, ...body] = headed
76
83
  ? [pad(rows[0]), ...rows.slice(1).map(pad)]
77
84
  : [Array(width).fill(''), ...rows.map(pad)];
@@ -83,6 +90,8 @@ export function tableToMarkdown(html) {
83
90
  }
84
91
  // The spaces either side go with it: at a block boundary they are not text.
85
92
  const BLOCK_TOKEN = /[ \t]*\u0000(\d+)\u0000[ \t]*/g;
93
+ /** A list that holds no other list. */
94
+ const LIST = /<(ul|ol)\b[^>]*>((?:(?!<(?:ul|ol)\b)[\s\S])*?)<\/\1>/gi;
86
95
  /** Block-level HTML → markdown. */
87
96
  export function htmlToMarkdown(html) {
88
97
  let out = html;
@@ -102,21 +111,26 @@ export function htmlToMarkdown(html) {
102
111
  ].join('\n');
103
112
  // Tables first — their inner markup must not be eaten by the generic rules.
104
113
  out = out.replace(/<table\b[^>]*>[\s\S]*?<\/table>/gi, m => block(tableToMarkdown(m)));
105
- // Lists. `<ol>` numbering is computed per-list, which is why this cannot be a
106
- // single regex with a `$1` backreference — inside a replace callback `$1` is
107
- // a literal, not a substitution. (That exact bug shipped once and rendered
108
- // every ordered list as a column of `1. $1`.)
109
- out = out.replace(/<ul\b[^>]*>([\s\S]*?)<\/ul>/gi, (_m, body) => {
110
- const items = [...body.matchAll(/<li\b[^>]*>([\s\S]*?)<\/li>/gi)].map(i => listItem('-', i[1]));
111
- return block(items.join('\n'));
112
- });
113
- out = out.replace(/<ol\b[^>]*>([\s\S]*?)<\/ol>/gi, (_m, body) => {
114
- const items = [...body.matchAll(/<li\b[^>]*>([\s\S]*?)<\/li>/gi)].map((i, n) => listItem(`${n + 1}.`, i[1]));
115
- return block(items.join('\n'));
116
- });
114
+ // Lists, innermost first: a list holding no other list converts, and the
115
+ // pass repeats until none is left, so a list nested in one of its own kind
116
+ // is a block before its parent reads it. A lazy match from the outer `<ul>`
117
+ // ended at the inner `</ul>` and ran both lists onto one line. `<ol>`
118
+ // numbering is computed per-list, which is why this cannot be a single regex
119
+ // with a `$1` backreference — inside a replace callback `$1` is a literal,
120
+ // not a substitution. (That exact bug shipped once and rendered every
121
+ // ordered list as a column of `1. $1`.)
122
+ for (let prev = ''; prev !== out;) {
123
+ prev = out;
124
+ out = out.replace(LIST, (_m, tag, body) => {
125
+ const ordered = tag.toLowerCase() === 'ol';
126
+ const items = [...body.matchAll(/<li\b[^>]*>([\s\S]*?)<\/li>/gi)].map((i, n) => listItem(ordered ? `${n + 1}.` : '-', i[1]));
127
+ return block(items.join('\n'));
128
+ });
129
+ }
117
130
  out = out
118
- .replace(/<pre\b[^>]*>\s*<code\b[^>]*>([\s\S]*?)<\/code>\s*<\/pre>/gi, (_m, code) => block(`\`\`\`\n${decodeEntities(code).trim()}\n\`\`\``))
119
- .replace(/<pre\b[^>]*>([\s\S]*?)<\/pre>/gi, (_m, code) => block(`\`\`\`\n${decodeEntities(code).trim()}\n\`\`\``))
131
+ .replace(
132
+ // `<code>` is optional: a stored block may be `<pre><code>` or a bare `<pre>`.
133
+ /<pre\b[^>]*>\s*(?:<code\b[^>]*>)?([\s\S]*?)(?:<\/code>\s*)?<\/pre>/gi, (_m, code) => block(`\`\`\`\n${decodeEntities(code).trim()}\n\`\`\``))
120
134
  .replace(/<blockquote\b[^>]*>([\s\S]*?)<\/blockquote>/gi, (_m, body) => block(inlineToMarkdown(body)
121
135
  .split('\n')
122
136
  .map(l => `> ${l}`)
package/dist/mcp.d.ts CHANGED
@@ -105,6 +105,22 @@ export interface McpTool {
105
105
  };
106
106
  handler: (args: Record<string, unknown>, ctx: ToolContext) => Promise<unknown>;
107
107
  }
108
+ /**
109
+ * A tool as every surface advertises it — `tools/list`, the agent card and
110
+ * `/.well-known/mcp.json`. The handler and the A2A skill stay behind.
111
+ *
112
+ * One projection because the two copies drifted: the manifest's had no
113
+ * `outputSchema`, so a descriptor said a tool answers with prose while
114
+ * `tools/list` said it answers with `structuredContent`.
115
+ */
116
+ export declare function toolCard(tool: {
117
+ name: string;
118
+ title?: string;
119
+ description: string;
120
+ inputSchema: unknown;
121
+ outputSchema?: unknown;
122
+ annotations?: unknown;
123
+ }): Record<string, unknown>;
108
124
  export interface McpResource {
109
125
  uri: string;
110
126
  name: string;
package/dist/mcp.js CHANGED
@@ -67,6 +67,24 @@ function requestProtocol(request) {
67
67
  /** JSON-RPC batching was removed in 2025-06-18 and stays removed; it is legal only below that. */
68
68
  const allowsBatch = (v) => v === '2025-03-26' || v === '2024-11-05';
69
69
  import { clientKey, rateLimit, rateLimitHeaders, withRateLimit } from './rate-limit.js';
70
+ /**
71
+ * A tool as every surface advertises it — `tools/list`, the agent card and
72
+ * `/.well-known/mcp.json`. The handler and the A2A skill stay behind.
73
+ *
74
+ * One projection because the two copies drifted: the manifest's had no
75
+ * `outputSchema`, so a descriptor said a tool answers with prose while
76
+ * `tools/list` said it answers with `structuredContent`.
77
+ */
78
+ export function toolCard(tool) {
79
+ return {
80
+ name: tool.name,
81
+ ...(tool.title ? { title: tool.title } : {}),
82
+ description: tool.description,
83
+ inputSchema: tool.inputSchema,
84
+ ...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}),
85
+ ...(tool.annotations ? { annotations: tool.annotations } : {}),
86
+ };
87
+ }
70
88
  /**
71
89
  * A caller-fixable failure inside a handler (page not found, empty input).
72
90
  * Reported as a tool result with `isError` so the model sees the text and can
@@ -151,6 +169,15 @@ function typeOf(v) {
151
169
  return 'null';
152
170
  return typeof v;
153
171
  }
172
+ /** A JSON-RPC success envelope. The error twin is `rpcError` below. */
173
+ const ok = (id, result) => ({ jsonrpc: '2.0', id, result });
174
+ /**
175
+ * The -32602 a lookup miss answers with: what was asked for, then everything
176
+ * this server does expose, as prose one retry can act on.
177
+ */
178
+ const notFound = (id, lead, kind, names, dataKey) => rpcError(id, -32602, `${lead} This server exposes: ${names.length ? quote(names) : `no ${kind}`}.`, {
179
+ [dataKey]: names,
180
+ });
154
181
  const rpcError = (id, code, message, data) => ({
155
182
  jsonrpc: '2.0',
156
183
  id,
@@ -265,6 +292,13 @@ const capabilitiesOf = (config) => ({
265
292
  ...(config.prompts?.length ? { prompts: {} } : {}),
266
293
  });
267
294
  async function handleRpc(req, config, dispatch) {
295
+ // The body is the caller's JSON, typed or not: a `null` in a batch would
296
+ // throw here and take the whole response with it, CORS headers included.
297
+ const entry = req;
298
+ if (!entry || typeof entry !== 'object' || typeof entry.method !== 'string') {
299
+ const id = entry?.id;
300
+ return rpcError(typeof id === 'string' || typeof id === 'number' ? id : null, -32600, 'Invalid Request: expected a JSON-RPC object with a string "method".');
301
+ }
268
302
  const { id, method, params } = req;
269
303
  const p = (params ?? {});
270
304
  const resources = config.resources ?? [];
@@ -277,104 +311,60 @@ async function handleRpc(req, config, dispatch) {
277
311
  try {
278
312
  switch (method) {
279
313
  case 'initialize':
280
- return {
281
- jsonrpc: '2.0',
282
- id,
283
- result: {
284
- protocolVersion: negotiateProtocol(p.protocolVersion),
285
- capabilities: capabilitiesOf(config),
286
- serverInfo: config.serverInfo,
287
- instructions: config.instructions,
288
- },
289
- };
314
+ return ok(id, {
315
+ protocolVersion: negotiateProtocol(p.protocolVersion),
316
+ capabilities: capabilitiesOf(config),
317
+ serverInfo: config.serverInfo,
318
+ instructions: config.instructions,
319
+ });
290
320
  // What `initialize` tells a legacy client, asked the modern way. The
291
321
  // transport adds `resultType`, the cache hints and `_meta` serverInfo.
292
322
  case 'server/discover':
293
- return {
294
- jsonrpc: '2.0',
295
- id,
296
- result: {
297
- supportedVersions: [...MCP_PROTOCOL_VERSIONS],
298
- capabilities: capabilitiesOf(config),
299
- instructions: config.instructions,
300
- },
301
- };
323
+ return ok(id, {
324
+ supportedVersions: [...MCP_PROTOCOL_VERSIONS],
325
+ capabilities: capabilitiesOf(config),
326
+ instructions: config.instructions,
327
+ });
302
328
  // Notifications have no response at all.
303
329
  case 'notifications/initialized':
304
330
  case 'notifications/cancelled':
305
331
  return null;
306
332
  case 'ping':
307
- return { jsonrpc: '2.0', id, result: {} };
333
+ return ok(id, {});
308
334
  case 'tools/list':
309
- return {
310
- jsonrpc: '2.0',
311
- id,
312
- result: {
313
- tools: config.tools.map(({ name, title, description, inputSchema, outputSchema, annotations }) => ({
314
- name,
315
- ...(title ? { title } : {}),
316
- description,
317
- inputSchema,
318
- ...(outputSchema ? { outputSchema } : {}),
319
- ...(annotations ? { annotations } : {}),
320
- })),
321
- },
322
- };
335
+ return ok(id, { tools: config.tools.map(toolCard) });
323
336
  case 'resources/list':
324
- return {
325
- jsonrpc: '2.0',
326
- id,
327
- result: {
328
- resources: resources.map(({ uri, name, description, mimeType }) => ({
329
- uri,
330
- name,
331
- description,
332
- mimeType,
333
- })),
334
- },
335
- };
337
+ return ok(id, {
338
+ resources: resources.map(({ uri, name, description, mimeType }) => ({ uri, name, description, mimeType })),
339
+ });
336
340
  // Declared `resources` makes a client ask for templates too. There are
337
341
  // none to give — every resource here is a fixed URI — but an empty list
338
342
  // is the answer to that question, where -32601 reads as a broken server.
339
343
  case 'resources/templates/list':
340
- return { jsonrpc: '2.0', id, result: { resourceTemplates: [] } };
344
+ return ok(id, { resourceTemplates: [] });
341
345
  case 'resources/read': {
342
346
  const { uri } = p;
343
347
  const resource = uri ? resources.find(r => r.uri === uri) : undefined;
344
348
  const content = resource ? await resource.read() : null;
345
349
  if (content == null) {
346
- const uris = resources.map(r => r.uri);
347
350
  const lead = uri ? `Unknown resource "${uri}".` : 'Missing "uri" in params.';
348
- return rpcError(id, -32602, `${lead} This server exposes: ${uris.length ? quote(uris) : 'no resources'}.`, { availableResources: uris });
351
+ return notFound(id, lead, 'resources', resources.map(r => r.uri), 'availableResources');
349
352
  }
350
- return {
351
- jsonrpc: '2.0',
352
- id,
353
- result: { contents: [{ uri, mimeType: resource.mimeType, text: content }] },
354
- };
353
+ return ok(id, { contents: [{ uri, mimeType: resource.mimeType, text: content }] });
355
354
  }
356
355
  case 'prompts/list':
357
- return {
358
- jsonrpc: '2.0',
359
- id,
360
- result: { prompts: prompts.map(({ name, description }) => ({ name, description })) },
361
- };
356
+ return ok(id, { prompts: prompts.map(({ name, description }) => ({ name, description })) });
362
357
  case 'prompts/get': {
363
358
  const { name } = p;
364
359
  const prompt = name ? prompts.find(x => x.name === name) : undefined;
365
360
  if (!prompt) {
366
- const names = prompts.map(x => x.name);
367
361
  const lead = name ? `Unknown prompt "${name}".` : 'Missing "name" in params.';
368
- return rpcError(id, -32602, `${lead} This server exposes: ${names.length ? quote(names) : 'no prompts'}.`, { availablePrompts: names });
362
+ return notFound(id, lead, 'prompts', prompts.map(x => x.name), 'availablePrompts');
369
363
  }
370
- return {
371
- jsonrpc: '2.0',
372
- id,
373
- result: {
374
- description: prompt.description,
375
- messages: [{ role: 'user', content: { type: 'text', text: prompt.text } }],
376
- },
377
- };
364
+ return ok(id, {
365
+ description: prompt.description,
366
+ messages: [{ role: 'user', content: { type: 'text', text: prompt.text } }],
367
+ });
378
368
  }
379
369
  case 'tools/call': {
380
370
  const { name, arguments: rawArgs = {} } = p;
@@ -42,20 +42,3 @@ export declare function listEnvelope<T>(pages: T[], total: number, page: number,
42
42
  note?: string;
43
43
  pages: T[];
44
44
  };
45
- /**
46
- * The entries either side of the current one in an already-ordered list.
47
- *
48
- * The ordering is the caller's, deliberately — it is the one thing here that is
49
- * never portable. A wiki's sequence is its section's configured sort, a
50
- * knowledge base's is a taxonomy walk, and a company log's is `updatedAt` desc.
51
- *
52
- * `null` on both sides when the page is not in the list, so a page reached by a
53
- * URL its own section does not list renders no nav rather than a wrong one.
54
- *
55
- * No query. Callers already hold the ordered siblings for something else, a
56
- * related-pages panel or a section listing, so the neighbours cost no lookups.
57
- */
58
- export declare function adjacentPages<T>(ordered: readonly T[], isCurrent: (page: T) => boolean): {
59
- prev: T | null;
60
- next: T | null;
61
- };
@@ -52,22 +52,3 @@ export function listEnvelope(pages, total, page, pageSize, emptyNote) {
52
52
  pages,
53
53
  };
54
54
  }
55
- /**
56
- * The entries either side of the current one in an already-ordered list.
57
- *
58
- * The ordering is the caller's, deliberately — it is the one thing here that is
59
- * never portable. A wiki's sequence is its section's configured sort, a
60
- * knowledge base's is a taxonomy walk, and a company log's is `updatedAt` desc.
61
- *
62
- * `null` on both sides when the page is not in the list, so a page reached by a
63
- * URL its own section does not list renders no nav rather than a wrong one.
64
- *
65
- * No query. Callers already hold the ordered siblings for something else, a
66
- * related-pages panel or a section listing, so the neighbours cost no lookups.
67
- */
68
- export function adjacentPages(ordered, isCurrent) {
69
- const i = ordered.findIndex(isCurrent);
70
- if (i < 0)
71
- return { prev: null, next: null };
72
- return { prev: ordered[i - 1] ?? null, next: ordered[i + 1] ?? null };
73
- }
@@ -195,40 +195,6 @@ export declare function Breadcrumbs({ items, base, className, link: Link, lastAs
195
195
  export declare function BreadcrumbsRow({ actions, className, ...props }: BreadcrumbsProps & {
196
196
  actions?: ReactNode;
197
197
  }): import("react").JSX.Element;
198
- /** One end of the foot nav: where it goes and what it is called. */
199
- export interface PageNavRef {
200
- title: string;
201
- href: string;
202
- }
203
- export interface PageNavProps {
204
- prev?: PageNavRef | null;
205
- next?: PageNavRef | null;
206
- link?: WikiLinkComponent;
207
- /** The landmark's accessible name. */
208
- label?: string;
209
- prevLabel?: string;
210
- nextLabel?: string;
211
- /**
212
- * The arrows. `ReactNode` rather than a string because both wikis draw them
213
- * with an icon component, and this package is not acquiring an icon peer to
214
- * hold two glyphs.
215
- */
216
- prevGlyph?: ReactNode;
217
- nextGlyph?: ReactNode;
218
- }
219
- /**
220
- * The sequential read at the foot of an article — the move the infobox rail's
221
- * lateral links do not cover.
222
- *
223
- * Ordering is entirely the caller's; pair this with `adjacentPages` from
224
- * `wiki-formant/pagination` over a list you already hold.
225
- *
226
- * The `page-nav__*` class names are NOT props: they are the convention every
227
- * stylesheet implements, and making them configurable is how a convention
228
- * forks. The empty `<div>` holds the first article's left column so the next
229
- * link stays in the right one.
230
- */
231
- export declare function PageNav({ prev, next, link: Link, label, prevLabel, nextLabel, prevGlyph, nextGlyph, }: PageNavProps): import("react").JSX.Element | null;
232
198
  export interface InfoboxAsideProps {
233
199
  /** The landmark's accessible name, e.g. `Key facts about ${title}`. */
234
200
  label: string;
@@ -148,23 +148,6 @@ export function Breadcrumbs({ items, base, className = '', link: Link = Anchor,
148
148
  export function BreadcrumbsRow({ actions, className = '', ...props }) {
149
149
  return (_jsxs("div", { className: `breadcrumbs-row ${actions ? 'spread' : ''} ${className}`.trim(), children: [_jsx(Breadcrumbs, { ...props }), actions] }));
150
150
  }
151
- /**
152
- * The sequential read at the foot of an article — the move the infobox rail's
153
- * lateral links do not cover.
154
- *
155
- * Ordering is entirely the caller's; pair this with `adjacentPages` from
156
- * `wiki-formant/pagination` over a list you already hold.
157
- *
158
- * The `page-nav__*` class names are NOT props: they are the convention every
159
- * stylesheet implements, and making them configurable is how a convention
160
- * forks. The empty `<div>` holds the first article's left column so the next
161
- * link stays in the right one.
162
- */
163
- export function PageNav({ prev, next, link: Link = Anchor, label = 'Article navigation', prevLabel = 'Previous', nextLabel = 'Next', prevGlyph = '\u2190', nextGlyph = '\u2192', }) {
164
- if (!prev && !next)
165
- return null;
166
- return (_jsxs("nav", { className: "page-nav", "aria-label": label, children: [prev ? (_jsxs(Link, { href: prev.href, className: "page-nav-link", rel: "prev", children: [_jsxs("span", { className: "page-nav-label", children: [prevGlyph, prevLabel] }), _jsx("span", { className: "page-nav-title", children: prev.title })] })) : (_jsx("div", {})), next && (_jsxs(Link, { href: next.href, className: "page-nav-link page-nav-link--end", rel: "next", children: [_jsxs("span", { className: "page-nav-label page-nav-label--end", children: [nextLabel, nextGlyph] }), _jsx("span", { className: "page-nav-title", children: next.title })] }))] }));
167
- }
168
151
  /**
169
152
  * The facts panel beside an article. Named, because two of the three asides
170
153
  * this replaced had no accessible name, and a complementary landmark without
@@ -1,5 +1,6 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import type { Digest } from './analytics.js';
3
+ export declare const count: (n: number) => string;
3
4
  /**
4
5
  * The figures as `.stats-figures button[aria-pressed]`, each with its change
5
6
  * as `.stats-delta[data-better]` for a stylesheet to colour, and the pressed one per
@@ -8,7 +8,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
8
8
  import { startAuthentication, startRegistration } from '@simplewebauthn/browser';
9
9
  import { useMemo, useState } from 'react';
10
10
  import { TimeChart, stepDecimals } from './chart.js';
11
- const count = (n) => n.toLocaleString('en');
11
+ export const count = (n) => n.toLocaleString('en');
12
12
  // Whole seconds and percentages, unless an axis tick's step needs more to tell it from the next.
13
13
  const places = (step) => (step ? stepDecimals(step) : 0);
14
14
  const duration = (s, step) => (s < 60 ? `${s.toFixed(places(step))}s` : `${Math.floor(s / 60)}m ${Math.round(s % 60)}s`);
package/dist/stats.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { STATS_FILTERS } from './analytics.js';
3
- import { PasskeyButton, PasskeySignOut, StatsFigures } from './stats-figures.js';
3
+ import { PasskeyButton, PasskeySignOut, StatsFigures, count } from './stats-figures.js';
4
4
  /** The windows the stats page offers, in days; `Infinity` is all time. */
5
5
  const STATS_RANGES = [1, 7, 30, 90, 365, Infinity];
6
6
  /** A window as its `?days=` value. */
@@ -31,7 +31,6 @@ const href = (days, filter) => {
31
31
  q.set(k, filter[k]);
32
32
  return `?${q}`;
33
33
  };
34
- const count = (n) => n.toLocaleString('en');
35
34
  const regions = new Intl.DisplayNames(['en'], { type: 'region' });
36
35
  const country = (code) => {
37
36
  try {
package/dist/taxonomy.js CHANGED
@@ -37,7 +37,11 @@ export function hrefBuilder(prefix = '') {
37
37
  /** Categories mounted at the root. */
38
38
  export const defaultHref = hrefBuilder();
39
39
  const DEFAULT_ALPHA_INDEX_MIN_PAGES = 40;
40
- const metaValue = (page, key) => (page.metadata?.[key] ?? '').trim();
40
+ /** A key's value as text. Metadata is untyped JSON, and a number or a list stored under a key must not throw. */
41
+ const metaValue = (page, key) => {
42
+ const value = page.metadata?.[key];
43
+ return typeof value === 'string' ? value.trim() : typeof value === 'number' ? String(value) : '';
44
+ };
41
45
  /** Bucket for titles that don't start with a letter (numerals, `$CAPER`). @internal */
42
46
  export function firstLetter(title) {
43
47
  const ch = title.trim()[0]?.toUpperCase() ?? '';
package/dist/tiptap.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { Extension, Node as TiptapNode } from '@tiptap/core';
3
- /** Local `cn`. Both wikis import one; the package will not depend on one. */
4
3
  /**
5
4
  * An embed's address, if it is https on a host the sanitiser and the CSP
6
5
  * admit; otherwise null, and the node draws an empty frame. The editor draws
package/dist/tiptap.js CHANGED
@@ -23,7 +23,6 @@ import { useClickOutside } from './react.js';
23
23
  import { cx } from './html.js';
24
24
  import { slugifyHeading, uniqueHeadingId } from './headings.js';
25
25
  import { CODE_LANGS, DEFAULT_LANG } from './blocks.js';
26
- /** Local `cn`. Both wikis import one; the package will not depend on one. */
27
26
  // ---- iframe -----------------------------------------------------------------
28
27
  /**
29
28
  * An embed's address, if it is https on a host the sanitiser and the CSP
@@ -56,7 +56,7 @@ export function validateLinkGroups(groups) {
56
56
  Array.isArray(g.links) &&
57
57
  g.links.every(l => isRecord(l) && typeof l.label === 'string' && okUrl(l.href))));
58
58
  }
59
- /** `[{ label, language?, code }]` — a code-tabs block's tabs. `code` renders as HTML. @internal */
59
+ /** `[{ label, language?, code }]` — a code-tabs block's tabs. `code` is source text, escaped when it renders. @internal */
60
60
  export function validateCodeTabs(tabs) {
61
61
  return (Array.isArray(tabs) &&
62
62
  tabs.every(t => isRecord(t) &&
@@ -10,4 +10,3 @@ export declare function formatVersion(version: SemVer): string;
10
10
  export declare function incrementVersion(version: SemVer, changeType: ChangeType): SemVer;
11
11
  /** Bump a version string in one step — the call site almost always wants this. */
12
12
  export declare function bump(version: string | null | undefined, changeType: ChangeType): string;
13
- export declare function compareVersions(a: string, b: string): number;
@@ -33,8 +33,3 @@ export function incrementVersion(version, changeType) {
33
33
  export function bump(version, changeType) {
34
34
  return formatVersion(incrementVersion(parseVersion(version), changeType));
35
35
  }
36
- export function compareVersions(a, b) {
37
- const x = parseVersion(a);
38
- const y = parseVersion(b);
39
- return x.major - y.major || x.minor - y.minor || x.patch - y.patch;
40
- }
@@ -110,6 +110,7 @@ export interface ManifestTool {
110
110
  title?: string;
111
111
  description: string;
112
112
  inputSchema: unknown;
113
+ outputSchema?: unknown;
113
114
  annotations?: unknown;
114
115
  }
115
116
  export interface McpManifestConfig {
@@ -9,7 +9,7 @@
9
9
  // Nothing here touches a framework: these return plain values, and the caller
10
10
  // wraps them in whatever its router wants. That keeps this package free of a
11
11
  // Next dependency and keeps each app's route a two-liner.
12
- import { MCP_PROTOCOL_VERSION, MCP_PROTOCOL_VERSIONS } from './mcp.js';
12
+ import { MCP_PROTOCOL_VERSION, MCP_PROTOCOL_VERSIONS, toolCard } from './mcp.js';
13
13
  import { MCP_RATE_LIMIT_TEXT } from './rate-limit.js';
14
14
  import { descriptorResponse } from './http.js';
15
15
  /**
@@ -195,13 +195,7 @@ export function mcpManifest(config) {
195
195
  rateLimit: MCP_RATE_LIMIT_TEXT,
196
196
  },
197
197
  ...extra,
198
- tools: tools.map(({ name, title, description, inputSchema, annotations }) => ({
199
- name,
200
- ...(title ? { title } : {}),
201
- description,
202
- inputSchema,
203
- ...(annotations ? { annotations } : {}),
204
- })),
198
+ tools: tools.map(toolCard),
205
199
  };
206
200
  }
207
201
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-formant",
3
- "version": "0.63.1",
3
+ "version": "0.64.0",
4
4
  "description": "The portable half of a wiki: derived taxonomy and facet controls, a version-negotiating MCP transport, markdown twins, block rendering, a rich-text editor engine, conditional-GET plumbing, and the headers and CSP every origin sends.",
5
5
  "type": "module",
6
6
  "license": "MIT",