jamdesk 1.1.155 → 1.1.157

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.
Files changed (60) hide show
  1. package/dist/__tests__/unit/ai-fix-client.test.js +2 -1
  2. package/dist/__tests__/unit/ai-fix-client.test.js.map +1 -1
  3. package/dist/__tests__/unit/fix-path-containment.test.d.ts +2 -0
  4. package/dist/__tests__/unit/fix-path-containment.test.d.ts.map +1 -0
  5. package/dist/__tests__/unit/fix-path-containment.test.js +139 -0
  6. package/dist/__tests__/unit/fix-path-containment.test.js.map +1 -0
  7. package/dist/__tests__/unit/fix.test.js +5 -2
  8. package/dist/__tests__/unit/fix.test.js.map +1 -1
  9. package/dist/__tests__/unit/mdx-validator-path-containment.test.d.ts +2 -0
  10. package/dist/__tests__/unit/mdx-validator-path-containment.test.d.ts.map +1 -0
  11. package/dist/__tests__/unit/mdx-validator-path-containment.test.js +35 -0
  12. package/dist/__tests__/unit/mdx-validator-path-containment.test.js.map +1 -0
  13. package/dist/__tests__/unit/path-realpath.test.d.ts +2 -0
  14. package/dist/__tests__/unit/path-realpath.test.d.ts.map +1 -0
  15. package/dist/__tests__/unit/path-realpath.test.js +61 -0
  16. package/dist/__tests__/unit/path-realpath.test.js.map +1 -0
  17. package/dist/__tests__/unit/spellcheck-path-containment.test.d.ts +2 -0
  18. package/dist/__tests__/unit/spellcheck-path-containment.test.d.ts.map +1 -0
  19. package/dist/__tests__/unit/spellcheck-path-containment.test.js +56 -0
  20. package/dist/__tests__/unit/spellcheck-path-containment.test.js.map +1 -0
  21. package/dist/commands/dev.d.ts.map +1 -1
  22. package/dist/commands/dev.js +3 -4
  23. package/dist/commands/dev.js.map +1 -1
  24. package/dist/commands/fix.d.ts +3 -1
  25. package/dist/commands/fix.d.ts.map +1 -1
  26. package/dist/commands/fix.js +67 -12
  27. package/dist/commands/fix.js.map +1 -1
  28. package/dist/commands/spellcheck.d.ts +1 -0
  29. package/dist/commands/spellcheck.d.ts.map +1 -1
  30. package/dist/commands/spellcheck.js +23 -9
  31. package/dist/commands/spellcheck.js.map +1 -1
  32. package/dist/commands/validate.d.ts.map +1 -1
  33. package/dist/commands/validate.js +14 -11
  34. package/dist/commands/validate.js.map +1 -1
  35. package/dist/lib/ai-fix-client.js +1 -1
  36. package/dist/lib/ai-fix-client.js.map +1 -1
  37. package/dist/lib/mdx-validator.d.ts +1 -0
  38. package/dist/lib/mdx-validator.d.ts.map +1 -1
  39. package/dist/lib/mdx-validator.js +24 -9
  40. package/dist/lib/mdx-validator.js.map +1 -1
  41. package/dist/lib/path-realpath.d.ts +3 -0
  42. package/dist/lib/path-realpath.d.ts.map +1 -0
  43. package/dist/lib/path-realpath.js +90 -0
  44. package/dist/lib/path-realpath.js.map +1 -0
  45. package/dist/lib/spellcheck-fix.d.ts +4 -0
  46. package/dist/lib/spellcheck-fix.d.ts.map +1 -1
  47. package/dist/lib/spellcheck-fix.js +33 -7
  48. package/dist/lib/spellcheck-fix.js.map +1 -1
  49. package/package.json +1 -1
  50. package/vendored/app/api/og/route.tsx +1 -0
  51. package/vendored/app/not-found.tsx +6 -0
  52. package/vendored/lib/mcp-search.ts +12 -11
  53. package/vendored/lib/openapi/operation-description.ts +110 -0
  54. package/vendored/lib/r2-content.ts +6 -0
  55. package/vendored/lib/render-doc-page.tsx +80 -1
  56. package/vendored/lib/static-artifacts.ts +8 -2
  57. package/vendored/lib/validate-page-frontmatter.ts +39 -3
  58. package/vendored/themes/jam/variables.css +11 -0
  59. package/vendored/themes/nebula/variables.css +10 -0
  60. package/vendored/workspace-package-lock.json +198 -282
