jamdesk 1.1.139 → 1.1.141
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/__tests__/unit/config-suggestions.test.js +2 -2
- package/dist/__tests__/unit/config-suggestions.test.js.map +1 -1
- package/dist/lib/config-suggestions.d.ts.map +1 -1
- package/dist/lib/config-suggestions.js +1 -3
- package/dist/lib/config-suggestions.js.map +1 -1
- package/package.json +1 -1
- package/vendored/components/mdx/MermaidInner.tsx +96 -5
- package/vendored/lib/config-suggestions.ts +1 -3
- package/vendored/lib/vector-store.ts +153 -69
- package/vendored/workspace-package-lock.json +12 -12
|
@@ -7,7 +7,7 @@ describe('suggestConfigImprovements (CLI)', () => {
|
|
|
7
7
|
expect(suggestions).toHaveLength(3);
|
|
8
8
|
expect(suggestions.map((s) => s.link)).toEqual([
|
|
9
9
|
'https://jamdesk.com/docs/customization/branding#favicon',
|
|
10
|
-
'https://jamdesk.com/docs/customization/branding#
|
|
10
|
+
'https://jamdesk.com/docs/customization/branding#description',
|
|
11
11
|
'https://jamdesk.com/docs/customization/branding#logo',
|
|
12
12
|
]);
|
|
13
13
|
});
|
|
@@ -24,7 +24,7 @@ describe('suggestConfigImprovements (CLI)', () => {
|
|
|
24
24
|
it('empty object logo and whitespace description count as missing', () => {
|
|
25
25
|
const suggestions = suggestConfigImprovements({ favicon: '/f.svg', logo: {}, description: ' ' });
|
|
26
26
|
expect(suggestions.map((s) => s.link)).toEqual([
|
|
27
|
-
'https://jamdesk.com/docs/customization/branding#
|
|
27
|
+
'https://jamdesk.com/docs/customization/branding#description',
|
|
28
28
|
'https://jamdesk.com/docs/customization/branding#logo',
|
|
29
29
|
]);
|
|
30
30
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-suggestions.test.js","sourceRoot":"","sources":["../../../src/__tests__/unit/config-suggestions.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,yBAAyB,EAAyB,MAAM,iCAAiC,CAAC;AAEnG,MAAM,QAAQ,GAAG,CAAC,MAAwB,EAAE,EAAE,CAC5C,yBAAyB,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;AAE1D,QAAQ,CAAC,iCAAiC,EAAE,GAAG,EAAE;IAC/C,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACpE,MAAM,WAAW,GAAG,yBAAyB,CAAC,EAAE,CAAC,CAAC;QAClD,MAAM,CAAC,WAAW,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACpC,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC;YAC7C,yDAAyD;YACzD,
|
|
1
|
+
{"version":3,"file":"config-suggestions.test.js","sourceRoot":"","sources":["../../../src/__tests__/unit/config-suggestions.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,yBAAyB,EAAyB,MAAM,iCAAiC,CAAC;AAEnG,MAAM,QAAQ,GAAG,CAAC,MAAwB,EAAE,EAAE,CAC5C,yBAAyB,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;AAE1D,QAAQ,CAAC,iCAAiC,EAAE,GAAG,EAAE;IAC/C,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACpE,MAAM,WAAW,GAAG,yBAAyB,CAAC,EAAE,CAAC,CAAC;QAClD,MAAM,CAAC,WAAW,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACpC,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC;YAC7C,yDAAyD;YACzD,6DAA6D;YAC7D,sDAAsD;SACvD,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,MAAM,CAAC,yBAAyB,CAAC;YAC/B,OAAO,EAAE,cAAc;YACvB,IAAI,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE;YAC5B,WAAW,EAAE,MAAM;SACpB,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAClG,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+DAA+D,EAAE,GAAG,EAAE;QACvE,MAAM,WAAW,GAAG,yBAAyB,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;QAClG,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC;YAC7C,6DAA6D;YAC7D,sDAAsD;SACvD,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,WAAW,GAAG,yBAAyB,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,CAAC;QACxH,MAAM,CAAC,WAAW,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACpC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kFAAkF,EAAE,GAAG,EAAE;QAC1F,MAAM,CAAC,yBAAyB,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;YAClE,kGAAkG;YAClG,6FAA6F;YAC7F,sFAAsF;SACvF,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-suggestions.d.ts","sourceRoot":"","sources":["../../src/lib/config-suggestions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,MAAM,GAAG;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACrD,IAAI,CAAC,EAAE,MAAM,GAAG;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAWD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,EAAE,
|
|
1
|
+
{"version":3,"file":"config-suggestions.d.ts","sourceRoot":"","sources":["../../src/lib/config-suggestions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,MAAM,GAAG;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACrD,IAAI,CAAC,EAAE,MAAM,GAAG;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAWD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,EAAE,CAqBtF"}
|
|
@@ -29,9 +29,7 @@ export function suggestConfigImprovements(config) {
|
|
|
29
29
|
if (!config.description || config.description.trim() === '') {
|
|
30
30
|
suggestions.push({
|
|
31
31
|
message: 'Add a site description to improve how your docs appear in search results and social shares.',
|
|
32
|
-
|
|
33
|
-
// page's "Site Name" section (no #description anchor exists yet).
|
|
34
|
-
link: `${BRANDING_DOCS_URL}#site-name`,
|
|
32
|
+
link: `${BRANDING_DOCS_URL}#description`,
|
|
35
33
|
});
|
|
36
34
|
}
|
|
37
35
|
if (!hasImageValue(config.logo)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-suggestions.js","sourceRoot":"","sources":["../../src/lib/config-suggestions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAiBH,MAAM,iBAAiB,GAAG,iDAAiD,CAAC;AAE5E,8EAA8E;AAC9E,SAAS,aAAa,CAAC,KAA6D;IAClF,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzB,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;IAC1D,OAAO,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,MAAwB;IAChE,MAAM,WAAW,GAAuB,EAAE,CAAC;IAC3C,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;QACnC,WAAW,CAAC,IAAI,CAAC;YACf,OAAO,EAAE,kGAAkG;YAC3G,IAAI,EAAE,GAAG,iBAAiB,UAAU;SACrC,CAAC,CAAC;IACL,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,WAAW,IAAI,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC5D,WAAW,CAAC,IAAI,CAAC;YACf,OAAO,EAAE,6FAA6F;YACtG,
|
|
1
|
+
{"version":3,"file":"config-suggestions.js","sourceRoot":"","sources":["../../src/lib/config-suggestions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAiBH,MAAM,iBAAiB,GAAG,iDAAiD,CAAC;AAE5E,8EAA8E;AAC9E,SAAS,aAAa,CAAC,KAA6D;IAClF,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzB,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;IAC1D,OAAO,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,MAAwB;IAChE,MAAM,WAAW,GAAuB,EAAE,CAAC;IAC3C,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;QACnC,WAAW,CAAC,IAAI,CAAC;YACf,OAAO,EAAE,kGAAkG;YAC3G,IAAI,EAAE,GAAG,iBAAiB,UAAU;SACrC,CAAC,CAAC;IACL,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,WAAW,IAAI,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC5D,WAAW,CAAC,IAAI,CAAC;YACf,OAAO,EAAE,6FAA6F;YACtG,IAAI,EAAE,GAAG,iBAAiB,cAAc;SACzC,CAAC,CAAC;IACL,CAAC;IACD,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QAChC,WAAW,CAAC,IAAI,CAAC;YACf,OAAO,EAAE,sFAAsF;YAC/F,IAAI,EAAE,GAAG,iBAAiB,OAAO;SAClC,CAAC,CAAC;IACL,CAAC;IACD,OAAO,WAAW,CAAC;AACrB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jamdesk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.141",
|
|
4
4
|
"description": "CLI for Jamdesk — build, preview, and deploy documentation sites from MDX. Dev server with hot reload, 50+ components, OpenAPI support, AI search, and Mintlify migration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jamdesk",
|
|
@@ -47,8 +47,29 @@ interface ColorPalette {
|
|
|
47
47
|
ganttSections: string[];
|
|
48
48
|
ganttGridLine: string;
|
|
49
49
|
gitBranchColors: string[];
|
|
50
|
+
pieSliceStroke: string;
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
// Mermaid's built-in palettes (including 'neutral') color pie slices with
|
|
54
|
+
// washed-out, near-white grays: low-contrast on a light background and wiped to
|
|
55
|
+
// transparent by the dark-mode path-fill inversion below — so slices render with
|
|
56
|
+
// no visible color in either mode. We override slice + legend fills with a
|
|
57
|
+
// distinct, mid-tone palette that reads on both backgrounds, mirroring the
|
|
58
|
+
// curated colors already used for gantt bars and git branches. Every entry has at
|
|
59
|
+
// least one channel < 200, so isLightColor() never flags them.
|
|
60
|
+
const PIE_SLICE_COLORS = [
|
|
61
|
+
'#3b82f6', // blue
|
|
62
|
+
'#ef4444', // red
|
|
63
|
+
'#22c55e', // green
|
|
64
|
+
'#f59e0b', // amber
|
|
65
|
+
'#a855f7', // purple
|
|
66
|
+
'#14b8a6', // teal
|
|
67
|
+
'#ec4899', // pink
|
|
68
|
+
'#f97316', // orange
|
|
69
|
+
'#64748b', // slate
|
|
70
|
+
'#84cc16', // lime
|
|
71
|
+
];
|
|
72
|
+
|
|
52
73
|
const darkPalette: ColorPalette = {
|
|
53
74
|
text: '#e5e7eb',
|
|
54
75
|
line: '#9ca3af',
|
|
@@ -59,6 +80,7 @@ const darkPalette: ColorPalette = {
|
|
|
59
80
|
ganttSections: ['#1e1e3f', '#2d2d1e', '#1e2d1e'],
|
|
60
81
|
ganttGridLine: '#374151',
|
|
61
82
|
gitBranchColors: ['#3b82f6', '#eab308', '#22c55e', '#f97316', '#ec4899', '#a78bfa'],
|
|
83
|
+
pieSliceStroke: '#1f2937', // dark slate — crisp separation between slices on a dark bg
|
|
62
84
|
};
|
|
63
85
|
|
|
64
86
|
const lightPalette: ColorPalette = {
|
|
@@ -71,6 +93,7 @@ const lightPalette: ColorPalette = {
|
|
|
71
93
|
ganttSections: ['#f0f0ff', '#fff8e6', '#f0fff0'],
|
|
72
94
|
ganttGridLine: '#e5e7eb',
|
|
73
95
|
gitBranchColors: ['#3b82f6', '#eab308', '#22c55e', '#f97316', '#ec4899', '#8b5cf6'],
|
|
96
|
+
pieSliceStroke: '#ffffff', // white — crisp separation between slices on a light bg
|
|
74
97
|
};
|
|
75
98
|
|
|
76
99
|
// Styling helper utilities
|
|
@@ -129,7 +152,46 @@ function applyClassDiagramStyles(svgEl: SVGElement, palette: ColorPalette): void
|
|
|
129
152
|
}
|
|
130
153
|
|
|
131
154
|
function applyPieChartStyles(svgEl: SVGElement, palette: ColorPalette): void {
|
|
155
|
+
// Title and legend text follow the theme text color.
|
|
132
156
|
styleElements(svgEl, '.pieLabel, .legend text, .pieTitleText', { fill: palette.text });
|
|
157
|
+
|
|
158
|
+
const slices = svgEl.querySelectorAll<SVGPathElement>('path.pieCircle');
|
|
159
|
+
if (slices.length === 0) return; // not a pie chart
|
|
160
|
+
|
|
161
|
+
// Re-color slices and legend swatches from PIE_SLICE_COLORS. Each distinct
|
|
162
|
+
// original (washed-out) color maps to one vivid color, recorded as the slices
|
|
163
|
+
// are walked, then reused for the legend so a slice and its legend entry —
|
|
164
|
+
// which share mermaid's original ordinal color — get the same override.
|
|
165
|
+
const colorMap = new Map<string, string>();
|
|
166
|
+
let nextColor = 0;
|
|
167
|
+
const overrideFor = (rawFill: string | null | undefined): string => {
|
|
168
|
+
const key = normalizeColor(rawFill) ?? `pos-${nextColor}`;
|
|
169
|
+
let vivid = colorMap.get(key);
|
|
170
|
+
if (!vivid) {
|
|
171
|
+
vivid = PIE_SLICE_COLORS[nextColor % PIE_SLICE_COLORS.length];
|
|
172
|
+
nextColor += 1;
|
|
173
|
+
colorMap.set(key, vivid);
|
|
174
|
+
}
|
|
175
|
+
return vivid;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
slices.forEach((slice) => {
|
|
179
|
+
const vivid = overrideFor(slice.style.fill || slice.getAttribute('fill'));
|
|
180
|
+
slice.style.fill = vivid;
|
|
181
|
+
slice.style.stroke = palette.pieSliceStroke;
|
|
182
|
+
slice.style.strokeWidth = '2';
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Legend swatches: <g class="legend"> > rect, fill set via inline style.
|
|
186
|
+
svgEl.querySelectorAll<SVGRectElement>('.legend rect').forEach((swatch) => {
|
|
187
|
+
const vivid = overrideFor(swatch.style.fill || swatch.getAttribute('fill'));
|
|
188
|
+
swatch.style.fill = vivid;
|
|
189
|
+
swatch.style.stroke = vivid;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Percentage labels sit on top of the slices — white reads on every palette
|
|
193
|
+
// tone. The dark-mode text inversion spares text.slice so this survives there.
|
|
194
|
+
styleElements(svgEl, 'text.slice', { fill: '#ffffff' });
|
|
133
195
|
}
|
|
134
196
|
|
|
135
197
|
// Check if an RGB color is light (r,g,b > threshold)
|
|
@@ -147,6 +209,26 @@ function isDarkColor(rgbString: string, threshold = 150): boolean {
|
|
|
147
209
|
return r < threshold && g < threshold && b < threshold;
|
|
148
210
|
}
|
|
149
211
|
|
|
212
|
+
// Normalize a CSS color (hex or rgb()) to a canonical "r,g,b" string. A pie
|
|
213
|
+
// slice's fill arrives as a hex attribute (e.g. "#ECECFF") while its matching
|
|
214
|
+
// legend swatch's fill is an inline style the browser reports as "rgb(...)" —
|
|
215
|
+
// normalizing lets the same underlying color compare equal across both formats,
|
|
216
|
+
// so a slice and its legend entry get the same override color. Returns null when
|
|
217
|
+
// the value can't be parsed (caller falls back to a positional key).
|
|
218
|
+
function normalizeColor(value: string | null | undefined): string | null {
|
|
219
|
+
if (!value) return null;
|
|
220
|
+
const v = value.trim();
|
|
221
|
+
const rgb = v.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
|
|
222
|
+
if (rgb) return `${+rgb[1]},${+rgb[2]},${+rgb[3]}`;
|
|
223
|
+
let hex = v.replace(/^#/, '');
|
|
224
|
+
if (hex.length === 3) hex = hex.split('').map((c) => c + c).join('');
|
|
225
|
+
if (hex.length === 6 && /^[0-9a-f]{6}$/i.test(hex)) {
|
|
226
|
+
const n = parseInt(hex, 16);
|
|
227
|
+
return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
150
232
|
// Shared styles applied to both light and dark modes
|
|
151
233
|
function applyCommonStyles(svgEl: SVGElement, palette: ColorPalette): void {
|
|
152
234
|
// Cluster/subgraph backgrounds
|
|
@@ -174,15 +256,22 @@ function applyCommonStyles(svgEl: SVGElement, palette: ColorPalette): void {
|
|
|
174
256
|
|
|
175
257
|
// Dark mode requires additional color inversions
|
|
176
258
|
function applyDarkModeInversions(svgEl: SVGElement, palette: ColorPalette): void {
|
|
177
|
-
// Make background rects transparent
|
|
259
|
+
// Make background rects transparent. Pie legend swatches (.legend rect) are
|
|
260
|
+
// re-colored in applyPieChartStyles — leave their fills alone.
|
|
178
261
|
svgEl.querySelectorAll('rect').forEach((rect) => {
|
|
179
|
-
if (
|
|
262
|
+
if (
|
|
263
|
+
!rect.closest('.cluster') &&
|
|
264
|
+
!rect.closest('.node') &&
|
|
265
|
+
!rect.classList.contains('actor') &&
|
|
266
|
+
!rect.closest('.legend')
|
|
267
|
+
) {
|
|
180
268
|
(rect as SVGElement).style.fill = 'transparent';
|
|
181
269
|
}
|
|
182
270
|
});
|
|
183
271
|
|
|
184
|
-
// Invert text colors
|
|
185
|
-
|
|
272
|
+
// Invert text colors. text.slice (pie percentage labels) is excluded so the
|
|
273
|
+
// white set in applyPieChartStyles stays legible on the colored slices.
|
|
274
|
+
styleElements(svgEl, 'text:not(.slice), .nodeLabel, .edgeLabel, .label, tspan', { fill: palette.text });
|
|
186
275
|
styleElements(svgEl, 'text.actor, .messageText, .labelText, .loopText, .noteText', { fill: palette.text });
|
|
187
276
|
|
|
188
277
|
// Invert foreignObject text and clear light backgrounds
|
|
@@ -198,8 +287,10 @@ function applyDarkModeInversions(svgEl: SVGElement, palette: ColorPalette): void
|
|
|
198
287
|
}
|
|
199
288
|
});
|
|
200
289
|
|
|
201
|
-
// Invert lines and paths
|
|
290
|
+
// Invert lines and paths. Pie slices (path.pieCircle) carry their own colors
|
|
291
|
+
// from applyPieChartStyles — skip them so the fill isn't wiped to transparent.
|
|
202
292
|
svgEl.querySelectorAll('path, line').forEach((el) => {
|
|
293
|
+
if ((el as Element).classList.contains('pieCircle')) return;
|
|
203
294
|
const computed = window.getComputedStyle(el);
|
|
204
295
|
if (computed.stroke && computed.stroke !== 'none') {
|
|
205
296
|
(el as SVGElement).style.stroke = palette.line;
|
|
@@ -44,9 +44,7 @@ const SUGGESTION_DEFS: SuggestionDef[] = [
|
|
|
44
44
|
{
|
|
45
45
|
type: 'missing_description',
|
|
46
46
|
message: 'Add a site description to improve how your docs appear in search results and social shares.',
|
|
47
|
-
|
|
48
|
-
// page's "Site Name" section (no #description anchor exists yet).
|
|
49
|
-
link: `${BRANDING_DOCS_URL}#site-name`,
|
|
47
|
+
link: `${BRANDING_DOCS_URL}#description`,
|
|
50
48
|
isMissing: (c) => !c.description || c.description.trim() === '',
|
|
51
49
|
},
|
|
52
50
|
{
|
|
@@ -52,15 +52,88 @@ const MIN_SCORE = 0.3;
|
|
|
52
52
|
/** Max chunks per page — raised from 3 to 4 to match broader topK retrieval budget */
|
|
53
53
|
const MAX_CHUNKS_PER_PAGE = 4;
|
|
54
54
|
|
|
55
|
-
/**
|
|
56
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Hard deadline for the build-time embedding write (reset + all upsert batches
|
|
57
|
+
* combined). A transient Upstash stall must fail this step FAST — it is
|
|
58
|
+
* non-fatal to the build (build.ts swallows it) — instead of hanging the single
|
|
59
|
+
* Cloud Run build instance until the 21-minute stale-build watchdog reaps it as
|
|
60
|
+
* a false "timed out" failure (kapptivate, 2026-06-11). Healthy writes finish in
|
|
61
|
+
* <15s; 90s tolerates a slow-but-alive Upstash while still failing ~14× faster
|
|
62
|
+
* than the watchdog.
|
|
63
|
+
*/
|
|
64
|
+
const UPSERT_TIMEOUT_MS = 90_000;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Hard deadline for a live chat/search retrieval. Queries normally finish in
|
|
68
|
+
* <500ms; bound them so a stalled Upstash cannot hang the chat SSE request. The
|
|
69
|
+
* chat/search routes already treat a rejected query gracefully (rewrite path
|
|
70
|
+
* resolves to [], original-query reject → 503 chat / 502 docs-search), so a fast
|
|
71
|
+
* reject is preferable to an open-ended hang.
|
|
72
|
+
*/
|
|
73
|
+
const QUERY_TIMEOUT_MS = 10_000;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create a namespaced Upstash Vector index for a project.
|
|
77
|
+
*
|
|
78
|
+
* `signal` is REQUIRED on purpose: it must come from a withDeadline() wrapper.
|
|
79
|
+
* A bare getNamespace() would issue UNBOUNDED reset/upsert/query requests — the
|
|
80
|
+
* exact hang this module guards against (kapptivate, 2026-06-11) — so making the
|
|
81
|
+
* param non-optional forces every (current and future) caller through a deadline
|
|
82
|
+
* at compile time rather than by remembering a comment. The signal is forwarded
|
|
83
|
+
* to the SDK so an aborted deadline closes the underlying socket.
|
|
84
|
+
*/
|
|
85
|
+
function getNamespace(projectId: string, signal: AbortSignal) {
|
|
57
86
|
const index = new Index({
|
|
58
87
|
url: process.env.UPSTASH_VECTOR_REST_URL!,
|
|
59
88
|
token: process.env.UPSTASH_VECTOR_REST_TOKEN!,
|
|
89
|
+
signal,
|
|
60
90
|
});
|
|
61
91
|
return index.namespace(projectId);
|
|
62
92
|
}
|
|
63
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Run an Upstash operation under a hard deadline.
|
|
96
|
+
*
|
|
97
|
+
* The Promise.race timer is the GUARANTEE: it rejects the caller at `ms` no
|
|
98
|
+
* matter what the SDK does. The AbortSignal handed to `op` is best-effort —
|
|
99
|
+
* @upstash/vector 1.2.3 does NOT propagate an aborted fetch as a rejection; its
|
|
100
|
+
* request loop catches the AbortError and synthesizes a 200 "success". What the
|
|
101
|
+
* signal still buys us is closing the underlying socket so a hung connection
|
|
102
|
+
* does not linger past the deadline. So: timer = correctness, signal = resource
|
|
103
|
+
* hygiene — do NOT remove the timer believing the signal suffices.
|
|
104
|
+
*
|
|
105
|
+
* On timeout this REJECTS — callers decide whether that is fatal. A genuine
|
|
106
|
+
* pre-deadline rejection (e.g. the SDK exhausting its retries on a flapping
|
|
107
|
+
* endpoint) propagates as-is so the real Upstash error is logged rather than a
|
|
108
|
+
* generic timeout.
|
|
109
|
+
*/
|
|
110
|
+
async function withDeadline<T>(
|
|
111
|
+
ms: number,
|
|
112
|
+
operation: string,
|
|
113
|
+
op: (signal: AbortSignal) => Promise<T>,
|
|
114
|
+
): Promise<T> {
|
|
115
|
+
const controller = new AbortController();
|
|
116
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
117
|
+
const deadline = new Promise<never>((_, reject) => {
|
|
118
|
+
timer = setTimeout(() => {
|
|
119
|
+
controller.abort();
|
|
120
|
+
reject(new Error(`Upstash ${operation} timed out after ${ms}ms`));
|
|
121
|
+
}, ms);
|
|
122
|
+
});
|
|
123
|
+
const work = op(controller.signal);
|
|
124
|
+
// Swallow the late settlement of the losing promise so it cannot bubble as an
|
|
125
|
+
// unhandledRejection after the deadline already won the race. (Load-bearing in
|
|
126
|
+
// prod for the genuine-error case; on an abort the SDK resolves `work` with a
|
|
127
|
+
// synthetic 200, so this no-ops.) The genuine error still reaches the caller
|
|
128
|
+
// via the race below.
|
|
129
|
+
work.catch(() => {});
|
|
130
|
+
try {
|
|
131
|
+
return await Promise.race([work, deadline]);
|
|
132
|
+
} finally {
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
64
137
|
/**
|
|
65
138
|
* Replace all vectors for a project with fresh chunks.
|
|
66
139
|
*
|
|
@@ -76,33 +149,35 @@ export async function upsertChunks(
|
|
|
76
149
|
projectId: string,
|
|
77
150
|
chunks: EmbeddingChunk[],
|
|
78
151
|
): Promise<void> {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
152
|
+
await withDeadline(UPSERT_TIMEOUT_MS, 'upsert', async signal => {
|
|
153
|
+
const ns = getNamespace(projectId, signal);
|
|
154
|
+
|
|
155
|
+
await ns.reset();
|
|
156
|
+
|
|
157
|
+
for (let i = 0; i < chunks.length; i += BATCH_SIZE) {
|
|
158
|
+
const batch = chunks.slice(i, i + BATCH_SIZE);
|
|
159
|
+
await ns.upsert(
|
|
160
|
+
batch.map(c => {
|
|
161
|
+
const metadata: ChunkMetadata = {
|
|
162
|
+
pageSlug: c.pageSlug,
|
|
163
|
+
sectionHeading: c.sectionHeading,
|
|
164
|
+
pageTitle: c.pageTitle,
|
|
165
|
+
content: c.content.length > MAX_METADATA_CONTENT_CHARS
|
|
166
|
+
? c.content.slice(0, MAX_METADATA_CONTENT_CHARS) + '...'
|
|
167
|
+
: c.content,
|
|
168
|
+
};
|
|
169
|
+
if (c.locale) metadata.locale = c.locale; // omit when null
|
|
170
|
+
return {
|
|
171
|
+
id: c.id,
|
|
172
|
+
// Prefix + body goes to Upstash for embedding/BM25; metadata.content
|
|
173
|
+
// stays prefix-free so consumers display clean body text.
|
|
174
|
+
data: c.prefix + c.content,
|
|
175
|
+
metadata,
|
|
176
|
+
};
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
106
181
|
}
|
|
107
182
|
|
|
108
183
|
/**
|
|
@@ -205,48 +280,50 @@ export async function querySimilarChunks(
|
|
|
205
280
|
topK = 5,
|
|
206
281
|
options: { locale?: string } = {},
|
|
207
282
|
): Promise<Array<ChunkMetadata & { score: number }>> {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
283
|
+
return withDeadline(QUERY_TIMEOUT_MS, 'query', async signal => {
|
|
284
|
+
const ns = getNamespace(projectId, signal);
|
|
285
|
+
const locale = options.locale ? normalizeLocaleForFilter(options.locale) : undefined;
|
|
286
|
+
// Defense-in-depth: A5 rejects malformed locales at the API boundary. If a
|
|
287
|
+
// truthy locale here normalizes to empty, A5's guard was bypassed (test or
|
|
288
|
+
// internal caller) — surface it loudly rather than silently dropping the filter.
|
|
289
|
+
if (options.locale && !locale) {
|
|
290
|
+
logger.warn('vector-store: locale normalized to empty — filter skipped', { rawLocale: options.locale });
|
|
291
|
+
}
|
|
292
|
+
const filter = locale ? buildLocaleFilter(locale) : undefined;
|
|
293
|
+
// When filtering, raise effective topK by ~33% so we still get ~topK chunks
|
|
294
|
+
// back from a mixed-language namespace where filtering cuts the candidate set.
|
|
295
|
+
const effectiveTopK = filter ? Math.ceil(topK * 1.33) : topK;
|
|
296
|
+
const queryParams = { topK: effectiveTopK, includeMetadata: true as const, filter };
|
|
297
|
+
|
|
298
|
+
const topicQuery = extractTopicQuery(queryText);
|
|
299
|
+
|
|
300
|
+
// Dual-query: topic query is the PRIMARY source (better topical relevance);
|
|
301
|
+
// the full query fills remaining slots with unique results only.
|
|
302
|
+
let merged: Array<ChunkMetadata & { score: number }>;
|
|
303
|
+
if (topicQuery) {
|
|
304
|
+
const [fullResults, topicResults] = await Promise.all([
|
|
305
|
+
queryWithFallback(ns, { data: queryText, ...queryParams }),
|
|
306
|
+
queryWithFallback(ns, { data: topicQuery, ...queryParams }),
|
|
307
|
+
]);
|
|
308
|
+
merged = filterAndMerge([topicResults, fullResults], topK);
|
|
309
|
+
} else {
|
|
310
|
+
const results = await queryWithFallback(ns, { data: queryText, ...queryParams });
|
|
311
|
+
merged = filterAndMerge([results], topK);
|
|
312
|
+
}
|
|
237
313
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
314
|
+
// Telemetry: a filtered query that returns materially fewer chunks than
|
|
315
|
+
// requested signals the locale filter is hurting recall (project skewed away
|
|
316
|
+
// from the requested locale, or filter syntax matched a near-empty subset).
|
|
317
|
+
// Surface it so we can decide whether to widen the carve-out, increase the
|
|
318
|
+
// 1.33× boost, or rebuild the project's index.
|
|
319
|
+
if (filter && merged.length < Math.ceil(topK / 2)) {
|
|
320
|
+
logger.warn('vector-store: locale filter under-fills topK', {
|
|
321
|
+
projectId, locale, returned: merged.length, requested: topK,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
248
324
|
|
|
249
|
-
|
|
325
|
+
return merged;
|
|
326
|
+
});
|
|
250
327
|
}
|
|
251
328
|
|
|
252
329
|
/**
|
|
@@ -309,6 +386,13 @@ export async function deleteProjectNamespace(slug: string): Promise<boolean> {
|
|
|
309
386
|
// Ctor failures (e.g., a future SDK adding URL-format validation) are
|
|
310
387
|
// misconfiguration, not transient errors — let them propagate so ops see
|
|
311
388
|
// a 500 instead of a silent `vector_orphan_on_delete`.
|
|
389
|
+
//
|
|
390
|
+
// Deliberately UNBOUNDED (unlike upsertChunks/querySimilarChunks, which go
|
|
391
|
+
// through withDeadline + getNamespace): this is a single best-effort delete on
|
|
392
|
+
// the request-scoped dashboard delete handler — NOT the shared Cloud Run build
|
|
393
|
+
// instance — so a stall here cannot wedge other builds or trip the stale-build
|
|
394
|
+
// watchdog. A hung delete is bounded by the platform request timeout. Don't
|
|
395
|
+
// "fix" the asymmetry by reflex; bounding it would need its own deadline wiring.
|
|
312
396
|
const index = new Index({ url, token });
|
|
313
397
|
try {
|
|
314
398
|
await index.deleteNamespace(slug);
|
|
@@ -1997,9 +1997,9 @@
|
|
|
1997
1997
|
}
|
|
1998
1998
|
},
|
|
1999
1999
|
"node_modules/acorn": {
|
|
2000
|
-
"version": "8.
|
|
2001
|
-
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.
|
|
2002
|
-
"integrity": "sha512-
|
|
2000
|
+
"version": "8.17.0",
|
|
2001
|
+
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
|
|
2002
|
+
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
|
|
2003
2003
|
"license": "MIT",
|
|
2004
2004
|
"bin": {
|
|
2005
2005
|
"acorn": "bin/acorn"
|
|
@@ -2145,9 +2145,9 @@
|
|
|
2145
2145
|
}
|
|
2146
2146
|
},
|
|
2147
2147
|
"node_modules/baseline-browser-mapping": {
|
|
2148
|
-
"version": "2.10.
|
|
2149
|
-
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.
|
|
2150
|
-
"integrity": "sha512-
|
|
2148
|
+
"version": "2.10.36",
|
|
2149
|
+
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.36.tgz",
|
|
2150
|
+
"integrity": "sha512-lVq/Df7LXlO79MVaaUHztSwWiG9oXoWHlgvNS51v8Dpd4+G4/VIy6qYePTw31nAVls33nUtnfezYeLkYAak9dg==",
|
|
2151
2151
|
"license": "Apache-2.0",
|
|
2152
2152
|
"bin": {
|
|
2153
2153
|
"baseline-browser-mapping": "dist/cli.cjs"
|
|
@@ -2208,9 +2208,9 @@
|
|
|
2208
2208
|
"license": "MIT"
|
|
2209
2209
|
},
|
|
2210
2210
|
"node_modules/caniuse-lite": {
|
|
2211
|
-
"version": "1.0.
|
|
2212
|
-
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.
|
|
2213
|
-
"integrity": "sha512-
|
|
2211
|
+
"version": "1.0.30001799",
|
|
2212
|
+
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
|
|
2213
|
+
"integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
|
|
2214
2214
|
"funding": [
|
|
2215
2215
|
{
|
|
2216
2216
|
"type": "opencollective",
|
|
@@ -2945,9 +2945,9 @@
|
|
|
2945
2945
|
"license": "ISC"
|
|
2946
2946
|
},
|
|
2947
2947
|
"node_modules/enhanced-resolve": {
|
|
2948
|
-
"version": "5.
|
|
2949
|
-
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.
|
|
2950
|
-
"integrity": "sha512-
|
|
2948
|
+
"version": "5.24.0",
|
|
2949
|
+
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.0.tgz",
|
|
2950
|
+
"integrity": "sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ==",
|
|
2951
2951
|
"license": "MIT",
|
|
2952
2952
|
"dependencies": {
|
|
2953
2953
|
"graceful-fs": "^4.2.4",
|