@ctcstack/ctcdocs-core 0.18.0 → 0.20.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/corpus-structure.d.ts +2 -0
- package/dist/corpus-structure.js +3 -0
- package/dist/index.d.ts +1 -1
- package/dist/project-layout.d.ts +8 -0
- package/dist/project-layout.js +8 -0
- package/dist/site-configuration.d.ts +18 -0
- package/dist/site-configuration.js +46 -5
- package/package.json +1 -1
- package/src/corpus-structure.ts +5 -0
- package/src/index.ts +1 -0
- package/src/project-layout.ts +8 -0
- package/src/site-configuration.ts +91 -6
|
@@ -19,6 +19,8 @@ export interface CorpusDocument {
|
|
|
19
19
|
* on its new chain to be confirmed.
|
|
20
20
|
*/
|
|
21
21
|
readonly publishedReaders?: '*' | readonly string[];
|
|
22
|
+
/** Set while such a move waits, as the manifest's `readersHeld` says. */
|
|
23
|
+
readonly readersHeld?: true;
|
|
22
24
|
/** The permanent short ID (ADR-022), the title and Drive's modified time. */
|
|
23
25
|
readonly shortId?: string;
|
|
24
26
|
readonly title?: string;
|
package/dist/corpus-structure.js
CHANGED
|
@@ -74,6 +74,9 @@ export function parseCorpusStructure(manifest) {
|
|
|
74
74
|
...(title ? { title } : {}),
|
|
75
75
|
...(modified ? { modified } : {}),
|
|
76
76
|
...(source ? { source } : {}),
|
|
77
|
+
...(typeof record.readersHeld === 'object' && record.readersHeld !== null
|
|
78
|
+
? { readersHeld: true }
|
|
79
|
+
: {}),
|
|
77
80
|
...(published === '*' ||
|
|
78
81
|
(Array.isArray(published) &&
|
|
79
82
|
published.every((group) => typeof group === 'string'))
|
package/dist/index.d.ts
CHANGED
|
@@ -11,4 +11,4 @@ export type { AccessClass, AccessModel, ChainReaders, PublishedReaders, Readers,
|
|
|
11
11
|
export { accessFindings } from './access-findings.js';
|
|
12
12
|
export type { AccessFinding } from './access-findings.js';
|
|
13
13
|
export { fetchCharacterLimit, loadSiteConfiguration, MCP_DEFAULTS, parseSiteConfiguration, SiteConfigurationError, } from './site-configuration.js';
|
|
14
|
-
export type { AddressPolicy, BrandConfiguration, DeploymentConfiguration, DeploymentEnvironmentConfiguration, DeploymentEnvironmentConfigurations, DeploymentVisibility, HomeConfiguration, McpConfiguration, McpSearchConfiguration, NavigationConfiguration, SignInConfiguration, SiteConfiguration, SyncConfigurationDefaults, } from './site-configuration.js';
|
|
14
|
+
export type { AddressPolicy, BrandConfiguration, DeploymentConfiguration, DeploymentEnvironmentConfiguration, DeploymentEnvironmentConfigurations, DeploymentVisibility, HomeConfiguration, McpConfiguration, McpRecentConfiguration, McpSearchConfiguration, NavigationConfiguration, SignInConfiguration, SiteConfiguration, SyncConfigurationDefaults, } from './site-configuration.js';
|
package/dist/project-layout.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export declare const PROJECT_LAYOUT: {
|
|
|
26
26
|
readonly publicDirectory: "public";
|
|
27
27
|
readonly robotsFile: "public/robots.txt";
|
|
28
28
|
readonly headersFile: "public/_headers";
|
|
29
|
+
/** The pages a project writes by hand; generated ones sit beneath, apart. */
|
|
30
|
+
readonly documentsDirectory: "src/content/docs";
|
|
29
31
|
/** Generated Markdown, one directory per Drive folder. */
|
|
30
32
|
readonly generatedDocumentsDirectory: "src/content/docs/_generated";
|
|
31
33
|
/** Original images, one directory per Google file identifier. */
|
|
@@ -68,6 +70,11 @@ export declare const PLATFORM_ROUTES: {
|
|
|
68
70
|
* docs/ADR/024-content-health-page.md.
|
|
69
71
|
*/
|
|
70
72
|
readonly contentHealth: "content-health";
|
|
73
|
+
/**
|
|
74
|
+
* Who may read each folder, for admins. See
|
|
75
|
+
* docs/ADR/045-an-access-review-page-shows-admins-who-may-read-each-folder.md.
|
|
76
|
+
*/
|
|
77
|
+
readonly accessReview: "access-review";
|
|
71
78
|
};
|
|
72
79
|
/**
|
|
73
80
|
* The platform's Workers, as a project's Wrangler configuration names them
|
|
@@ -100,6 +107,7 @@ export declare const ACCESS_CLASSES_ROUTE = "/_kb/classes";
|
|
|
100
107
|
export declare const PLATFORM_ROUTE_HREFS: {
|
|
101
108
|
readonly fullIndex: "/documents/";
|
|
102
109
|
readonly contentHealth: "/content-health/";
|
|
110
|
+
readonly accessReview: "/access-review/";
|
|
103
111
|
};
|
|
104
112
|
export declare const RESERVED_SLUGS: readonly string[];
|
|
105
113
|
/**
|
package/dist/project-layout.js
CHANGED
|
@@ -26,6 +26,8 @@ export const PROJECT_LAYOUT = {
|
|
|
26
26
|
publicDirectory: 'public',
|
|
27
27
|
robotsFile: 'public/robots.txt',
|
|
28
28
|
headersFile: 'public/_headers',
|
|
29
|
+
/** The pages a project writes by hand; generated ones sit beneath, apart. */
|
|
30
|
+
documentsDirectory: 'src/content/docs',
|
|
29
31
|
/** Generated Markdown, one directory per Drive folder. */
|
|
30
32
|
generatedDocumentsDirectory: 'src/content/docs/_generated',
|
|
31
33
|
/** Original images, one directory per Google file identifier. */
|
|
@@ -68,6 +70,11 @@ export const PLATFORM_ROUTES = {
|
|
|
68
70
|
* docs/ADR/024-content-health-page.md.
|
|
69
71
|
*/
|
|
70
72
|
contentHealth: 'content-health',
|
|
73
|
+
/**
|
|
74
|
+
* Who may read each folder, for admins. See
|
|
75
|
+
* docs/ADR/045-an-access-review-page-shows-admins-who-may-read-each-folder.md.
|
|
76
|
+
*/
|
|
77
|
+
accessReview: 'access-review',
|
|
71
78
|
};
|
|
72
79
|
/**
|
|
73
80
|
* The platform's Workers, as a project's Wrangler configuration names them
|
|
@@ -100,6 +107,7 @@ export const ACCESS_CLASSES_ROUTE = '/_kb/classes';
|
|
|
100
107
|
export const PLATFORM_ROUTE_HREFS = {
|
|
101
108
|
fullIndex: `/${PLATFORM_ROUTES.fullIndex}/`,
|
|
102
109
|
contentHealth: `/${PLATFORM_ROUTES.contentHealth}/`,
|
|
110
|
+
accessReview: `/${PLATFORM_ROUTES.accessReview}/`,
|
|
103
111
|
};
|
|
104
112
|
/**
|
|
105
113
|
* Top-level addresses the platform serves beside the corpus: the Worker's
|
|
@@ -33,6 +33,14 @@ export interface HomeConfiguration {
|
|
|
33
33
|
* outgrown one page may prefer the home page to stay an entrance.
|
|
34
34
|
*/
|
|
35
35
|
readonly corpusIndex: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* The address of the hand-authored page the home page sends a newcomer to,
|
|
38
|
+
* as a slug: `about` for `src/content/docs/about.md`. The link reads that
|
|
39
|
+
* page's own title, so renaming the page renames the link. Without it the
|
|
40
|
+
* home page offers no such link: the platform does not know what a
|
|
41
|
+
* deployment calls its introduction, or whether it has one.
|
|
42
|
+
*/
|
|
43
|
+
readonly start?: string;
|
|
36
44
|
}
|
|
37
45
|
/**
|
|
38
46
|
* Who a deployment is for.
|
|
@@ -177,6 +185,13 @@ export interface McpSearchConfiguration {
|
|
|
177
185
|
/** Characters of passage text one search returns at most, in all. */
|
|
178
186
|
readonly passageCharacters: number;
|
|
179
187
|
}
|
|
188
|
+
/** What `recent` lists (ADR-044). */
|
|
189
|
+
export interface McpRecentConfiguration {
|
|
190
|
+
/** Documents it lists when a call does not ask for a number. */
|
|
191
|
+
readonly defaultResults: number;
|
|
192
|
+
/** Documents a call may ask for at most. */
|
|
193
|
+
readonly results: number;
|
|
194
|
+
}
|
|
180
195
|
/**
|
|
181
196
|
* The MCP server through which AI assistants read the site as the person who
|
|
182
197
|
* connected them (ADR-041). It needs the Worker's sign-in, so it is accepted
|
|
@@ -187,6 +202,9 @@ export interface McpConfiguration {
|
|
|
187
202
|
readonly search: McpSearchConfiguration;
|
|
188
203
|
/** Characters `fetch` returns at most; a longer document is cut there. */
|
|
189
204
|
readonly fetchCharacters: number;
|
|
205
|
+
/** Characters of JSON a `browse` tree takes at most (ADR-044). */
|
|
206
|
+
readonly browseCharacters: number;
|
|
207
|
+
readonly recent: McpRecentConfiguration;
|
|
190
208
|
}
|
|
191
209
|
/** What `mcp` holds when a project sets only `enabled` (ADR-042). */
|
|
192
210
|
export declare const MCP_DEFAULTS: Omit<McpConfiguration, 'enabled'>;
|
|
@@ -27,11 +27,13 @@ export const MCP_DEFAULTS = Object.freeze({
|
|
|
27
27
|
model: '@cf/baai/bge-reranker-base',
|
|
28
28
|
threshold: 0,
|
|
29
29
|
}),
|
|
30
|
-
results:
|
|
30
|
+
results: 15,
|
|
31
31
|
passagesPerResult: 3,
|
|
32
32
|
passageCharacters: 24_000,
|
|
33
33
|
}),
|
|
34
34
|
fetchCharacters: 100_000,
|
|
35
|
+
browseCharacters: 24_000,
|
|
36
|
+
recent: Object.freeze({ defaultResults: 20, results: 50 }),
|
|
35
37
|
});
|
|
36
38
|
/**
|
|
37
39
|
* The characters an assistant reads of a document: the cut `fetch` makes
|
|
@@ -44,8 +46,6 @@ export function fetchCharacterLimit(mcp) {
|
|
|
44
46
|
}
|
|
45
47
|
/** About 10,000 tokens of English: a starting value, not a measured one. */
|
|
46
48
|
const LARGE_DOCUMENT_CHARACTERS = 40_000;
|
|
47
|
-
/** A passage of fewer characters than this says too little to judge by. */
|
|
48
|
-
const SHORTEST_PASSAGE = 100;
|
|
49
49
|
export class SiteConfigurationError extends Error {
|
|
50
50
|
name = 'SiteConfigurationError';
|
|
51
51
|
}
|
|
@@ -121,6 +121,21 @@ function flag(source, key, path) {
|
|
|
121
121
|
function optionalFlag(source, key, path, fallback) {
|
|
122
122
|
return source[key] === undefined ? fallback : flag(source, key, path);
|
|
123
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* A page's address as Starlight derives it from a file under
|
|
126
|
+
* `src/content/docs`: lowercase segments joined by `/`, with no leading or
|
|
127
|
+
* trailing slash, so it is the same string the content collection knows.
|
|
128
|
+
*/
|
|
129
|
+
function optionalPageSlug(source, key, path) {
|
|
130
|
+
if (source[key] === undefined) {
|
|
131
|
+
return {};
|
|
132
|
+
}
|
|
133
|
+
const value = text(source, key, path);
|
|
134
|
+
if (!/^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-]*)*$/u.test(value)) {
|
|
135
|
+
fail(path, 'must be a page address such as "about", without slashes at either end');
|
|
136
|
+
}
|
|
137
|
+
return { [key]: value };
|
|
138
|
+
}
|
|
124
139
|
/**
|
|
125
140
|
* A list of distinct Unicode script names, as a `\p{Script=…}` property
|
|
126
141
|
* escape spells them. Each name is proved by compiling that escape, so a typo
|
|
@@ -231,7 +246,24 @@ function mcpSearch(value) {
|
|
|
231
246
|
reranking,
|
|
232
247
|
results,
|
|
233
248
|
passagesPerResult: optionalNumber(source, 'passagesPerResult', 'mcp.search.passagesPerResult', defaults.passagesPerResult, { min: 1, whole: true }),
|
|
234
|
-
passageCharacters: optionalNumber(source, 'passageCharacters', 'mcp.search.passageCharacters', defaults.passageCharacters,
|
|
249
|
+
passageCharacters: optionalNumber(source, 'passageCharacters', 'mcp.search.passageCharacters', defaults.passageCharacters,
|
|
250
|
+
// Chunks are shown whole, so the budget answers to the chunk size, not
|
|
251
|
+
// to the results: those it cannot hold are listed without text.
|
|
252
|
+
{ min: 1_000, whole: true }),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function mcpRecent(value) {
|
|
256
|
+
const defaults = MCP_DEFAULTS.recent;
|
|
257
|
+
if (value === undefined) {
|
|
258
|
+
return defaults;
|
|
259
|
+
}
|
|
260
|
+
const source = record(value, 'mcp.recent');
|
|
261
|
+
knownKeys(source, 'mcp.recent', Object.keys(defaults));
|
|
262
|
+
const results = optionalNumber(source, 'results', 'mcp.recent.results', defaults.results, { min: 1, whole: true });
|
|
263
|
+
// The default, like a set value, is no more than a call may ask for.
|
|
264
|
+
return {
|
|
265
|
+
defaultResults: optionalNumber(source, 'defaultResults', 'mcp.recent.defaultResults', Math.min(defaults.defaultResults, results), { min: 1, max: results, whole: true }),
|
|
266
|
+
results,
|
|
235
267
|
};
|
|
236
268
|
}
|
|
237
269
|
function optionalMegabytes(source, key, path, fallback) {
|
|
@@ -378,7 +410,13 @@ export function parseSiteConfiguration(input) {
|
|
|
378
410
|
let mcp;
|
|
379
411
|
if (root.mcp !== undefined) {
|
|
380
412
|
const source = record(root.mcp, 'mcp');
|
|
381
|
-
knownKeys(source, 'mcp', [
|
|
413
|
+
knownKeys(source, 'mcp', [
|
|
414
|
+
'enabled',
|
|
415
|
+
'search',
|
|
416
|
+
'fetchCharacters',
|
|
417
|
+
'browseCharacters',
|
|
418
|
+
'recent',
|
|
419
|
+
]);
|
|
382
420
|
const enabled = flag(source, 'enabled', 'mcp.enabled');
|
|
383
421
|
if (enabled) {
|
|
384
422
|
if (!signIn) {
|
|
@@ -392,6 +430,8 @@ export function parseSiteConfiguration(input) {
|
|
|
392
430
|
enabled,
|
|
393
431
|
search: mcpSearch(source.search),
|
|
394
432
|
fetchCharacters: optionalNumber(source, 'fetchCharacters', 'mcp.fetchCharacters', MCP_DEFAULTS.fetchCharacters, { min: 1_000, whole: true }),
|
|
433
|
+
browseCharacters: optionalNumber(source, 'browseCharacters', 'mcp.browseCharacters', MCP_DEFAULTS.browseCharacters, { min: 1_000, whole: true }),
|
|
434
|
+
recent: mcpRecent(source.recent),
|
|
395
435
|
};
|
|
396
436
|
}
|
|
397
437
|
const fetchLimit = fetchCharacterLimit(mcp);
|
|
@@ -405,6 +445,7 @@ export function parseSiteConfiguration(input) {
|
|
|
405
445
|
corpusIndex: optionalFlag(homeSource, 'corpusIndex', 'home.corpusIndex', true),
|
|
406
446
|
lede: text(homeSource, 'lede', 'home.lede'),
|
|
407
447
|
recentLimit: optionalCount(homeSource, 'recentLimit', 'home.recentLimit', 6),
|
|
448
|
+
...optionalPageSlug(homeSource, 'start', 'home.start'),
|
|
408
449
|
},
|
|
409
450
|
navigation: {
|
|
410
451
|
landingDocumentTitles: titleList(navigationSource, 'landingDocumentTitles', 'navigation.landingDocumentTitles'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ctcstack/ctcdocs-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Configuration schema, project layout, generated-path allowlist and the published Markdown form shared by the CTCDocs platform packages.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ctcdocs",
|
package/src/corpus-structure.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface CorpusDocument {
|
|
|
33
33
|
* on its new chain to be confirmed.
|
|
34
34
|
*/
|
|
35
35
|
readonly publishedReaders?: '*' | readonly string[];
|
|
36
|
+
/** Set while such a move waits, as the manifest's `readersHeld` says. */
|
|
37
|
+
readonly readersHeld?: true;
|
|
36
38
|
/** The permanent short ID (ADR-022), the title and Drive's modified time. */
|
|
37
39
|
readonly shortId?: string;
|
|
38
40
|
readonly title?: string;
|
|
@@ -125,6 +127,9 @@ export function parseCorpusStructure(manifest: unknown): CorpusStructure {
|
|
|
125
127
|
...(title ? { title } : {}),
|
|
126
128
|
...(modified ? { modified } : {}),
|
|
127
129
|
...(source ? { source } : {}),
|
|
130
|
+
...(typeof record.readersHeld === 'object' && record.readersHeld !== null
|
|
131
|
+
? { readersHeld: true as const }
|
|
132
|
+
: {}),
|
|
128
133
|
...(published === '*' ||
|
|
129
134
|
(Array.isArray(published) &&
|
|
130
135
|
published.every((group) => typeof group === 'string'))
|
package/src/index.ts
CHANGED
package/src/project-layout.ts
CHANGED
|
@@ -26,6 +26,8 @@ export const PROJECT_LAYOUT = {
|
|
|
26
26
|
publicDirectory: 'public',
|
|
27
27
|
robotsFile: 'public/robots.txt',
|
|
28
28
|
headersFile: 'public/_headers',
|
|
29
|
+
/** The pages a project writes by hand; generated ones sit beneath, apart. */
|
|
30
|
+
documentsDirectory: 'src/content/docs',
|
|
29
31
|
/** Generated Markdown, one directory per Drive folder. */
|
|
30
32
|
generatedDocumentsDirectory: 'src/content/docs/_generated',
|
|
31
33
|
/** Original images, one directory per Google file identifier. */
|
|
@@ -69,6 +71,11 @@ export const PLATFORM_ROUTES = {
|
|
|
69
71
|
* docs/ADR/024-content-health-page.md.
|
|
70
72
|
*/
|
|
71
73
|
contentHealth: 'content-health',
|
|
74
|
+
/**
|
|
75
|
+
* Who may read each folder, for admins. See
|
|
76
|
+
* docs/ADR/045-an-access-review-page-shows-admins-who-may-read-each-folder.md.
|
|
77
|
+
*/
|
|
78
|
+
accessReview: 'access-review',
|
|
72
79
|
} as const;
|
|
73
80
|
|
|
74
81
|
/**
|
|
@@ -104,6 +111,7 @@ export const ACCESS_CLASSES_ROUTE = '/_kb/classes';
|
|
|
104
111
|
export const PLATFORM_ROUTE_HREFS = {
|
|
105
112
|
fullIndex: `/${PLATFORM_ROUTES.fullIndex}/`,
|
|
106
113
|
contentHealth: `/${PLATFORM_ROUTES.contentHealth}/`,
|
|
114
|
+
accessReview: `/${PLATFORM_ROUTES.accessReview}/`,
|
|
107
115
|
} as const;
|
|
108
116
|
|
|
109
117
|
/**
|
|
@@ -55,6 +55,14 @@ export interface HomeConfiguration {
|
|
|
55
55
|
* outgrown one page may prefer the home page to stay an entrance.
|
|
56
56
|
*/
|
|
57
57
|
readonly corpusIndex: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* The address of the hand-authored page the home page sends a newcomer to,
|
|
60
|
+
* as a slug: `about` for `src/content/docs/about.md`. The link reads that
|
|
61
|
+
* page's own title, so renaming the page renames the link. Without it the
|
|
62
|
+
* home page offers no such link: the platform does not know what a
|
|
63
|
+
* deployment calls its introduction, or whether it has one.
|
|
64
|
+
*/
|
|
65
|
+
readonly start?: string;
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
/**
|
|
@@ -209,6 +217,14 @@ export interface McpSearchConfiguration {
|
|
|
209
217
|
readonly passageCharacters: number;
|
|
210
218
|
}
|
|
211
219
|
|
|
220
|
+
/** What `recent` lists (ADR-044). */
|
|
221
|
+
export interface McpRecentConfiguration {
|
|
222
|
+
/** Documents it lists when a call does not ask for a number. */
|
|
223
|
+
readonly defaultResults: number;
|
|
224
|
+
/** Documents a call may ask for at most. */
|
|
225
|
+
readonly results: number;
|
|
226
|
+
}
|
|
227
|
+
|
|
212
228
|
/**
|
|
213
229
|
* The MCP server through which AI assistants read the site as the person who
|
|
214
230
|
* connected them (ADR-041). It needs the Worker's sign-in, so it is accepted
|
|
@@ -219,6 +235,9 @@ export interface McpConfiguration {
|
|
|
219
235
|
readonly search: McpSearchConfiguration;
|
|
220
236
|
/** Characters `fetch` returns at most; a longer document is cut there. */
|
|
221
237
|
readonly fetchCharacters: number;
|
|
238
|
+
/** Characters of JSON a `browse` tree takes at most (ADR-044). */
|
|
239
|
+
readonly browseCharacters: number;
|
|
240
|
+
readonly recent: McpRecentConfiguration;
|
|
222
241
|
}
|
|
223
242
|
|
|
224
243
|
/** What `mcp` holds when a project sets only `enabled` (ADR-042). */
|
|
@@ -233,11 +252,13 @@ export const MCP_DEFAULTS: Omit<McpConfiguration, 'enabled'> = Object.freeze({
|
|
|
233
252
|
model: '@cf/baai/bge-reranker-base',
|
|
234
253
|
threshold: 0,
|
|
235
254
|
}),
|
|
236
|
-
results:
|
|
255
|
+
results: 15,
|
|
237
256
|
passagesPerResult: 3,
|
|
238
257
|
passageCharacters: 24_000,
|
|
239
258
|
}),
|
|
240
259
|
fetchCharacters: 100_000,
|
|
260
|
+
browseCharacters: 24_000,
|
|
261
|
+
recent: Object.freeze({ defaultResults: 20, results: 50 }),
|
|
241
262
|
});
|
|
242
263
|
|
|
243
264
|
/**
|
|
@@ -253,9 +274,6 @@ export function fetchCharacterLimit(mcp: McpConfiguration | undefined): number {
|
|
|
253
274
|
/** About 10,000 tokens of English: a starting value, not a measured one. */
|
|
254
275
|
const LARGE_DOCUMENT_CHARACTERS = 40_000;
|
|
255
276
|
|
|
256
|
-
/** A passage of fewer characters than this says too little to judge by. */
|
|
257
|
-
const SHORTEST_PASSAGE = 100;
|
|
258
|
-
|
|
259
277
|
export interface SiteConfiguration {
|
|
260
278
|
/** Required to serve a private environment through the platform's Worker. */
|
|
261
279
|
readonly signIn?: SignInConfiguration;
|
|
@@ -380,6 +398,29 @@ function optionalFlag(
|
|
|
380
398
|
return source[key] === undefined ? fallback : flag(source, key, path);
|
|
381
399
|
}
|
|
382
400
|
|
|
401
|
+
/**
|
|
402
|
+
* A page's address as Starlight derives it from a file under
|
|
403
|
+
* `src/content/docs`: lowercase segments joined by `/`, with no leading or
|
|
404
|
+
* trailing slash, so it is the same string the content collection knows.
|
|
405
|
+
*/
|
|
406
|
+
function optionalPageSlug(
|
|
407
|
+
source: Record<string, unknown>,
|
|
408
|
+
key: string,
|
|
409
|
+
path: string,
|
|
410
|
+
): Record<string, string> {
|
|
411
|
+
if (source[key] === undefined) {
|
|
412
|
+
return {};
|
|
413
|
+
}
|
|
414
|
+
const value = text(source, key, path);
|
|
415
|
+
if (!/^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-]*)*$/u.test(value)) {
|
|
416
|
+
fail(
|
|
417
|
+
path,
|
|
418
|
+
'must be a page address such as "about", without slashes at either end',
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
return { [key]: value };
|
|
422
|
+
}
|
|
423
|
+
|
|
383
424
|
/**
|
|
384
425
|
* A list of distinct Unicode script names, as a `\p{Script=…}` property
|
|
385
426
|
* escape spells them. Each name is proved by compiling that escape, so a typo
|
|
@@ -570,8 +611,37 @@ function mcpSearch(value: unknown): McpSearchConfiguration {
|
|
|
570
611
|
'passageCharacters',
|
|
571
612
|
'mcp.search.passageCharacters',
|
|
572
613
|
defaults.passageCharacters,
|
|
573
|
-
|
|
614
|
+
// Chunks are shown whole, so the budget answers to the chunk size, not
|
|
615
|
+
// to the results: those it cannot hold are listed without text.
|
|
616
|
+
{ min: 1_000, whole: true },
|
|
617
|
+
),
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function mcpRecent(value: unknown): McpRecentConfiguration {
|
|
622
|
+
const defaults = MCP_DEFAULTS.recent;
|
|
623
|
+
if (value === undefined) {
|
|
624
|
+
return defaults;
|
|
625
|
+
}
|
|
626
|
+
const source = record(value, 'mcp.recent');
|
|
627
|
+
knownKeys(source, 'mcp.recent', Object.keys(defaults));
|
|
628
|
+
const results = optionalNumber(
|
|
629
|
+
source,
|
|
630
|
+
'results',
|
|
631
|
+
'mcp.recent.results',
|
|
632
|
+
defaults.results,
|
|
633
|
+
{ min: 1, whole: true },
|
|
634
|
+
);
|
|
635
|
+
// The default, like a set value, is no more than a call may ask for.
|
|
636
|
+
return {
|
|
637
|
+
defaultResults: optionalNumber(
|
|
638
|
+
source,
|
|
639
|
+
'defaultResults',
|
|
640
|
+
'mcp.recent.defaultResults',
|
|
641
|
+
Math.min(defaults.defaultResults, results),
|
|
642
|
+
{ min: 1, max: results, whole: true },
|
|
574
643
|
),
|
|
644
|
+
results,
|
|
575
645
|
};
|
|
576
646
|
}
|
|
577
647
|
|
|
@@ -775,7 +845,13 @@ export function parseSiteConfiguration(input: unknown): SiteConfiguration {
|
|
|
775
845
|
let mcp: McpConfiguration | undefined;
|
|
776
846
|
if (root.mcp !== undefined) {
|
|
777
847
|
const source = record(root.mcp, 'mcp');
|
|
778
|
-
knownKeys(source, 'mcp', [
|
|
848
|
+
knownKeys(source, 'mcp', [
|
|
849
|
+
'enabled',
|
|
850
|
+
'search',
|
|
851
|
+
'fetchCharacters',
|
|
852
|
+
'browseCharacters',
|
|
853
|
+
'recent',
|
|
854
|
+
]);
|
|
779
855
|
const enabled = flag(source, 'enabled', 'mcp.enabled');
|
|
780
856
|
if (enabled) {
|
|
781
857
|
if (!signIn) {
|
|
@@ -798,6 +874,14 @@ export function parseSiteConfiguration(input: unknown): SiteConfiguration {
|
|
|
798
874
|
MCP_DEFAULTS.fetchCharacters,
|
|
799
875
|
{ min: 1_000, whole: true },
|
|
800
876
|
),
|
|
877
|
+
browseCharacters: optionalNumber(
|
|
878
|
+
source,
|
|
879
|
+
'browseCharacters',
|
|
880
|
+
'mcp.browseCharacters',
|
|
881
|
+
MCP_DEFAULTS.browseCharacters,
|
|
882
|
+
{ min: 1_000, whole: true },
|
|
883
|
+
),
|
|
884
|
+
recent: mcpRecent(source.recent),
|
|
801
885
|
};
|
|
802
886
|
}
|
|
803
887
|
const fetchLimit = fetchCharacterLimit(mcp);
|
|
@@ -822,6 +906,7 @@ export function parseSiteConfiguration(input: unknown): SiteConfiguration {
|
|
|
822
906
|
'home.recentLimit',
|
|
823
907
|
6,
|
|
824
908
|
),
|
|
909
|
+
...optionalPageSlug(homeSource, 'start', 'home.start'),
|
|
825
910
|
},
|
|
826
911
|
navigation: {
|
|
827
912
|
landingDocumentTitles: titleList(
|