@ctcstack/ctcdocs-core 0.17.0 → 0.18.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.
@@ -23,6 +23,8 @@ export interface CorpusDocument {
23
23
  readonly shortId?: string;
24
24
  readonly title?: string;
25
25
  readonly modified?: string;
26
+ /** The Google Doc or PDF in Drive, as the page links it. */
27
+ readonly source?: string;
26
28
  }
27
29
  export interface CorpusStructure {
28
30
  readonly rootFolderId: string | null;
@@ -65,6 +65,7 @@ export function parseCorpusStructure(manifest) {
65
65
  const shortId = optional('shortId');
66
66
  const title = optional('displayTitle');
67
67
  const modified = optional('googleModifiedTime');
68
+ const source = optional('sourceUrl');
68
69
  documents.set(id, {
69
70
  id,
70
71
  parentId: field(raw, 'googleParentId', path, true),
@@ -72,6 +73,7 @@ export function parseCorpusStructure(manifest) {
72
73
  ...(shortId ? { shortId } : {}),
73
74
  ...(title ? { title } : {}),
74
75
  ...(modified ? { modified } : {}),
76
+ ...(source ? { source } : {}),
75
77
  ...(published === '*' ||
76
78
  (Array.isArray(published) &&
77
79
  published.every((group) => typeof group === 'string'))
@@ -30,4 +30,15 @@ export declare function publishedFileUrl(googleFileId: string, file: string): st
30
30
  * titles, which come from Drive names and may contain any of them.
31
31
  */
32
32
  export declare function inlineMarkdown(value: string): string;
33
+ /**
34
+ * The page's Markdown without its front matter: its title as a heading and
35
+ * its body. It is what the MCP server stores, indexes and returns from
36
+ * `fetch` (ADR-042), so it is also what the sync measures (ADR-043).
37
+ */
38
+ export declare function publishedMarkdownBody(input: Pick<PublishedMarkdownInput, 'title' | 'ownershipHeader' | 'body' | 'stableSlugs' | 'permanentLinks'>): string;
39
+ /**
40
+ * The page's Markdown version (ADR-010): front matter, then
41
+ * `publishedMarkdownBody`. Each front matter value is one JSON string, so no
42
+ * value spans a line and the first `---` line after the opening one closes it.
43
+ */
33
44
  export declare function serializePublishedMarkdown(input: PublishedMarkdownInput): string;
@@ -98,8 +98,25 @@ export function inlineMarkdown(value) {
98
98
  .trim()
99
99
  .replace(/([\\`*_[\]<>#])/gu, '\\$1');
100
100
  }
101
- export function serializePublishedMarkdown(input) {
101
+ /**
102
+ * The page's Markdown without its front matter: its title as a heading and
103
+ * its body. It is what the MCP server stores, indexes and returns from
104
+ * `fetch` (ADR-042), so it is also what the sync measures (ADR-043).
105
+ */
106
+ export function publishedMarkdownBody(input) {
102
107
  const body = rewriteInternalLinks(cleanBody(input.body, input.ownershipHeader), input.stableSlugs, input.permanentLinks);
108
+ return [
109
+ `# ${inlineMarkdown(input.title)}`,
110
+ '',
111
+ ...(body ? [body, ''] : []),
112
+ ].join('\n');
113
+ }
114
+ /**
115
+ * The page's Markdown version (ADR-010): front matter, then
116
+ * `publishedMarkdownBody`. Each front matter value is one JSON string, so no
117
+ * value spans a line and the first `---` line after the opening one closes it.
118
+ */
119
+ export function serializePublishedMarkdown(input) {
103
120
  return [
104
121
  '---',
105
122
  `title: ${yamlString(input.title)}`,
@@ -110,8 +127,6 @@ export function serializePublishedMarkdown(input) {
110
127
  `content_hash: ${yamlString(input.contentHash)}`,
111
128
  '---',
112
129
  '',
113
- `# ${inlineMarkdown(input.title)}`,
114
- '',
115
- ...(body ? [body, ''] : []),
130
+ publishedMarkdownBody(input),
116
131
  ].join('\n');
117
132
  }
@@ -152,13 +152,16 @@ export interface McpSearchConfiguration {
152
152
  readonly chunks: number;
153
153
  /**
154
154
  * The vector similarity below which AI Search drops a chunk, 0 to 1. It
155
- * applies to vector similarity alone, so a keyword match far from the
156
- * question in meaning is dropped too when it is set high.
155
+ * drops only chunks vector search alone found, and AI Search ignores it
156
+ * while reranking is on (ADR-042), so it acts only with reranking off.
157
157
  */
158
158
  readonly vectorThreshold: number;
159
159
  /** Whether a keyword match needs every word of the query, or any. */
160
160
  readonly keywordMatch: 'and' | 'or';
161
- /** Neighbouring chunks AI Search adds on each side of a match: 0 to 3. */
161
+ /**
162
+ * Neighbouring chunks AI Search joins to each side of a match: 0 to 3. A
163
+ * chunk of AI Search's default size is already longer than a passage.
164
+ */
162
165
  readonly contextChunks: number;
163
166
  readonly reranking: {
164
167
  readonly enabled: boolean;
@@ -21,7 +21,7 @@ export const MCP_DEFAULTS = Object.freeze({
21
21
  chunks: 50,
22
22
  vectorThreshold: 0.2,
23
23
  keywordMatch: 'or',
24
- contextChunks: 1,
24
+ contextChunks: 0,
25
25
  reranking: Object.freeze({
26
26
  enabled: true,
27
27
  model: '@cf/baai/bge-reranker-base',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctcstack/ctcdocs-core",
3
- "version": "0.17.0",
3
+ "version": "0.18.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",
@@ -37,6 +37,8 @@ export interface CorpusDocument {
37
37
  readonly shortId?: string;
38
38
  readonly title?: string;
39
39
  readonly modified?: string;
40
+ /** The Google Doc or PDF in Drive, as the page links it. */
41
+ readonly source?: string;
40
42
  }
41
43
 
42
44
  export interface CorpusStructure {
@@ -114,6 +116,7 @@ export function parseCorpusStructure(manifest: unknown): CorpusStructure {
114
116
  const shortId = optional('shortId');
115
117
  const title = optional('displayTitle');
116
118
  const modified = optional('googleModifiedTime');
119
+ const source = optional('sourceUrl');
117
120
  documents.set(id, {
118
121
  id,
119
122
  parentId: field(raw, 'googleParentId', path, true),
@@ -121,6 +124,7 @@ export function parseCorpusStructure(manifest: unknown): CorpusStructure {
121
124
  ...(shortId ? { shortId } : {}),
122
125
  ...(title ? { title } : {}),
123
126
  ...(modified ? { modified } : {}),
127
+ ...(source ? { source } : {}),
124
128
  ...(published === '*' ||
125
129
  (Array.isArray(published) &&
126
130
  published.every((group) => typeof group === 'string'))
@@ -153,14 +153,37 @@ export function inlineMarkdown(value: string): string {
153
153
  .replace(/([\\`*_[\]<>#])/gu, '\\$1');
154
154
  }
155
155
 
156
- export function serializePublishedMarkdown(
157
- input: PublishedMarkdownInput,
156
+ /**
157
+ * The page's Markdown without its front matter: its title as a heading and
158
+ * its body. It is what the MCP server stores, indexes and returns from
159
+ * `fetch` (ADR-042), so it is also what the sync measures (ADR-043).
160
+ */
161
+ export function publishedMarkdownBody(
162
+ input: Pick<
163
+ PublishedMarkdownInput,
164
+ 'title' | 'ownershipHeader' | 'body' | 'stableSlugs' | 'permanentLinks'
165
+ >,
158
166
  ): string {
159
167
  const body = rewriteInternalLinks(
160
168
  cleanBody(input.body, input.ownershipHeader),
161
169
  input.stableSlugs,
162
170
  input.permanentLinks,
163
171
  );
172
+ return [
173
+ `# ${inlineMarkdown(input.title)}`,
174
+ '',
175
+ ...(body ? [body, ''] : []),
176
+ ].join('\n');
177
+ }
178
+
179
+ /**
180
+ * The page's Markdown version (ADR-010): front matter, then
181
+ * `publishedMarkdownBody`. Each front matter value is one JSON string, so no
182
+ * value spans a line and the first `---` line after the opening one closes it.
183
+ */
184
+ export function serializePublishedMarkdown(
185
+ input: PublishedMarkdownInput,
186
+ ): string {
164
187
  return [
165
188
  '---',
166
189
  `title: ${yamlString(input.title)}`,
@@ -171,8 +194,6 @@ export function serializePublishedMarkdown(
171
194
  `content_hash: ${yamlString(input.contentHash)}`,
172
195
  '---',
173
196
  '',
174
- `# ${inlineMarkdown(input.title)}`,
175
- '',
176
- ...(body ? [body, ''] : []),
197
+ publishedMarkdownBody(input),
177
198
  ].join('\n');
178
199
  }
@@ -183,13 +183,16 @@ export interface McpSearchConfiguration {
183
183
  readonly chunks: number;
184
184
  /**
185
185
  * The vector similarity below which AI Search drops a chunk, 0 to 1. It
186
- * applies to vector similarity alone, so a keyword match far from the
187
- * question in meaning is dropped too when it is set high.
186
+ * drops only chunks vector search alone found, and AI Search ignores it
187
+ * while reranking is on (ADR-042), so it acts only with reranking off.
188
188
  */
189
189
  readonly vectorThreshold: number;
190
190
  /** Whether a keyword match needs every word of the query, or any. */
191
191
  readonly keywordMatch: 'and' | 'or';
192
- /** Neighbouring chunks AI Search adds on each side of a match: 0 to 3. */
192
+ /**
193
+ * Neighbouring chunks AI Search joins to each side of a match: 0 to 3. A
194
+ * chunk of AI Search's default size is already longer than a passage.
195
+ */
193
196
  readonly contextChunks: number;
194
197
  readonly reranking: {
195
198
  readonly enabled: boolean;
@@ -224,7 +227,7 @@ export const MCP_DEFAULTS: Omit<McpConfiguration, 'enabled'> = Object.freeze({
224
227
  chunks: 50,
225
228
  vectorThreshold: 0.2,
226
229
  keywordMatch: 'or',
227
- contextChunks: 1,
230
+ contextChunks: 0,
228
231
  reranking: Object.freeze({
229
232
  enabled: true,
230
233
  model: '@cf/baai/bge-reranker-base',