wiki-formant 0.73.0 → 0.76.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 +7 -12
- package/dist/base.css +38 -0
- package/dist/block-editor.d.ts +27 -0
- package/dist/block-editor.js +34 -4
- package/dist/blocks.d.ts +31 -0
- package/dist/blocks.js +6 -1
- package/dist/conformance.d.ts +0 -15
- package/dist/conformance.js +0 -88
- package/dist/mcp.d.ts +0 -12
- package/dist/mcp.js +0 -22
- package/dist/react-server.d.ts +52 -0
- package/dist/react-server.js +43 -0
- package/dist/validation.d.ts +3 -2
- package/dist/validation.js +11 -2
- package/dist/well-known.d.ts +0 -7
- package/dist/well-known.js +0 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -213,12 +213,6 @@ What it gets right:
|
|
|
213
213
|
- **The rate limit is declared, not wired.** Put `rateLimit: {capacity, refillPerSec}` on the config and `mcpResponse` enforces it before parsing the body, refuses with a JSON-RPC envelope, and states `RateLimit-Limit`/`-Remaining`/`-Reset` on every answer. Four routes had each transcribed that by hand through three differently-named local helpers, which is how a headroom header gets added to one surface and forgotten on the next.
|
|
214
214
|
- **`mcpRateLimited(retryAfterSec)` is a JSON-RPC envelope.** A 429 whose body is `{"error": "..."}` is a string where the client's parser expects `{code, message}`, on the one response an agent meets exactly when it is working hard.
|
|
215
215
|
|
|
216
|
-
### A read-only surface
|
|
217
|
-
|
|
218
|
-
`readOnly(config, hide?)` is the same server with its writes taken off: the tools marked `readOnlyHint: true` and not named in `hide`, with every instruction line and prompt that names a dropped tool dropped too, and `destructiveHint` and `openWorldHint` stated on each tool that is kept. It exists for ChatGPT plugins, whose review refuses crypto transfers and checkout links and whose reviewers cannot sign a ROLA challenge. Mount it at its own route (`mcpResponse(req, readOnly(config))`), so the main endpoint keeps its writes, and hold the plugin package to it with `pluginParity` (Conformance). Keep each instruction a line of its own, so a line that names a write takes nothing else with it.
|
|
219
|
-
|
|
220
|
-
`openaiChallengeHandler()` from `wiki-formant/well-known` serves `/.well-known/openai-apps-challenge`: the portal's domain token from `OPENAI_APPS_CHALLENGE`, as bare text, or a 404 while it is unset.
|
|
221
|
-
|
|
222
216
|
### Structured output, `_meta`, and the envelope gate
|
|
223
217
|
|
|
224
218
|
**Every object a handler returns comes back as `structuredContent` beside the text block**, so a client reads the answer rather than scraping prose for it. This used to be gated on declaring an `outputSchema`, and the result was that across four live servers and thirty-two tools — every one of them answering in JSON — not a single response ever carried it. `outputSchema` remains the stronger contract, because a client validates against it; it is no longer the price of admission. A handler returning a string is left alone.
|
|
@@ -266,7 +260,6 @@ The drift checks hold an MCP server to what its own site does, so a tool cannot
|
|
|
266
260
|
- `roundTrip(t, label, { read, write })`: what a read tool hands out, the write tool takes back unchanged, and the next read matches.
|
|
267
261
|
- `openapiWriteParity(t, [[path, method, tool], …])`: every field an OpenAPI write documents, the matching tool takes.
|
|
268
262
|
- `rolaOriginCheck(t)`: a challenge tool's ROLA recipe names the origin under test.
|
|
269
|
-
- `pluginParity(t, { plugin, mcp })`: a ChatGPT plugin package (`chatgpt/plugin.json` and `chatgpt/mcp.json`) agrees with the endpoint it names, rebased onto the origin under test: one streamable-HTTP server that answers; every tool a review case triggers is listed there; every tool states `readOnlyHint`, `destructiveHint` and `openWorldHint` as booleans; `capabilities` claims `Write` exactly when a write tool is listed; no instruction, description or prompt names a tool the main endpoint has and the plugin's lacks; the listing is within the portal's limits with five positive and three negative cases; and each listing page on the site's own origin answers, with its `#anchor` where it names one. The demo recording is the one review field it cannot check.
|
|
270
263
|
|
|
271
264
|
`standardVersionSources(t, openapiPaths)` is the `versionCoherence` map every S10 surface serves — both agent-card paths, `mcp.json`, the server card and each path the OpenAPI document answers at — so a suite names only the paths it adds. `t.rpcAt(endpoint, …)` and `t.callAt` drive a second MCP endpoint on the same surface with the same backoff, User-Agent and tally.
|
|
272
265
|
|
|
@@ -467,7 +460,7 @@ are unit-tested without a DOM.
|
|
|
467
460
|
`FacetBar` renders the rows `createTaxonomy` already produces. This is why the taxonomy exports a rows model rather than markup: the rows could always cross the boundary and, until this subpath existed, the markup could not, so all three wikis hand-rendered it and two put `aria-pressed` on an `<a>`. `Breadcrumbs` renders the trail and its `BreadcrumbList` JSON-LD together, because a trail whose structured data is written somewhere else is a trail that will one day disagree with its own markup — which is the case Google penalises. It takes a `base` origin: structured-data URLs must be absolute and a package cannot know the site.
|
|
468
461
|
`FacetBar` takes a `count` class to render each count as its own element, and `alphaFirst` to lead with the A–Z row. A chip row above the grid and a panel beside it are the same markup laid out by different stylesheets, so the class names are what tell them apart: `letterRow`, `letterControl` and `letterControlActive` style the A–Z row apart from the facet rows (each falls back to `row`, `control` or `controlActive`); a `text` class wraps each label in its own element and gives a facet option its value as `title`, so a long value can truncate while its count stays in view; and `letterCounts="title"` keeps each letter a bare glyph with its count in the tooltip. With none of them set the markup is what it was. Its JSON-LD goes out through `JsonLd`, which is exported for every other payload a page emits: it re-encodes each `<` as `\u003c`, because these payloads carry authored titles and an authored `</script>` would otherwise close the tag.
|
|
469
462
|
|
|
470
|
-
`
|
|
463
|
+
`Pager` is the "← Previous · Page 2 of 9 · Next →" line, for paged results and for a carousel. Pass `href` and the steps are links (pair it with `paginatedResponse`'s `page` and `totalPages`); pass `onPage` and they are buttons, `wrap` stepping round from either end as a snap row does. A step with nowhere to go stays in place, inert, so the count never jumps sideways, and `spread` puts the steps at the ends of the line. `noun` names what a page is ("Point 2 of 21", "Previous point"). `SnapDots` is a snap row's dots: with `onGo` (`useSnapRow`'s `go`) each is a named button, without it they only count and are hidden from assistive technology. Three sites had written the pager by hand, each stopping at the ends differently, and two had drawn their own dots around the shared hook. `base.css` lays both out; the colours are the site's, the dots' through `--snap-dot` and `--snap-dot-on`.
|
|
471
464
|
|
|
472
465
|
`RailShell` stays in `wiki-formant/react`, because it calls `useSidebar` and genuinely is a client component. It renders the rail's landmark, its scroll wrapper and the three collapse states — including the `--instant` class that keeps a remembered-closed rail from animating shut on first paint, and the close-on-navigate that a mobile drawer needs. Only a followed link closes it: a search field or a table-of-contents entry inside the drawer leaves it open. Compose your own rail inside it and mark the active link with `isRailLinkActive`; all three wikis do, and the pre-composed component that used to sit here had one consumer and had already lost both of those behaviours.
|
|
473
466
|
|
|
@@ -718,7 +711,7 @@ leafBlocks(blocks, BLOCK_SHAPE.containers);
|
|
|
718
711
|
|
|
719
712
|
The dispatch over a repo's own block union stays in that repo; the bodies come from here. `coreAtomicText` has a prose body for every core leaf — the stats, link-grid and page-list bodies joined the originals when two of the three wikis turned out to extract nothing from them, so their MCP `get_page` answered a hub page as nearly empty.
|
|
720
713
|
|
|
721
|
-
The core leaves — `content`, `codeTabs`, `banner`, `references`, `stats`, `linkGrid`, `testimonial`, `recentPages`, `pageList` — had the same case bodies in every wiki's text export, markdown twin, validator and new-block record. A repo's switch hands those cases over and keeps its own, so it stays exhaustive over its union:
|
|
714
|
+
The core leaves — `content`, `codeTabs`, `banner`, `references`, `stats`, `linkGrid`, `testimonial`, `recentPages`, `pageList`, and `list`, which does what the two page lists do between them — had the same case bodies in every wiki's text export, markdown twin, validator and new-block record. A repo's switch hands those cases over and keeps its own, so it stays exhaustive over its union:
|
|
722
715
|
|
|
723
716
|
```ts
|
|
724
717
|
import { coreAtomicText, coreAtomicToMarkdown, coreBlockDefaults } from 'wiki-formant/blocks';
|
|
@@ -749,7 +742,7 @@ if (issues.length) return badRequest(`Invalid content: ${describeBlockIssues(iss
|
|
|
749
742
|
|
|
750
743
|
A `codeTabs` tab's `code` is source text in every wiki here, and every view treats it that way: `CodeTabsView` escapes it into a `<pre><code>`, the sanitiser leaves it alone, the markdown twin fences it verbatim. A wiki that escapes and highlights on the server passes `highlighted` to the view. The package used to assume editors stored highlighted markup; none ever had, and every twin printed `Vec<u8>` as `Vec`.
|
|
751
744
|
|
|
752
|
-
`recentPages` and `
|
|
745
|
+
`recentPages`, `pageList` and a `list` resolve to `ResolvedPageRef`s — build each with `pageRef(page, now)`, which carries the ISO date a `<time>` needs — and `PageRefsView` renders them as a list, with a `renderItem` slot for a wiki that has its own row.
|
|
753
746
|
|
|
754
747
|
## Dates
|
|
755
748
|
|
|
@@ -849,7 +842,7 @@ const SCHEMA: BlockSchema<Block> = {
|
|
|
849
842
|
<BlockEditor blocks={blocks} onChange={setBlocks} schema={SCHEMA} kit={KIT} />
|
|
850
843
|
```
|
|
851
844
|
|
|
852
|
-
The dispatch stays a `switch` over the wiki's own union, so a new type is a compile error there. `CodeTabsFields`, `StatsFields`, `ReferencesFields`, `LinkGridFields`, `BannerFields`, `PageListFields`, `ColumnsFields` and `InfoboxFields` take the core blocks `wiki-formant/blocks` declares; the two containers hold the same list, offering `nestedTypes`. `Field`, `Choice` and `LimitField` are exported for a wiki's own editors, so its native types wear the same kit. A card is marked `data-selected` while it is being edited and `data-nested` inside a container; a type the build does not know is shown by its raw type and carried through untouched. Keep the kit and the schema stable — a module constant or a `useMemo` — since both reach every card through context, and a fresh one re-renders every card on every keystroke.
|
|
845
|
+
The dispatch stays a `switch` over the wiki's own union, so a new type is a compile error there. `CodeTabsFields`, `StatsFields`, `ReferencesFields`, `LinkGridFields`, `BannerFields`, `PageListFields`, `ListFields`, `ColumnsFields` and `InfoboxFields` take the core blocks `wiki-formant/blocks` declares; the two containers hold the same list, offering `nestedTypes`. `Field`, `Choice` and `LimitField` are exported for a wiki's own editors, so its native types wear the same kit. A card is marked `data-selected` while it is being edited and `data-nested` inside a container; a type the build does not know is shown by its raw type and carried through untouched. Keep the kit and the schema stable — a module constant or a `useMemo` — since both reach every card through context, and a fresh one re-renders every card on every keystroke.
|
|
853
846
|
|
|
854
847
|
An editor that draws each block in place, with a list of its own (miow's), hosts the same fields in `<BlockFieldsKit kit={KIT}>`, without `<BlockEditor>`. Only the two container fields need the full editor, since they hold a block list.
|
|
855
848
|
|
|
@@ -857,7 +850,9 @@ A `testimonial` is `{ quote, author, role?, avatarUrl? }`, drawn by `Testimonial
|
|
|
857
850
|
|
|
858
851
|
A banner with no text of its own shows `BANNER_MESSAGES` from `wiki-formant/text`, a sentence per variant that names no wiki; `BannerView` takes it unless `message` overrides it.
|
|
859
852
|
|
|
860
|
-
The core blocks are `ContentBlock`, `CodeTabsBlock`, `StatsBlock`, `LinkGridBlock`, `ReferencesBlock`, `BannerBlock`, `TestimonialBlock`, `ColumnsBlock<A>` and `InfoboxBlock<A>`, `A` being the wiki's atomic union. A wiki builds its union from them beside its own `recentPages` and `pageList`, which hold its own page rows or a section, and its native types.
|
|
853
|
+
The core blocks are `ContentBlock`, `CodeTabsBlock`, `StatsBlock`, `LinkGridBlock`, `ReferencesBlock`, `BannerBlock`, `TestimonialBlock`, `ListBlock`, `ColumnsBlock<A>` and `InfoboxBlock<A>`, `A` being the wiki's atomic union. A wiki builds its union from them beside its own `recentPages` and `pageList`, which hold its own page rows or a section, and its native types.
|
|
854
|
+
|
|
855
|
+
`ListBlock` is one list of the wiki's pages where those two were two: the pages in a section (`source: 'under'`), the newest, in a section or across the wiki (`'newest'`, `count`), or pages picked by hand (`'chosen'`, `pages`, keyed as the wiki keys a page, an id or a path), shown as links with their age or as cards (`show`). A wiki resolves it to `items` as it does a page list. `ListFields` edits it, with `pages` to pick from and `sections` to name; a wiki with a source of its own, a shop's products say, passes it in `sources` and draws its fields as `children`.
|
|
861
856
|
|
|
862
857
|
## Analytics
|
|
863
858
|
|
package/dist/base.css
CHANGED
|
@@ -201,6 +201,44 @@ html[data-sidebar='closed'] .wiki-sidebar { width: 0; }
|
|
|
201
201
|
}
|
|
202
202
|
.breadcrumbs .separator { user-select: none; }
|
|
203
203
|
|
|
204
|
+
/* ---- Pager and SnapDots -----------------------------------------------------
|
|
205
|
+
One line that never wraps, the count in tabular figures so stepping does not
|
|
206
|
+
shift it. A step is a bare control: its colour, size and hit area are the
|
|
207
|
+
site's, set on `.pager-step`. The dots' colours are `--snap-dot` and
|
|
208
|
+
`--snap-dot-on`, defaulting to the text colour faint and full. */
|
|
209
|
+
.pager {
|
|
210
|
+
display: flex;
|
|
211
|
+
align-items: center;
|
|
212
|
+
justify-content: center;
|
|
213
|
+
gap: var(--pager-gap, 1rem);
|
|
214
|
+
white-space: nowrap;
|
|
215
|
+
font-variant-numeric: tabular-nums;
|
|
216
|
+
}
|
|
217
|
+
.pager[data-spread] { justify-content: space-between; gap: 0; }
|
|
218
|
+
.pager-step { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
|
|
219
|
+
.pager-step:is(:disabled, [aria-disabled='true']) { opacity: 0.4; cursor: default; }
|
|
220
|
+
|
|
221
|
+
.snap-dots { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--snap-dot-gap, 0); }
|
|
222
|
+
.snap-dots button {
|
|
223
|
+
display: grid;
|
|
224
|
+
place-items: center;
|
|
225
|
+
inline-size: var(--snap-dot-hit, 1.5rem);
|
|
226
|
+
block-size: var(--snap-dot-hit, 1.5rem);
|
|
227
|
+
padding: 0;
|
|
228
|
+
border: 0;
|
|
229
|
+
background: none;
|
|
230
|
+
cursor: pointer;
|
|
231
|
+
}
|
|
232
|
+
.snap-dots i {
|
|
233
|
+
display: block;
|
|
234
|
+
inline-size: var(--snap-dot-size, 0.5rem);
|
|
235
|
+
block-size: var(--snap-dot-size, 0.5rem);
|
|
236
|
+
border-radius: 9999px;
|
|
237
|
+
background: var(--snap-dot, color-mix(in srgb, currentColor 30%, transparent));
|
|
238
|
+
transition: background-color 150ms;
|
|
239
|
+
}
|
|
240
|
+
.snap-dots :is(i[data-on], [data-on] > i) { background: var(--snap-dot-on, currentColor); }
|
|
241
|
+
|
|
204
242
|
/* ---- spinner, skeleton, empty state ----------------------------------------
|
|
205
243
|
The shape and the motion; colour, size and surface are the site's. The
|
|
206
244
|
spinner is a ring in currentColor sized by `--spinner-size`, the skeleton a
|
package/dist/block-editor.d.ts
CHANGED
|
@@ -200,6 +200,33 @@ export declare function PageListFields<B extends {
|
|
|
200
200
|
onUpdate: (block: B) => void;
|
|
201
201
|
pages?: readonly PageOption[];
|
|
202
202
|
}): import("react").JSX.Element;
|
|
203
|
+
/**
|
|
204
|
+
* A list block's fields: where its pages come from, and how it shows them.
|
|
205
|
+
* `pages` are what a hand-picked list chooses from, keyed as the wiki keys
|
|
206
|
+
* them; `sections` the sections a list may name, else a section is typed.
|
|
207
|
+
* A wiki with a source of its own passes it in `sources` and draws its fields
|
|
208
|
+
* as `children`, which show while it is chosen; `show` is then the wiki's own.
|
|
209
|
+
*/
|
|
210
|
+
export declare function ListFields<B extends {
|
|
211
|
+
source: string;
|
|
212
|
+
under?: string;
|
|
213
|
+
count?: number;
|
|
214
|
+
pages?: string[];
|
|
215
|
+
show: 'links' | 'cards';
|
|
216
|
+
}>({ block, onUpdate, pages, sections, sources, children, }: {
|
|
217
|
+
block: B;
|
|
218
|
+
onUpdate: (block: B) => void;
|
|
219
|
+
pages?: readonly PageOption[];
|
|
220
|
+
sections?: readonly {
|
|
221
|
+
value: string;
|
|
222
|
+
label: string;
|
|
223
|
+
}[];
|
|
224
|
+
sources?: readonly {
|
|
225
|
+
value: string;
|
|
226
|
+
label: string;
|
|
227
|
+
}[];
|
|
228
|
+
children?: ReactNode;
|
|
229
|
+
}): import("react").JSX.Element;
|
|
203
230
|
/** Two to four columns, each a block list offering the schema's `nestedTypes`. */
|
|
204
231
|
export declare function ColumnsFields<A extends EditableBlock>({ block, onUpdate }: {
|
|
205
232
|
block: ColumnsBlock<A>;
|
package/dist/block-editor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
// block-editor.tsx — the block list an author edits, and the fields of the core
|
|
4
4
|
// block types.
|
|
5
5
|
//
|
|
@@ -23,7 +23,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
23
23
|
// card re-renders on every keystroke.
|
|
24
24
|
import { createContext, memo, useCallback, useContext, useMemo, useState, } from 'react';
|
|
25
25
|
import { BlockActions, useBlockOperations } from './react.js';
|
|
26
|
-
import { CODE_LANGS, DEFAULT_LANG, } from './blocks.js';
|
|
26
|
+
import { CODE_LANGS, DEFAULT_LANG, LIST_SOURCES, } from './blocks.js';
|
|
27
27
|
import { BANNER_VARIANTS } from './text.js';
|
|
28
28
|
import { MAX_BLOCK_ROWS } from './validation.js';
|
|
29
29
|
import { cx } from './html.js';
|
|
@@ -187,11 +187,13 @@ export function BannerFields({ block, onUpdate }) {
|
|
|
187
187
|
* the rest are offered in a picker; without, an id is typed in.
|
|
188
188
|
*/
|
|
189
189
|
export function PageListFields({ block, onUpdate, pages }) {
|
|
190
|
+
return _jsx(PagePicker, { ids: block.pageIds, onChange: pageIds => onUpdate({ ...block, pageIds }), pages: pages });
|
|
191
|
+
}
|
|
192
|
+
/** Pages picked by key, in order: the body of a page list and of a list that picks its pages by hand. */
|
|
193
|
+
function PagePicker({ ids, onChange: set, pages }) {
|
|
190
194
|
const { kit } = useEditorContext();
|
|
191
195
|
const c = kit.classNames;
|
|
192
196
|
const [typed, setTyped] = useState('');
|
|
193
|
-
const ids = block.pageIds;
|
|
194
|
-
const set = (pageIds) => onUpdate({ ...block, pageIds });
|
|
195
197
|
const byId = new Map(pages?.map(p => [p.id, p]));
|
|
196
198
|
const full = ids.length >= MAX_BLOCK_ROWS;
|
|
197
199
|
const add = (id) => {
|
|
@@ -222,6 +224,34 @@ export function PageListFields({ block, onUpdate, pages }) {
|
|
|
222
224
|
setTyped('');
|
|
223
225
|
}, children: "Add page" })] }))] }));
|
|
224
226
|
}
|
|
227
|
+
/** What each of a list's sources is called where it is chosen. */
|
|
228
|
+
const LIST_SOURCE_LABELS = {
|
|
229
|
+
under: 'The pages in a section',
|
|
230
|
+
newest: 'The newest pages',
|
|
231
|
+
chosen: 'Pages picked by hand',
|
|
232
|
+
};
|
|
233
|
+
const LIST_SHOWS = [
|
|
234
|
+
{ value: 'links', label: 'As links, with their age' },
|
|
235
|
+
{ value: 'cards', label: 'As cards, with a picture and a line each' },
|
|
236
|
+
];
|
|
237
|
+
/**
|
|
238
|
+
* A list block's fields: where its pages come from, and how it shows them.
|
|
239
|
+
* `pages` are what a hand-picked list chooses from, keyed as the wiki keys
|
|
240
|
+
* them; `sections` the sections a list may name, else a section is typed.
|
|
241
|
+
* A wiki with a source of its own passes it in `sources` and draws its fields
|
|
242
|
+
* as `children`, which show while it is chosen; `show` is then the wiki's own.
|
|
243
|
+
*/
|
|
244
|
+
export function ListFields({ block, onUpdate, pages, sections, sources = [], children, }) {
|
|
245
|
+
const c = useEditorContext().kit.classNames;
|
|
246
|
+
const core = LIST_SOURCES.includes(block.source);
|
|
247
|
+
const set = (change) => onUpdate({ ...block, ...change });
|
|
248
|
+
const section = (label) => sections ? (_jsx(Choice, { label: label, value: block.under ?? '', options: [{ value: '', label: 'The whole wiki' }, ...sections], onChange: under => set({ under: under || undefined }) })) : (_jsx(Field, { label: label, children: _jsx(TextInput, { label: label, optional: true, value: block.under, onChange: under => set({ under: under?.trim().replace(/^\/+|\/+$/g, '') || undefined }) }) }));
|
|
249
|
+
return (_jsxs("div", { className: c.stack, children: [_jsx(Choice, { label: "Which pages", value: block.source, options: [...LIST_SOURCES.map(v => ({ value: v, label: LIST_SOURCE_LABELS[v] })), ...sources], onChange: source => set({
|
|
250
|
+
source,
|
|
251
|
+
...(source === 'newest' && !block.count && { count: 5 }),
|
|
252
|
+
...(source === 'chosen' && !block.pages && { pages: [] }),
|
|
253
|
+
}) }), block.source === 'under' && section('The section'), block.source === 'newest' && (_jsxs(_Fragment, { children: [_jsx(LimitField, { value: block.count, onChange: count => set({ count }) }), section('From the section')] })), block.source === 'chosen' && _jsx(PagePicker, { ids: block.pages ?? [], onChange: p => set({ pages: p }), pages: pages }), core ? _jsx(Choice, { label: "Shown", value: block.show, options: LIST_SHOWS, onChange: show => set({ show }) }) : children] }));
|
|
254
|
+
}
|
|
225
255
|
const GAPS = ['sm', 'md', 'lg'].map(v => ({ value: v, label: v }));
|
|
226
256
|
const ALIGNS = ['start', 'center', 'end', 'stretch'].map(v => ({ value: v, label: v }));
|
|
227
257
|
const MAX_COLUMNS = 4;
|
package/dist/blocks.d.ts
CHANGED
|
@@ -126,6 +126,28 @@ export interface InfoboxBlock<A> {
|
|
|
126
126
|
type: 'infobox';
|
|
127
127
|
blocks: A[];
|
|
128
128
|
}
|
|
129
|
+
/** Where a list's pages come from: those in a section, the newest, or ones picked by hand. */
|
|
130
|
+
export declare const LIST_SOURCES: readonly ["under", "newest", "chosen"];
|
|
131
|
+
export type ListSource = (typeof LIST_SOURCES)[number];
|
|
132
|
+
/**
|
|
133
|
+
* A list of the wiki's pages, one block for what `recentPages` and `pageList`
|
|
134
|
+
* do between them and for a section's index besides: the pages in a section,
|
|
135
|
+
* the newest (in a section or across the wiki), or pages picked by hand, shown
|
|
136
|
+
* as links with their age or as cards. `under` names a section the wiki's own
|
|
137
|
+
* way, a tag path or a URL path; `pages` holds whatever key the wiki picks a
|
|
138
|
+
* page by, an id or a path, in the order they are listed. A wiki resolves it
|
|
139
|
+
* to `items` when the page is read, as it does a page list.
|
|
140
|
+
*/
|
|
141
|
+
export interface ListBlock {
|
|
142
|
+
id: string;
|
|
143
|
+
type: 'list';
|
|
144
|
+
source: ListSource;
|
|
145
|
+
under?: string;
|
|
146
|
+
/** How many of the newest. */
|
|
147
|
+
count?: number;
|
|
148
|
+
pages?: string[];
|
|
149
|
+
show: 'links' | 'cards';
|
|
150
|
+
}
|
|
129
151
|
export interface BlockTreeOptions<B> {
|
|
130
152
|
/** Render one leaf block. Return `''` to drop it from the document. */
|
|
131
153
|
atomic: (block: B) => string;
|
|
@@ -264,6 +286,9 @@ export type CoreAtomicBlock<P = ResolvedPageRef> = {
|
|
|
264
286
|
} | {
|
|
265
287
|
type: 'recentPages' | 'pageList';
|
|
266
288
|
resolvedPages?: readonly P[] | null;
|
|
289
|
+
} | {
|
|
290
|
+
type: 'list';
|
|
291
|
+
items?: readonly P[] | null;
|
|
267
292
|
};
|
|
268
293
|
/**
|
|
269
294
|
* One core leaf as prose, for the LLM and MCP exports. A page list reads only
|
|
@@ -313,6 +338,12 @@ export interface CoreBlockDefaults {
|
|
|
313
338
|
type: 'pageList';
|
|
314
339
|
pageIds: string[];
|
|
315
340
|
};
|
|
341
|
+
list: () => {
|
|
342
|
+
type: 'list';
|
|
343
|
+
source: 'newest';
|
|
344
|
+
count: number;
|
|
345
|
+
show: 'links';
|
|
346
|
+
};
|
|
316
347
|
columns: () => {
|
|
317
348
|
type: 'columns';
|
|
318
349
|
columns: {
|
package/dist/blocks.js
CHANGED
|
@@ -29,6 +29,8 @@ export function pageRef(page, now, style = 'long') {
|
|
|
29
29
|
updated: new Date(page.updatedAt).toISOString(),
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
|
+
/** Where a list's pages come from: those in a section, the newest, or ones picked by hand. */
|
|
33
|
+
export const LIST_SOURCES = ['under', 'newest', 'chosen'];
|
|
32
34
|
/**
|
|
33
35
|
* The whole tree as markdown. Containers flatten in document order, because a
|
|
34
36
|
* reader following prose does not have columns — and an agent quoting the page
|
|
@@ -218,6 +220,7 @@ export function coreAtomicText(block) {
|
|
|
218
220
|
case 'testimonial': return testimonialToText(block.quote, block.author, block.role);
|
|
219
221
|
case 'recentPages':
|
|
220
222
|
case 'pageList': return pageListToText(block.resolvedPages ?? []);
|
|
223
|
+
case 'list': return pageListToText(block.items ?? []);
|
|
221
224
|
}
|
|
222
225
|
}
|
|
223
226
|
/** One core leaf as markdown, for a page's `.md` twin. */
|
|
@@ -232,7 +235,8 @@ export function coreAtomicToMarkdown(block, opts) {
|
|
|
232
235
|
case 'testimonial': return testimonialToMarkdown(block.quote, block.author, block.role);
|
|
233
236
|
case 'recentPages':
|
|
234
237
|
case 'pageList':
|
|
235
|
-
|
|
238
|
+
case 'list':
|
|
239
|
+
return linkList(((block.type === 'list' ? block.items : block.resolvedPages) ?? []).map(p => ({
|
|
236
240
|
label: p.title,
|
|
237
241
|
href: p.href.startsWith('/') ? `${opts.siteUrl}${p.href}` : p.href,
|
|
238
242
|
})));
|
|
@@ -257,6 +261,7 @@ export function coreBlockDefaults(opts = {}) {
|
|
|
257
261
|
content: () => ({ type: 'content', text: '' }),
|
|
258
262
|
recentPages: () => ({ type: 'recentPages', limit: 5 }),
|
|
259
263
|
pageList: () => ({ type: 'pageList', pageIds: [] }),
|
|
264
|
+
list: () => ({ type: 'list', source: 'newest', count: 5, show: 'links' }),
|
|
260
265
|
columns: () => ({
|
|
261
266
|
type: 'columns',
|
|
262
267
|
columns: [{ id: newId(), blocks: [] }, { id: newId(), blocks: [] }],
|
package/dist/conformance.d.ts
CHANGED
|
@@ -260,18 +260,3 @@ export declare function rolaOriginCheck(t: Tester, opts?: {
|
|
|
260
260
|
endpoint?: string;
|
|
261
261
|
tool?: string;
|
|
262
262
|
}): Promise<void>;
|
|
263
|
-
/**
|
|
264
|
-
* A ChatGPT plugin package agrees with the server it names. The ZIP is uploaded
|
|
265
|
-
* once and OpenAI rescans the server every day, so a renamed tool, a write that
|
|
266
|
-
* reaches the plugin's endpoint, a description naming a tool the endpoint lacks,
|
|
267
|
-
* or a moved legal page fails review with nothing in the repo noticing. Pass the
|
|
268
|
-
* parsed `chatgpt/plugin.json` and `chatgpt/mcp.json`.
|
|
269
|
-
*
|
|
270
|
-
* URLs on the plugin's own origin are rebased onto the origin under test, so a
|
|
271
|
-
* run against a dev server checks the dev server. The demo recording is the
|
|
272
|
-
* one review field no code can hold, so it is not checked.
|
|
273
|
-
*/
|
|
274
|
-
export declare function pluginParity(t: Tester, pkg: {
|
|
275
|
-
plugin: unknown;
|
|
276
|
-
mcp: unknown;
|
|
277
|
-
}): Promise<void>;
|
package/dist/conformance.js
CHANGED
|
@@ -600,91 +600,3 @@ export async function rolaOriginCheck(t, opts = {}) {
|
|
|
600
600
|
const message = answer?.sign?.message ?? '';
|
|
601
601
|
t.check('ROLA recipe names this origin', message.includes(`"${origin}"`), `expects "${origin}" in: …${message.slice(-90)}`);
|
|
602
602
|
}
|
|
603
|
-
const LISTING_LIMITS = { displayName: 30, shortDescription: 30, longDescription: 4000, developerName: 80, category: 80 };
|
|
604
|
-
const LISTING_URLS = ['websiteURL', 'supportURL', 'privacyPolicyURL', 'termsOfServiceURL'];
|
|
605
|
-
const HINTS = ['readOnlyHint', 'destructiveHint', 'openWorldHint'];
|
|
606
|
-
/**
|
|
607
|
-
* A ChatGPT plugin package agrees with the server it names. The ZIP is uploaded
|
|
608
|
-
* once and OpenAI rescans the server every day, so a renamed tool, a write that
|
|
609
|
-
* reaches the plugin's endpoint, a description naming a tool the endpoint lacks,
|
|
610
|
-
* or a moved legal page fails review with nothing in the repo noticing. Pass the
|
|
611
|
-
* parsed `chatgpt/plugin.json` and `chatgpt/mcp.json`.
|
|
612
|
-
*
|
|
613
|
-
* URLs on the plugin's own origin are rebased onto the origin under test, so a
|
|
614
|
-
* run against a dev server checks the dev server. The demo recording is the
|
|
615
|
-
* one review field no code can hold, so it is not checked.
|
|
616
|
-
*/
|
|
617
|
-
export async function pluginParity(t, pkg) {
|
|
618
|
-
const openai = pkg.plugin.extensions?.['com.openai'] ?? {};
|
|
619
|
-
const listing = openai.interface ?? {};
|
|
620
|
-
const cases = openai.review?.test_cases ?? {};
|
|
621
|
-
const servers = Object.values(pkg.mcp.mcpServers ?? {});
|
|
622
|
-
const named = servers[0]?.url ?? '';
|
|
623
|
-
t.check('plugin names one server', servers.length === 1 && servers[0]?.type === 'streamable-http' && named.startsWith('https://'), `${servers.length} server(s): ${named || 'no url'}`);
|
|
624
|
-
if (!named.startsWith('https://'))
|
|
625
|
-
return;
|
|
626
|
-
const canonical = new URL(named).origin;
|
|
627
|
-
const here = (url) => {
|
|
628
|
-
const u = new URL(url);
|
|
629
|
-
return u.origin === canonical ? new URL(`${u.pathname}${u.search}`, t.base).href : url;
|
|
630
|
-
};
|
|
631
|
-
const endpoint = here(named);
|
|
632
|
-
const tools = (await t.rpcAt(endpoint, 'tools/list')).result?.tools ?? [];
|
|
633
|
-
const names = new Set(tools.map(x => x.name));
|
|
634
|
-
t.check('plugin endpoint answers', tools.length > 0, `${endpoint}: ${tools.length} tools`);
|
|
635
|
-
const triggered = [...new Set((cases.positive ?? []).flatMap(c => (c.tools_triggered ?? '').split(/[\s,]+/).filter(Boolean)))];
|
|
636
|
-
const absent = triggered.filter(n => !names.has(n));
|
|
637
|
-
t.check('review cases name live tools', triggered.length > 0 && absent.length === 0, absent.length ? `not on the endpoint: ${absent.join(', ')}` : `${triggered.join(', ')}`);
|
|
638
|
-
const vague = tools.filter(x => HINTS.some(k => typeof x.annotations?.[k] !== 'boolean'));
|
|
639
|
-
t.check('plugin hints are booleans', vague.length === 0, vague.length ? vague.map(x => x.name).join(', ') : `${HINTS.join(', ')} on ${tools.length} tools`);
|
|
640
|
-
const writes = tools.filter(x => x.annotations?.readOnlyHint === false).map(x => x.name);
|
|
641
|
-
const claimsWrite = Array.isArray(listing.capabilities) && listing.capabilities.includes('Write');
|
|
642
|
-
t.check('plugin capabilities', claimsWrite === writes.length > 0, `capabilities ${claimsWrite ? 'claim' : 'omit'} Write; ${writes.length ? `writes: ${writes.join(', ')}` : 'no write tools'}`);
|
|
643
|
-
// A tool the main endpoint has and the plugin's lacks, named anywhere the
|
|
644
|
-
// model reads on the plugin's endpoint, sends it to a call that cannot work.
|
|
645
|
-
const lost = ((await t.rpc('tools/list')).result?.tools ?? []).map(x => x.name).filter(n => !names.has(n));
|
|
646
|
-
const init = await t.rpcAt(endpoint, 'initialize', {
|
|
647
|
-
protocolVersion: MCP_LEGACY_PROTOCOL_VERSION,
|
|
648
|
-
capabilities: {},
|
|
649
|
-
clientInfo: { name: 'plugin-parity', version: '1' },
|
|
650
|
-
});
|
|
651
|
-
const prompts = (await t.rpcAt(endpoint, 'prompts/list')).result?.prompts ?? [];
|
|
652
|
-
const texts = [
|
|
653
|
-
['instructions', init.result?.instructions ?? ''],
|
|
654
|
-
...tools.map((x) => [`${x.name} description`, x.description ?? '']),
|
|
655
|
-
];
|
|
656
|
-
for (const p of prompts) {
|
|
657
|
-
const got = (await t.rpcAt(endpoint, 'prompts/get', { name: p.name })).result;
|
|
658
|
-
texts.push([`prompt ${p.name}`, (got?.messages ?? []).map(m => m.content?.text ?? '').join('\n')]);
|
|
659
|
-
}
|
|
660
|
-
const strays = texts.flatMap(([where, text]) => lost.filter(n => new RegExp(`\\b${n}\\b`).test(text)).map(n => `${where} names ${n}`));
|
|
661
|
-
t.check('plugin text names only its tools', strays.length === 0, strays.length ? strays.join('; ') : `${texts.length} texts, ${lost.length} tools withheld`);
|
|
662
|
-
const str = (k) => (typeof listing[k] === 'string' ? listing[k] : '');
|
|
663
|
-
const starters = Array.isArray(listing.defaultPrompt) ? listing.defaultPrompt : [];
|
|
664
|
-
const positive = cases.positive ?? [];
|
|
665
|
-
const faults = [
|
|
666
|
-
...Object.entries(LISTING_LIMITS).filter(([k, n]) => !str(k) || str(k).length > n).map(([k, n]) => `${k} ${str(k).length}/${n}`),
|
|
667
|
-
...(starters.length > 3 || starters.some(p => typeof p !== 'string' || p.length > 128) ? ['defaultPrompt: at most 3, 128 each'] : []),
|
|
668
|
-
...(typeof listing.logo === 'string' ? [] : ['no logo']),
|
|
669
|
-
...(positive.length === 5 && positive.every(c => c.description && c.prompt && c.tools_triggered && c.expected_behavior)
|
|
670
|
-
? []
|
|
671
|
-
: ['5 positive cases, each with description, prompt, tools_triggered and expected_behavior']),
|
|
672
|
-
...((cases.negative ?? []).length === 3 && (cases.negative ?? []).every(c => c.description && c.prompt) ? [] : ['3 negative cases']),
|
|
673
|
-
];
|
|
674
|
-
t.check('plugin listing', faults.length === 0, faults.length ? faults.join('; ') : `${str('displayName')}: ${str('shortDescription')}`);
|
|
675
|
-
for (const k of LISTING_URLS) {
|
|
676
|
-
const url = str(k);
|
|
677
|
-
if (!url.startsWith('https://')) {
|
|
678
|
-
t.check(`listing ${k}`, false, url ? `not https: ${url}` : 'missing');
|
|
679
|
-
continue;
|
|
680
|
-
}
|
|
681
|
-
if (new URL(url).origin !== canonical) {
|
|
682
|
-
t.check(`listing ${k}`, true, `${url} (another origin, not fetched)`);
|
|
683
|
-
continue;
|
|
684
|
-
}
|
|
685
|
-
const res = await fetch(here(url));
|
|
686
|
-
const anchor = decodeURIComponent(new URL(url).hash.slice(1));
|
|
687
|
-
const found = !anchor || (await res.text()).includes(`id="${anchor}"`);
|
|
688
|
-
t.check(`listing ${k}`, res.ok && found, `${res.status} ${url}${found ? '' : ` (no id="${anchor}")`}`);
|
|
689
|
-
}
|
|
690
|
-
}
|
package/dist/mcp.d.ts
CHANGED
|
@@ -240,18 +240,6 @@ export interface EnvelopeGate {
|
|
|
240
240
|
empty: boolean;
|
|
241
241
|
finish(dispatched: object | object[] | null): Promise<object | object[] | null>;
|
|
242
242
|
}
|
|
243
|
-
/**
|
|
244
|
-
* The same server with its writes taken off, for a client that may not carry
|
|
245
|
-
* them: ChatGPT's plugin review refuses crypto transfers and checkout links, and
|
|
246
|
-
* a reviewer cannot sign a ROLA challenge. It keeps the tools marked
|
|
247
|
-
* `readOnlyHint: true` and not in `hide`, then drops every instruction line and
|
|
248
|
-
* prompt that names a tool it lost, so nothing sends an agent to a tool that is
|
|
249
|
-
* not there.
|
|
250
|
-
*
|
|
251
|
-
* The kept tools state all three hints that review reads. A read-only tool
|
|
252
|
-
* destroys nothing, and an unstated `openWorldHint` is the spec's default, true.
|
|
253
|
-
*/
|
|
254
|
-
export declare function readOnly(config: McpServerConfig, hide?: readonly string[]): McpServerConfig;
|
|
255
243
|
/**
|
|
256
244
|
* A caller-fixable failure inside a handler (page not found, empty input).
|
|
257
245
|
* Reported as a tool result with `isError` so the model sees the text and can
|
package/dist/mcp.js
CHANGED
|
@@ -86,28 +86,6 @@ export function toolCard(tool) {
|
|
|
86
86
|
...(tool.annotations ? { annotations: tool.annotations } : {}),
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
|
-
/**
|
|
90
|
-
* The same server with its writes taken off, for a client that may not carry
|
|
91
|
-
* them: ChatGPT's plugin review refuses crypto transfers and checkout links, and
|
|
92
|
-
* a reviewer cannot sign a ROLA challenge. It keeps the tools marked
|
|
93
|
-
* `readOnlyHint: true` and not in `hide`, then drops every instruction line and
|
|
94
|
-
* prompt that names a tool it lost, so nothing sends an agent to a tool that is
|
|
95
|
-
* not there.
|
|
96
|
-
*
|
|
97
|
-
* The kept tools state all three hints that review reads. A read-only tool
|
|
98
|
-
* destroys nothing, and an unstated `openWorldHint` is the spec's default, true.
|
|
99
|
-
*/
|
|
100
|
-
export function readOnly(config, hide = []) {
|
|
101
|
-
const kept = config.tools.filter(t => t.annotations?.readOnlyHint === true && !hide.includes(t.name));
|
|
102
|
-
const lost = config.tools.filter(t => !kept.includes(t)).map(t => t.name);
|
|
103
|
-
const names = (text) => lost.some(name => new RegExp(`\\b${name}\\b`).test(text));
|
|
104
|
-
return {
|
|
105
|
-
...config,
|
|
106
|
-
instructions: config.instructions.split('\n').filter(line => !names(line)).join('\n'),
|
|
107
|
-
tools: kept.map(t => ({ ...t, annotations: { destructiveHint: false, openWorldHint: true, ...t.annotations } })),
|
|
108
|
-
prompts: config.prompts?.filter(p => !names(p.text)),
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
89
|
/**
|
|
112
90
|
* A caller-fixable failure inside a handler (page not found, empty input).
|
|
113
91
|
* Reported as a tool result with `isError` so the model sees the text and can
|
package/dist/react-server.d.ts
CHANGED
|
@@ -195,6 +195,58 @@ 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
|
+
export interface PagerProps {
|
|
199
|
+
/** The page showing, from 1. */
|
|
200
|
+
page: number;
|
|
201
|
+
total: number;
|
|
202
|
+
/** Each page's address: the steps are links and the pager ships no JavaScript. */
|
|
203
|
+
href?: (page: number) => string;
|
|
204
|
+
/** Or what a step does: the steps are buttons. A carousel passes `go`. */
|
|
205
|
+
onPage?: (page: number) => void;
|
|
206
|
+
/** Step round from either end, as a carousel does, instead of stopping. */
|
|
207
|
+
wrap?: boolean;
|
|
208
|
+
/** Both steps inert, as while the next page loads. */
|
|
209
|
+
disabled?: boolean;
|
|
210
|
+
/** What one page is: the middle reads "<noun> 2 of 9", the steps "Previous <noun>". */
|
|
211
|
+
noun?: string;
|
|
212
|
+
prev?: ReactNode;
|
|
213
|
+
next?: ReactNode;
|
|
214
|
+
/** The steps at the ends of the line rather than beside the count. */
|
|
215
|
+
spread?: boolean;
|
|
216
|
+
/** The landmark's name, when the page has more than one pager. */
|
|
217
|
+
label?: string;
|
|
218
|
+
link?: WikiLinkComponent;
|
|
219
|
+
className?: string;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* "← Previous · Page 2 of 9 · Next →", for search results and for a carousel.
|
|
223
|
+
*
|
|
224
|
+
* Three sites wrote this line by hand, two of them for paged results (one with
|
|
225
|
+
* links, one with buttons) and one for a snap row of points, and each stopped
|
|
226
|
+
* at the ends differently: a disabled button, an empty span, a wrap. Here a
|
|
227
|
+
* step at an end with nowhere to go stays in place, inert, so the count does
|
|
228
|
+
* not jump sideways on the first and last page.
|
|
229
|
+
*/
|
|
230
|
+
export declare function Pager({ page, total, href, onPage, wrap, disabled, noun, prev, next, spread, label, link: Link, className, }: PagerProps): import("react").JSX.Element | null;
|
|
231
|
+
export interface SnapDotsProps {
|
|
232
|
+
count: number;
|
|
233
|
+
/** The item showing, from 0: `useSnapRow`'s `at`. */
|
|
234
|
+
at: number;
|
|
235
|
+
/** `useSnapRow`'s `go`: each dot becomes a button. Omit it for dots that only count. */
|
|
236
|
+
onGo?: (i: number) => void;
|
|
237
|
+
/** What one item is, for each button's name: "Go to <noun> 3". */
|
|
238
|
+
noun?: string;
|
|
239
|
+
className?: string;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* A snap row's dots, one per item, the one in view marked.
|
|
243
|
+
*
|
|
244
|
+
* With `onGo` each is a button, named, its 0.5rem mark in a 1.5rem target; on a
|
|
245
|
+
* card whose whole face is already a link it is only a count, `aria-hidden`.
|
|
246
|
+
* The mark is the `<i>` either way, so one rule draws both, and `data-on`
|
|
247
|
+
* marks the current one in both.
|
|
248
|
+
*/
|
|
249
|
+
export declare function SnapDots({ count, at, onGo, noun, className }: SnapDotsProps): import("react").JSX.Element | null;
|
|
198
250
|
export interface InfoboxAsideProps {
|
|
199
251
|
/** The landmark's accessible name, e.g. `Key facts about ${title}`. */
|
|
200
252
|
label: string;
|
package/dist/react-server.js
CHANGED
|
@@ -148,6 +148,49 @@ 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
|
+
const Arrow = ({ children }) => _jsx("span", { "aria-hidden": "true", children: children });
|
|
152
|
+
/**
|
|
153
|
+
* "← Previous · Page 2 of 9 · Next →", for search results and for a carousel.
|
|
154
|
+
*
|
|
155
|
+
* Three sites wrote this line by hand, two of them for paged results (one with
|
|
156
|
+
* links, one with buttons) and one for a snap row of points, and each stopped
|
|
157
|
+
* at the ends differently: a disabled button, an empty span, a wrap. Here a
|
|
158
|
+
* step at an end with nowhere to go stays in place, inert, so the count does
|
|
159
|
+
* not jump sideways on the first and last page.
|
|
160
|
+
*/
|
|
161
|
+
export function Pager({ page, total, href, onPage, wrap = false, disabled = false, noun = 'Page', prev = _jsxs(_Fragment, { children: [_jsx(Arrow, { children: "\u2190" }), " Previous"] }), next = _jsxs(_Fragment, { children: ["Next ", _jsx(Arrow, { children: "\u2192" })] }), spread = false, label = `${noun}s`, link: Link = Anchor, className = '', }) {
|
|
162
|
+
if (total < 2)
|
|
163
|
+
return null;
|
|
164
|
+
const to = (dir) => {
|
|
165
|
+
const p = page + dir;
|
|
166
|
+
return p >= 1 && p <= total ? p : wrap ? (p < 1 ? total : 1) : null;
|
|
167
|
+
};
|
|
168
|
+
const name = noun.toLowerCase();
|
|
169
|
+
const step = (dir, content) => {
|
|
170
|
+
const p = disabled ? null : to(dir);
|
|
171
|
+
const aria = `${dir < 0 ? 'Previous' : 'Next'} ${name}`;
|
|
172
|
+
if (onPage) {
|
|
173
|
+
return (_jsx("button", { type: "button", className: "pager-step", "aria-label": aria, disabled: p === null, onClick: () => p !== null && onPage(p), children: content }));
|
|
174
|
+
}
|
|
175
|
+
return p !== null && href ? (_jsx(Link, { href: href(p), className: "pager-step", rel: dir < 0 ? 'prev' : 'next', children: content })) : (_jsx("span", { className: "pager-step", "aria-disabled": "true", children: content }));
|
|
176
|
+
};
|
|
177
|
+
return (_jsxs("nav", { "aria-label": label, className: `pager ${className}`.trim(), "data-spread": spread || undefined, children: [step(-1, prev), _jsxs("span", { className: "pager-count", "aria-live": onPage ? 'polite' : undefined, children: [noun, " ", page, " of ", total] }), step(1, next)] }));
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* A snap row's dots, one per item, the one in view marked.
|
|
181
|
+
*
|
|
182
|
+
* With `onGo` each is a button, named, its 0.5rem mark in a 1.5rem target; on a
|
|
183
|
+
* card whose whole face is already a link it is only a count, `aria-hidden`.
|
|
184
|
+
* The mark is the `<i>` either way, so one rule draws both, and `data-on`
|
|
185
|
+
* marks the current one in both.
|
|
186
|
+
*/
|
|
187
|
+
export function SnapDots({ count, at, onGo, noun = 'item', className = '' }) {
|
|
188
|
+
if (count < 2)
|
|
189
|
+
return null;
|
|
190
|
+
const cls = `snap-dots ${className}`.trim();
|
|
191
|
+
const dots = Array.from({ length: count }, (_, i) => i);
|
|
192
|
+
return onGo ? (_jsx("span", { className: cls, children: dots.map(i => (_jsx("button", { type: "button", "aria-label": `Go to ${noun} ${i + 1}`, "aria-current": i === at || undefined, "data-on": i === at || undefined, onClick: () => onGo(i), children: _jsx("i", {}) }, i))) })) : (_jsx("span", { className: cls, "aria-hidden": "true", children: dots.map(i => _jsx("i", { "data-on": i === at || undefined }, i)) }));
|
|
193
|
+
}
|
|
151
194
|
/**
|
|
152
195
|
* The facts panel beside an article. Named, because two of the three asides
|
|
153
196
|
* this replaced had no accessible name, and a complementary landmark without
|
package/dist/validation.d.ts
CHANGED
|
@@ -38,8 +38,9 @@ export interface CoreAtomicValidatorOptions {
|
|
|
38
38
|
* core, so the repo's own cases still decide its native types.
|
|
39
39
|
*
|
|
40
40
|
* `recentPages.limit` must be a whole number within the cap and `pageIds` no
|
|
41
|
-
* longer than it — caper's rule, which the other two lacked
|
|
42
|
-
* (`tagPath`, `section`)
|
|
41
|
+
* longer than it — caper's rule, which the other two lacked — and a `list`'s
|
|
42
|
+
* `count` and `pages` the same. A scope field (`tagPath`, `section`, `under`)
|
|
43
|
+
* is a string when present, and `pageId` checks a list's picked keys too.
|
|
43
44
|
*/
|
|
44
45
|
export declare function coreAtomicValidator(opts?: CoreAtomicValidatorOptions): (block: Record<string, unknown>) => boolean;
|
|
45
46
|
export interface BlockValidatorOptions {
|
package/dist/validation.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
//
|
|
9
9
|
// As elsewhere in this package, the dispatch stays with the caller so a new
|
|
10
10
|
// block type is a compile error there until it is handled.
|
|
11
|
+
import { LIST_SOURCES } from './blocks.js';
|
|
11
12
|
/**
|
|
12
13
|
* An href acceptable to persist: http, https, mailto, or a relative path or
|
|
13
14
|
* fragment. Everything else — `javascript:`, `data:`, `vbscript:`, a
|
|
@@ -93,8 +94,9 @@ const optionalString = (v) => v === undefined || typeof v === 'string';
|
|
|
93
94
|
* core, so the repo's own cases still decide its native types.
|
|
94
95
|
*
|
|
95
96
|
* `recentPages.limit` must be a whole number within the cap and `pageIds` no
|
|
96
|
-
* longer than it — caper's rule, which the other two lacked
|
|
97
|
-
* (`tagPath`, `section`)
|
|
97
|
+
* longer than it — caper's rule, which the other two lacked — and a `list`'s
|
|
98
|
+
* `count` and `pages` the same. A scope field (`tagPath`, `section`, `under`)
|
|
99
|
+
* is a string when present, and `pageId` checks a list's picked keys too.
|
|
98
100
|
*/
|
|
99
101
|
export function coreAtomicValidator(opts = {}) {
|
|
100
102
|
const { pageId = () => true, maxRows = MAX_BLOCK_ROWS } = opts;
|
|
@@ -108,6 +110,13 @@ export function coreAtomicValidator(opts = {}) {
|
|
|
108
110
|
return (Array.isArray(b.pageIds) &&
|
|
109
111
|
b.pageIds.length <= maxRows &&
|
|
110
112
|
b.pageIds.every(id => typeof id === 'string' && pageId(id)));
|
|
113
|
+
case 'list':
|
|
114
|
+
return (LIST_SOURCES.includes(b.source) &&
|
|
115
|
+
(b.show === 'links' || b.show === 'cards') &&
|
|
116
|
+
optionalString(b.under) &&
|
|
117
|
+
(b.count === undefined || okLimit(b.count, maxRows)) &&
|
|
118
|
+
(b.pages === undefined ||
|
|
119
|
+
(Array.isArray(b.pages) && b.pages.length <= maxRows && b.pages.every(id => typeof id === 'string' && pageId(id)))));
|
|
111
120
|
case 'codeTabs':
|
|
112
121
|
return validateCodeTabs(b.tabs);
|
|
113
122
|
case 'stats':
|
package/dist/well-known.d.ts
CHANGED
|
@@ -104,13 +104,6 @@ protocolVersions: readonly string[]): Record<string, unknown>;
|
|
|
104
104
|
* configured" instead of failing verification for a reason nobody sees.
|
|
105
105
|
*/
|
|
106
106
|
export declare function registryAuthHandler(publicKey?: string | undefined, keyType?: string | undefined): () => Response;
|
|
107
|
-
/**
|
|
108
|
-
* `/.well-known/openai-apps-challenge`: the token OpenAI's plugin portal issues
|
|
109
|
-
* to prove the MCP origin is the publisher's, served as the bare text it checks
|
|
110
|
-
* for. An env var for the same reason as the registry key: the portal issues it
|
|
111
|
-
* per plugin, after the code has shipped. Unset → 404.
|
|
112
|
-
*/
|
|
113
|
-
export declare function openaiChallengeHandler(token?: string | undefined): () => Response;
|
|
114
107
|
/** A tool as `/.well-known/mcp.json` lists it: what tools/list says, no handler. */
|
|
115
108
|
export interface ManifestTool {
|
|
116
109
|
name: string;
|
package/dist/well-known.js
CHANGED
|
@@ -178,17 +178,6 @@ export function registryAuthHandler(publicKey = process.env.MCP_REGISTRY_PUBLIC_
|
|
|
178
178
|
});
|
|
179
179
|
};
|
|
180
180
|
}
|
|
181
|
-
/**
|
|
182
|
-
* `/.well-known/openai-apps-challenge`: the token OpenAI's plugin portal issues
|
|
183
|
-
* to prove the MCP origin is the publisher's, served as the bare text it checks
|
|
184
|
-
* for. An env var for the same reason as the registry key: the portal issues it
|
|
185
|
-
* per plugin, after the code has shipped. Unset → 404.
|
|
186
|
-
*/
|
|
187
|
-
export function openaiChallengeHandler(token = process.env.OPENAI_APPS_CHALLENGE) {
|
|
188
|
-
return () => token
|
|
189
|
-
? new Response(token.trim(), { headers: { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store' } })
|
|
190
|
-
: new Response('Not found', { status: 404 });
|
|
191
|
-
}
|
|
192
181
|
/**
|
|
193
182
|
* `/.well-known/mcp.json`: the third path an arriving agent probes, after the
|
|
194
183
|
* agent card and the OpenAPI spec. All three origins wrote the same envelope,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wiki-formant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.76.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",
|