@@ -0,0 +1,110 @@
1
+ // builder/build-service/lib/openapi/operation-description.ts
2
+ //
3
+ // Pure, dependency-free helpers for deriving a page meta description from an
4
+ // OpenAPI operation. Used by the build-service (llms.txt, <meta>, warning
5
+ // re-messaging) AND — vendored verbatim — by the dashboard Fix-with-AI
6
+ // pipeline, so both surfaces agree on the exact text and the length gate.
7
+ //
8
+ // IMPORT-FREE of `@/` aliases: vendor-openapi.mjs (exit 1 on `@/`) copies this
9
+ // file into dashboard/functions/src/editor/openapi/. Keep it self-contained.
10
+
11
+ /** Frontmatter descriptions shorter than this flag `page_short_description`.
12
+ * MUST equal MIN_DESCRIPTION_CHARS in validate-page-frontmatter.ts (asserted
13
+ * by a test). A sourced description is WRITTEN into frontmatter (Slice 3) only
14
+ * when its cleaned length is >= this, so the write never trades a "missing"
15
+ * warning for a "too short" one. */
16
+ export const MIN_FRONTMATTER_DESCRIPTION_CHARS = 110;
17
+
18
+ /** Search engines truncate meta descriptions around 155-160 chars. */
19
+ const MAX_META_DESCRIPTION_CHARS = 160;
20
+
21
+ /** The two operation fields we read. Loose types: specs are untrusted input. */
22
+ export interface OperationSummary {
23
+ summary?: unknown;
24
+ description?: unknown;
25
+ }
26
+
27
+ /** Minimum chars before a sentence/word boundary is worth cutting to (avoids
28
+ * truncating to a tiny fragment like "OK."). Matches the word-boundary floor. */
29
+ const MIN_TRUNCATION_CHARS = 40;
30
+
31
+ /** Reduce markdown to plain prose fit for a <meta>/llms description: drop
32
+ * fenced+inline code, images, links (keep the text), headings/list/blockquote
33
+ * markers and emphasis; collapse whitespace. If over the limit, prefer ending
34
+ * on a COMPLETE SENTENCE within the limit (no ellipsis — it's a whole thought);
35
+ * else cut on a word boundary with an ellipsis. "" for empty input.
36
+ * All scanning is O(n) char-by-char — deliberately no truncation regex, to
37
+ * avoid the catastrophic-backtracking class this repo has been bitten by. */
38
+ export function cleanMetaDescription(text: string): string {
39
+ let s = text;
40
+ s = s.replace(/```[\s\S]*?```/g, " "); // fenced code blocks
41
+ s = s.replace(/`([^`]+)`/g, "$1"); // inline code
42
+ s = s.replace(/!\[[^\]]*\]\([^)]*\)/g, " "); // images
43
+ s = s.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1"); // links -> link text
44
+ s = s.replace(/^[ \t]*[#>*\-+]+[ \t]*/gm, ""); // heading/quote/list markers
45
+ s = s.replace(/[*_~]+/g, ""); // emphasis runs
46
+ s = s.replace(/\s+/g, " ").trim(); // collapse all whitespace
47
+ if (s.length <= MAX_META_DESCRIPTION_CHARS) return s;
48
+ const cut = s.slice(0, MAX_META_DESCRIPTION_CHARS);
49
+ // Last sentence terminator (.?!) that is followed by a space or end-of-cut.
50
+ let sentenceEnd = -1;
51
+ for (let i = 0; i < cut.length; i++) {
52
+ const c = cut[i];
53
+ if ((c === "." || c === "!" || c === "?") &&
54
+ (i + 1 >= cut.length || cut[i + 1] === " ")) {
55
+ sentenceEnd = i;
56
+ }
57
+ }
58
+ if (sentenceEnd >= MIN_TRUNCATION_CHARS) {
59
+ return cut.slice(0, sentenceEnd + 1).trimEnd(); // complete sentence, no ellipsis
60
+ }
61
+ const lastSpace = cut.lastIndexOf(" ");
62
+ return (lastSpace > MIN_TRUNCATION_CHARS ? cut.slice(0, lastSpace) : cut)
63
+ .trimEnd() + "…";
64
+ }
65
+
66
+ /** Operation description (preferred) or summary (fallback), cleaned. null when
67
+ * neither yields non-empty text. No length gate — callers apply their own. */
68
+ export function deriveOperationDescription(
69
+ op: OperationSummary,
70
+ ): string | null {
71
+ const desc =
72
+ typeof op.description === "string" ? cleanMetaDescription(op.description) : "";
73
+ if (desc) return desc;
74
+ const summary =
75
+ typeof op.summary === "string" ? cleanMetaDescription(op.summary) : "";
76
+ return summary || null;
77
+ }
78
+
79
+ /** Look up an operation by method+path in a parsed (not necessarily
80
+ * dereferenced) OpenAPI document. Tolerant of unknown shapes. Method is
81
+ * case-insensitive. null if not present. */
82
+ export function findOperation(
83
+ spec: unknown,
84
+ method: string,
85
+ path: string,
86
+ ): OperationSummary | null {
87
+ if (!spec || typeof spec !== "object") return null;
88
+ const paths = (spec as {paths?: unknown}).paths;
89
+ if (!paths || typeof paths !== "object") return null;
90
+ const pathItem = (paths as Record<string, unknown>)[path];
91
+ if (!pathItem || typeof pathItem !== "object") return null;
92
+ const op = (pathItem as Record<string, unknown>)[method.toLowerCase()];
93
+ if (!op || typeof op !== "object") return null;
94
+ return op as OperationSummary;
95
+ }
96
+
97
+ /** Normalize `config.api.openapi` (string | string[] | undefined) to the list
98
+ * of repo-relative spec paths a build/fix pipeline can read. Drops remote
99
+ * `http(s)://` URLs — those are fetched by the renderer, never present as
100
+ * local blobs. Lives here (shared, vendored) so the build-service and the
101
+ * dashboard apply an identical remote-skip rule. */
102
+ export function collectLocalSpecPaths(apiOpenapi: unknown): string[] {
103
+ const raw =
104
+ typeof apiOpenapi === "string"
105
+ ? [apiOpenapi]
106
+ : Array.isArray(apiOpenapi)
107
+ ? apiOpenapi.filter((s): s is string => typeof s === "string")
108
+ : [];
109
+ return raw.filter((p) => !/^https?:\/\//i.test(p));
110
+ }
@@ -77,3 +77,9 @@ export function withR2OpsContext<T>(fn: () => T): T { return fn(); }
77
77
  export function enterR2OpsContextForTest<T>(fn: () => T): T { return fn(); }
78
78
  export function emitR2OpsSummary(_phase: R2TimingPhase, _projectSlug?: string | null): void {}
79
79
  export async function withTimeout<T>(promise: Promise<T>, _operation: string): Promise<T> { return promise; }
80
+ // Pure helper (no AWS SDK) — mirror the real impl so docs-config timeout parsing
81
+ // behaves identically in CLI dev as in prod.
82
+ export function resolveR2Timeout(raw: string | undefined): number {
83
+ const parsed = parseInt((raw ?? '').trim(), 10);
84
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 5000;
85
+ }
@@ -84,6 +84,7 @@ import {
84
84
  type AuthMethod,
85
85
  } from '@/lib/openapi';
86
86
  import { classifyOpenApiLoadError } from '@/lib/openapi/classify-load-error';
87
+ import { findOperation, deriveOperationDescription, collectLocalSpecPaths } from '@/lib/openapi/operation-description';
87
88
  import { extractLanguageFromPath, isValidLanguageCode } from '@/lib/language-utils';
88
89
  import { findFirstNavPage } from '@/lib/find-first-nav-page';
89
90
  import { candidateSpecPaths } from '@/lib/openapi/lang-spec-path';
@@ -210,6 +211,47 @@ function resolveSlug(normalizedSlug: string[], config: DocsConfig): string[] {
210
211
  return normalizedSlug;
211
212
  }
212
213
 
214
+ /**
215
+ * Resolve a page `<meta>`/og description from its OpenAPI operation when the
216
+ * page has no frontmatter `description`. Pure: the spec loader is injected so
217
+ * this is unit-testable independent of the render path (and so `buildDocMetadata`
218
+ * can hand it the SAME cached loader the renderer uses — no extra spec fetch).
219
+ *
220
+ * `loadSpec` MUST be a cached loader (see `buildDocMetadata` wiring): docs
221
+ * routes are `force-dynamic`, so this runs on every page view; an uncached
222
+ * fetch+parse here would add an R2 round-trip per render. Remote (`http(s)://`)
223
+ * spec entries are dropped — they are fetched by the renderer, never resolvable
224
+ * as a meta-time read. Returns null on any unresolvable/unparseable input so the
225
+ * caller degrades to `generateAutoDescription`.
226
+ */
227
+ export async function resolveOpenApiMetaDescription(
228
+ openapiRef: string,
229
+ specPaths: string[],
230
+ loadSpec: (specPath: string) => Promise<unknown | null>,
231
+ ): Promise<string | null> {
232
+ const local = specPaths.filter((p) => !/^https?:\/\//i.test(p));
233
+ if (local.length === 0) return null;
234
+ let parsed;
235
+ try {
236
+ parsed = parseOpenApiFrontmatter(openapiRef, local);
237
+ } catch {
238
+ return null;
239
+ }
240
+ // Full-format ref carries its own spec path; short-format must try each
241
+ // configured spec until the operation resolves.
242
+ const candidates = parsed.isShortFormat ? local : [parsed.specPath];
243
+ for (const sp of candidates) {
244
+ if (!sp) continue;
245
+ const spec = await loadSpec(sp);
246
+ if (!spec) continue;
247
+ const op = findOperation(spec, parsed.method, parsed.path);
248
+ if (!op) continue;
249
+ const d = deriveOperationDescription(op);
250
+ if (d) return d;
251
+ }
252
+ return null;
253
+ }
254
+
213
255
  export async function buildDocMetadata(input: RenderInput): Promise<Metadata> {
214
256
  const { slug: slugInput, projectSlug, hostAtDocs, requestHeaders } = input;
215
257
 
@@ -252,7 +294,44 @@ export async function buildDocMetadata(input: RenderInput): Promise<Metadata> {
252
294
  const data = parsed.data as FrontmatterData;
253
295
 
254
296
  if (!data.description) {
255
- data.description = generateAutoDescription(parsed.content);
297
+ if (typeof data.openapi === 'string' && data.openapi && config.api?.openapi) {
298
+ // Source the description from the OpenAPI operation before falling back to
299
+ // first-paragraph extraction (which is empty for prose-less API pages).
300
+ // The spec loader REUSES the render path's cached loaders — the ISR module
301
+ // cache (`r2:${slug}:${specPath}`, 10-min TTL) that `renderDocPage`
302
+ // populates when it renders the page's <ApiEndpoint>, or the static
303
+ // `getCachedSpec`. So the spec is the same warm parse the render already
304
+ // did: the marginal cost of this lookup is a cache hit, never an uncached
305
+ // fetch+parse. That matters because docs routes are force-dynamic, so this
306
+ // metadata runs on EVERY page view (every human + crawler). Mirrors the
307
+ // `useIsr`/`projectDir` derivation in `renderDocPage`'s OpenAPI branch.
308
+ const specPaths = collectLocalSpecPaths(config.api.openapi);
309
+ const useIsr = isIsrMode() && !!projectSlug;
310
+ const projectDir = useIsr ? null : getContentDir();
311
+ const loadSpecForMeta = async (sp: string): Promise<unknown | null> => {
312
+ try {
313
+ if (useIsr && projectSlug) {
314
+ const { resolveOpenApiSpec } = await import('@/lib/openapi-isr');
315
+ return await resolveOpenApiSpec(projectSlug, sp);
316
+ }
317
+ if (projectDir) {
318
+ const { api } = await getCachedSpec(sp, projectDir);
319
+ return api;
320
+ }
321
+ return null;
322
+ } catch {
323
+ // Any load/parse error degrades to generateAutoDescription below.
324
+ return null;
325
+ }
326
+ };
327
+ const sourced = await resolveOpenApiMetaDescription(
328
+ data.openapi, specPaths, loadSpecForMeta,
329
+ );
330
+ if (sourced) data.description = sourced;
331
+ }
332
+ if (!data.description) {
333
+ data.description = generateAutoDescription(parsed.content);
334
+ }
256
335
  }
257
336
 
258
337
  const baseUrl = resolveBaseUrl(requestHeaders, projectSlug, hostAtDocs);
@@ -436,14 +436,20 @@ export interface RawPageInfo {
436
436
  * @param pages - Array of raw page info
437
437
  * @returns Array of page metadata
438
438
  */
439
- export function extractPageMetadata(pages: RawPageInfo[]): PageMetadata[] {
439
+ export function extractPageMetadata(
440
+ pages: RawPageInfo[],
441
+ sourced?: Map<string, {description: string}>,
442
+ ): PageMetadata[] {
440
443
  return pages.map(page => {
441
444
  const pathWithoutExt = page.path.replace(/\.mdx?$/, '');
442
445
 
443
446
  return {
444
447
  path: pathWithoutExt,
445
448
  title: page.frontmatter.title || pathWithoutExt,
446
- description: page.frontmatter.description,
449
+ // Frontmatter wins; else fall back to a description sourced from the
450
+ // page's OpenAPI operation (keyed by the extension-ful page.path).
451
+ description:
452
+ page.frontmatter.description ?? sourced?.get(page.path)?.description,
447
453
  noindex: page.frontmatter.noindex || page.frontmatter.seo?.noindex,
448
454
  hidden: page.frontmatter.hidden,
449
455
  lastModified: page.frontmatter.lastModified,
@@ -1,14 +1,20 @@
1
1
  import type { BuildWarning } from '../shared/status-reporter.js'; // vendored copy — NOT '../../shared'
2
+ import { MIN_FRONTMATTER_DESCRIPTION_CHARS } from './openapi/operation-description.js';
2
3
 
3
4
  const MIN_DESCRIPTION_CHARS = 110; // matches the Ahrefs "too short" floor used elsewhere
5
+ // Task 1 invariant: MIN_DESCRIPTION_CHARS must equal MIN_FRONTMATTER_DESCRIPTION_CHARS.
6
+ // Asserted by test. Do NOT change either value without updating both.
4
7
 
5
8
  interface PageInfo {
6
9
  path: string;
7
- frontmatter: { title?: unknown; description?: unknown; hidden?: unknown };
10
+ frontmatter: { title?: unknown; description?: unknown; hidden?: unknown; openapi?: unknown };
8
11
  content: string;
9
12
  }
10
13
 
11
- export function validatePageFrontmatter(pages: PageInfo[]): BuildWarning[] {
14
+ export function validatePageFrontmatter(
15
+ pages: PageInfo[],
16
+ sourced?: Map<string, { description: string; length: number }>,
17
+ ): BuildWarning[] {
12
18
  const warnings: BuildWarning[] = [];
13
19
  for (const page of pages) {
14
20
  const fm = page.frontmatter ?? {};
@@ -21,7 +27,37 @@ export function validatePageFrontmatter(pages: PageInfo[]): BuildWarning[] {
21
27
  warnings.push({ type: 'page_missing_title', file: page.path, message: 'Page has no frontmatter title; navigation and llms.txt fall back to the file path.' });
22
28
  }
23
29
  if (!description) {
24
- warnings.push({ type: 'page_missing_description', file: page.path, message: 'Page has no frontmatter description; add one for SEO and llms.txt.' });
30
+ const src = sourced?.get(page.path);
31
+ if (src) {
32
+ if (src.length >= MIN_FRONTMATTER_DESCRIPTION_CHARS) {
33
+ // Cat 1: OpenAPI-sourced description is long enough for Fix-with-AI to write
34
+ // into frontmatter — keep the warning but re-message it toward that action.
35
+ warnings.push({
36
+ type: 'page_missing_description',
37
+ file: page.path,
38
+ message: "No frontmatter description — your page's description is auto-sourced " +
39
+ "from your OpenAPI spec. Use Fix with AI to write it into the page for portability.",
40
+ });
41
+ } else {
42
+ // Cat 2: sourced text IS live in <meta>/llms, but too short to commit via
43
+ // Fix-with-AI. Do NOT suppress — the author still needs a longer description.
44
+ // A sourced-short description must warn no less than an authored-short one
45
+ // (else `summary: "TODO"` silently becomes the live meta + OG card with zero
46
+ // signal to fix it).
47
+ warnings.push({
48
+ type: 'page_missing_description',
49
+ file: page.path,
50
+ message:
51
+ `Description auto-sourced from your OpenAPI spec is only ` +
52
+ `${src.length} chars — add a longer frontmatter description ` +
53
+ `(aim for ${MIN_FRONTMATTER_DESCRIPTION_CHARS}–160; too short ` +
54
+ `for Fix with AI to write into the page).`,
55
+ });
56
+ }
57
+ } else {
58
+ // Cat 3: genuinely missing, nothing to source from the spec.
59
+ warnings.push({ type: 'page_missing_description', file: page.path, message: 'Page has no frontmatter description; add one for SEO and llms.txt.' });
60
+ }
25
61
  } else if (description.length < MIN_DESCRIPTION_CHARS) {
26
62
  warnings.push({ type: 'page_short_description', file: page.path, message: `Description is ${description.length} chars; aim for ${MIN_DESCRIPTION_CHARS}–160.` });
27
63
  }
@@ -710,6 +710,17 @@ body[data-theme="jam"] .sidebar-scroll .sidebar-nav-groups > div:first-child {
710
710
  margin-top: 0 !important;
711
711
  }
712
712
 
713
+ /* When the nav groups are the first thing in the sidebar (no left tabs / external
714
+ anchors), align the first group heading with the breadcrumb + "On this page" TOC
715
+ heading, both of which sit 40px down via the content/TOC `py-10` wrapper. The
716
+ base 16px above is the tabs→groups gap for the sidebar-tabs case, so only bump
717
+ the first-child, desktop-only path here. */
718
+ @media (min-width: 1024px) {
719
+ body[data-theme="jam"] .sidebar-scroll nav > .sidebar-nav-groups:first-child {
720
+ padding-top: 2.5rem !important;
721
+ }
722
+ }
723
+
713
724
  /* Sidebar scroll container positioning for header-logo layout */
714
725
  /* The sidebar-scroll div needs constrained height to scroll independently */
715
726
  /* When tabs exist at top, sidebar needs more top offset (header 57px + tabs 57px = 114px) */
@@ -294,3 +294,13 @@ body[data-theme="nebula"] .sidebar-scroll .sidebar-nav-groups > div.ml-6 {
294
294
  padding-top: 16px;
295
295
  }
296
296
 
297
+ /* When the nav groups are the first thing in the sidebar (no left tabs / external
298
+ anchors), align the first group heading with the breadcrumb + "On this page" TOC
299
+ heading, both 40px down via the content/TOC `py-10` wrapper. The 16px above is the
300
+ inter-group gap, so only the first group on desktop gets the larger offset. */
301
+ @media (min-width: 1024px) {
302
+ body[data-theme="nebula"] .sidebar-scroll nav > .sidebar-nav-groups:first-child > div.ml-6:first-child {
303
+ padding-top: 2.5rem;
304
+ }
305
+ }
306
+