jamdesk 1.1.149 → 1.1.151

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 (85) hide show
  1. package/README.md +28 -0
  2. package/dist/__tests__/integration/fix-scan-integration.test.d.ts +2 -0
  3. package/dist/__tests__/integration/fix-scan-integration.test.d.ts.map +1 -0
  4. package/dist/__tests__/integration/fix-scan-integration.test.js +77 -0
  5. package/dist/__tests__/integration/fix-scan-integration.test.js.map +1 -0
  6. package/dist/__tests__/unit/ai-fix-client.test.d.ts +2 -0
  7. package/dist/__tests__/unit/ai-fix-client.test.d.ts.map +1 -0
  8. package/dist/__tests__/unit/ai-fix-client.test.js +64 -0
  9. package/dist/__tests__/unit/ai-fix-client.test.js.map +1 -0
  10. package/dist/__tests__/unit/docs-config.test.js +66 -0
  11. package/dist/__tests__/unit/docs-config.test.js.map +1 -1
  12. package/dist/__tests__/unit/fix-ai-fallback.test.d.ts +2 -0
  13. package/dist/__tests__/unit/fix-ai-fallback.test.d.ts.map +1 -0
  14. package/dist/__tests__/unit/fix-ai-fallback.test.js +82 -0
  15. package/dist/__tests__/unit/fix-ai-fallback.test.js.map +1 -0
  16. package/dist/__tests__/unit/fix-engine-sync.test.d.ts +2 -0
  17. package/dist/__tests__/unit/fix-engine-sync.test.d.ts.map +1 -0
  18. package/dist/__tests__/unit/fix-engine-sync.test.js +16 -0
  19. package/dist/__tests__/unit/fix-engine-sync.test.js.map +1 -0
  20. package/dist/__tests__/unit/fix.test.d.ts +2 -0
  21. package/dist/__tests__/unit/fix.test.d.ts.map +1 -0
  22. package/dist/__tests__/unit/fix.test.js +61 -0
  23. package/dist/__tests__/unit/fix.test.js.map +1 -0
  24. package/dist/__tests__/unit/frontmatter-write.test.d.ts +2 -0
  25. package/dist/__tests__/unit/frontmatter-write.test.d.ts.map +1 -0
  26. package/dist/__tests__/unit/frontmatter-write.test.js +31 -0
  27. package/dist/__tests__/unit/frontmatter-write.test.js.map +1 -0
  28. package/dist/__tests__/unit/heading-extractor-sync.test.d.ts +2 -0
  29. package/dist/__tests__/unit/heading-extractor-sync.test.d.ts.map +1 -0
  30. package/dist/__tests__/unit/heading-extractor-sync.test.js +16 -0
  31. package/dist/__tests__/unit/heading-extractor-sync.test.js.map +1 -0
  32. package/dist/__tests__/unit/missing-description-scan.test.d.ts +2 -0
  33. package/dist/__tests__/unit/missing-description-scan.test.d.ts.map +1 -0
  34. package/dist/__tests__/unit/missing-description-scan.test.js +50 -0
  35. package/dist/__tests__/unit/missing-description-scan.test.js.map +1 -0
  36. package/dist/commands/fix.d.ts +97 -0
  37. package/dist/commands/fix.d.ts.map +1 -0
  38. package/dist/commands/fix.js +348 -0
  39. package/dist/commands/fix.js.map +1 -0
  40. package/dist/index.js +19 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/lib/ai-fix-client.d.ts +27 -0
  43. package/dist/lib/ai-fix-client.d.ts.map +1 -0
  44. package/dist/lib/ai-fix-client.js +45 -0
  45. package/dist/lib/ai-fix-client.js.map +1 -0
  46. package/dist/lib/deps.js +1 -1
  47. package/dist/lib/deps.js.map +1 -1
  48. package/dist/lib/docs-config.d.ts +1 -1
  49. package/dist/lib/docs-config.d.ts.map +1 -1
  50. package/dist/lib/docs-config.js +59 -4
  51. package/dist/lib/docs-config.js.map +1 -1
  52. package/dist/lib/fix-engine.d.ts +89 -0
  53. package/dist/lib/fix-engine.d.ts.map +1 -0
  54. package/dist/lib/fix-engine.js +236 -0
  55. package/dist/lib/fix-engine.js.map +1 -0
  56. package/dist/lib/frontmatter-write.d.ts +4 -0
  57. package/dist/lib/frontmatter-write.d.ts.map +1 -0
  58. package/dist/lib/frontmatter-write.js +10 -0
  59. package/dist/lib/frontmatter-write.js.map +1 -0
  60. package/dist/lib/heading-extractor.d.ts +63 -0
  61. package/dist/lib/heading-extractor.d.ts.map +1 -0
  62. package/dist/lib/heading-extractor.js +171 -0
  63. package/dist/lib/heading-extractor.js.map +1 -0
  64. package/dist/lib/missing-description-scan.d.ts +7 -0
  65. package/dist/lib/missing-description-scan.d.ts.map +1 -0
  66. package/dist/lib/missing-description-scan.js +28 -0
  67. package/dist/lib/missing-description-scan.js.map +1 -0
  68. package/package.json +3 -3
  69. package/vendored/components/mdx/ApiEndpoint.tsx +1 -1
  70. package/vendored/components/mdx/OpenApiEndpoint.tsx +7 -2
  71. package/vendored/components/navigation/SocialFooter.tsx +9 -1
  72. package/vendored/lib/build/error-parser.ts +6 -3
  73. package/vendored/lib/firestore-helpers.ts +2 -3
  74. package/vendored/lib/fix-engine.ts +293 -0
  75. package/vendored/lib/health-checks.ts +2 -2
  76. package/vendored/lib/json-ld.ts +98 -6
  77. package/vendored/lib/render-doc-page.tsx +8 -2
  78. package/vendored/lib/search.ts +2 -2
  79. package/vendored/lib/seo.ts +29 -13
  80. package/vendored/lib/static-artifacts.ts +2 -2
  81. package/vendored/lib/ui-strings.ts +6 -0
  82. package/vendored/lib/validate-config.ts +63 -4
  83. package/vendored/scripts/build-search-index.cjs +2 -2
  84. package/vendored/shared/types.ts +1 -0
  85. package/vendored/workspace-package-lock.json +36 -36
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Deterministic Doc-Warning Fix Engine.
3
+ *
4
+ * AUTO-SYNCED: This file is automatically copied from build-service by vendor.js.
5
+ * DO NOT EDIT directly - edit builder/build-service/lib/fix-engine.ts instead.
6
+ */
7
+ /** A markdown heading entry (not a <Step> or <Update> component anchor). */
8
+ export function isMarkdownHeading(h) {
9
+ return h.stepNumber == null && !h.isUpdate;
10
+ }
11
+ /** True iff `candidate` is an exact heading id on `headings`. The canonical
12
+ * guard for any externally-sourced (e.g. AI) anchor suggestion — the engine
13
+ * itself uses the same `allIds.includes(...)` check inside fixBrokenLink's
14
+ * `emit`. Phase 2's AI tier MUST route every suggestion through this so a
15
+ * hallucinated fragment can never be written. Pure, dependency-free. */
16
+ export function isRealAnchor(candidate, headings) {
17
+ if (!candidate)
18
+ return false;
19
+ return headings.some((h) => h.id === candidate);
20
+ }
21
+ /** Substring appended to the skip reason when the deterministic engine defers an
22
+ * anchor to the AI tier. Exported so the AI-tier callers match on this constant
23
+ * instead of a re-typed literal — producer and consumer cannot drift. */
24
+ export const NEEDS_AI_MARKER = '(needs AI)';
25
+ // ---- Edit application -----------------------------------------------------
26
+ // A flagged href is always the target of a markdown link `](href)` or an
27
+ // `href="…"`/`href='…'` attribute, so in source it is bounded on the left by a
28
+ // delimiter — never by another URL/path character. A match whose preceding char
29
+ // IS a path character is the href appearing as a SUBSTRING inside a longer URL
30
+ // (e.g. `/setup#x` inside `/guide/setup#x`) and must be skipped, or the edit
31
+ // would corrupt an unrelated, valid link on the same line.
32
+ const URL_PATH_CHAR = /[A-Za-z0-9\-._~%/#?:@]/;
33
+ /** Index of the next occurrence of `href` at/after `from` that sits at a link
34
+ * boundary (not as a substring inside a longer path), or -1. */
35
+ function findHrefBoundary(line, href, from) {
36
+ let pos = line.indexOf(href, from);
37
+ while (pos !== -1) {
38
+ if (pos === 0 || !URL_PATH_CHAR.test(line[pos - 1]))
39
+ return pos;
40
+ pos = line.indexOf(href, pos + 1);
41
+ }
42
+ return -1;
43
+ }
44
+ /**
45
+ * Apply edits, replacing each edit's oldText with newText on its specific
46
+ * (1-indexed) source line. Line-scoped, so an identical href on another line
47
+ * (e.g. a code sample) is never touched. Within a line: oldText is matched at a
48
+ * link boundary (so a short href is never replaced inside a longer one),
49
+ * matching runs against the ORIGINAL line (so an edit whose newText contains its
50
+ * oldText is never re-matched), and edits apply right-to-left so earlier
51
+ * positions keep their indices. Identical oldTexts on one line consume
52
+ * successive occurrences. Throws if a line is out of range or an oldText is
53
+ * absent — a missing match means the source changed since the fix was derived,
54
+ * and a partial/wrong edit must never be written.
55
+ */
56
+ export function applyEdits(content, edits) {
57
+ if (edits.length === 0)
58
+ return content;
59
+ const lines = content.split('\n');
60
+ const byLine = new Map();
61
+ for (const edit of edits) {
62
+ const list = byLine.get(edit.line);
63
+ if (list)
64
+ list.push(edit);
65
+ else
66
+ byLine.set(edit.line, [edit]);
67
+ }
68
+ for (const [line, lineEdits] of byLine) {
69
+ const idx = line - 1;
70
+ if (idx < 0 || idx >= lines.length) {
71
+ throw new Error(`applyEdits: line ${line} out of range (${lines.length} lines)`);
72
+ }
73
+ const src = lines[idx];
74
+ // Resolve every edit to a concrete [start,end) span on the ORIGINAL line.
75
+ const spans = [];
76
+ const nextFrom = new Map();
77
+ for (const edit of lineEdits) {
78
+ const from = nextFrom.get(edit.oldText) ?? 0;
79
+ const start = findHrefBoundary(src, edit.oldText, from);
80
+ if (start === -1) {
81
+ throw new Error(`applyEdits: oldText not found on line ${line}: ${JSON.stringify(edit.oldText)}`);
82
+ }
83
+ const end = start + edit.oldText.length;
84
+ nextFrom.set(edit.oldText, end);
85
+ spans.push({ start, end, newText: edit.newText });
86
+ }
87
+ // Apply right-to-left so an earlier span's indices survive later edits.
88
+ spans.sort((a, b) => b.start - a.start);
89
+ let out = src;
90
+ for (const s of spans)
91
+ out = out.slice(0, s.start) + s.newText + out.slice(s.end);
92
+ lines[idx] = out;
93
+ }
94
+ return lines.join('\n');
95
+ }
96
+ // ---- Distance / nearest-slug (ported from validate-links.cjs) -------------
97
+ const MAX_SUGGESTION_DISTANCE = 2;
98
+ export function levenshtein(a, b) {
99
+ if (a === b)
100
+ return 0;
101
+ if (a.length === 0)
102
+ return b.length;
103
+ if (b.length === 0)
104
+ return a.length;
105
+ let prev = new Array(b.length + 1);
106
+ let curr = new Array(b.length + 1);
107
+ for (let j = 0; j <= b.length; j++)
108
+ prev[j] = j;
109
+ for (let i = 0; i < a.length; i++) {
110
+ curr[0] = i + 1;
111
+ for (let j = 0; j < b.length; j++) {
112
+ const cost = a[i] === b[j] ? 0 : 1;
113
+ curr[j + 1] = Math.min(curr[j] + 1, prev[j + 1] + 1, prev[j] + cost);
114
+ }
115
+ [prev, curr] = [curr, prev];
116
+ }
117
+ return prev[b.length];
118
+ }
119
+ export function closestSlug(fragment, slugs) {
120
+ if (!fragment)
121
+ return null;
122
+ let best = null;
123
+ let bestDist = Infinity;
124
+ let secondDist = Infinity;
125
+ for (const slug of slugs) {
126
+ if (Math.abs(fragment.length - slug.length) > MAX_SUGGESTION_DISTANCE)
127
+ continue;
128
+ const d = levenshtein(fragment, slug);
129
+ if (d < bestDist) {
130
+ secondDist = bestDist;
131
+ bestDist = d;
132
+ best = slug;
133
+ }
134
+ else if (d < secondDist) {
135
+ secondDist = d;
136
+ }
137
+ }
138
+ return best !== null && bestDist >= 1 && bestDist <= MAX_SUGGESTION_DISTANCE && bestDist < secondDist
139
+ ? best
140
+ : null;
141
+ }
142
+ // ---- Link + locale helpers ------------------------------------------------
143
+ export function parseHref(href) {
144
+ const i = href.indexOf('#');
145
+ if (i === -1)
146
+ return { path: href, fragment: null };
147
+ return { path: i === 0 ? null : href.slice(0, i), fragment: href.slice(i + 1) || null };
148
+ }
149
+ export function normalizeLinkPath(linkPath, hostAtDocs) {
150
+ let p = linkPath;
151
+ if (hostAtDocs && p.startsWith('/docs/'))
152
+ p = p.slice('/docs/'.length);
153
+ if (p.startsWith('/'))
154
+ p = p.slice(1);
155
+ return p.replace(/\.mdx$/, '');
156
+ }
157
+ /** The default-locale twin of a localized page path, or null if already default. */
158
+ export function defaultTwinPath(pagePath, locales) {
159
+ const segments = pagePath.split('/');
160
+ const first = segments[0];
161
+ if (first !== locales.defaultCode && locales.codes.includes(first)) {
162
+ return segments.slice(1).join('/');
163
+ }
164
+ return null;
165
+ }
166
+ // ---- Broken-link fixer ----------------------------------------------------
167
+ function skip(reason) {
168
+ return { status: 'skipped', edits: [], reason };
169
+ }
170
+ export const fixBrokenLink = (warning, ctx) => {
171
+ const { path: linkPath, fragment } = parseHref(warning.link);
172
+ if (!fragment)
173
+ return skip('not a fragment link — out of scope for phase 1');
174
+ if (!linkPath)
175
+ return skip('same-page fragment — out of scope for phase 1');
176
+ if (warning.line == null)
177
+ return skip('warning has no line — cannot place a safe edit');
178
+ const targetPage = ctx.resolveLinkPagePath(linkPath);
179
+ if (!targetPage)
180
+ return skip('target page could not be resolved');
181
+ const targetHeadings = ctx.headingsOf(targetPage);
182
+ if (!targetHeadings)
183
+ return skip('target page headings could not be read');
184
+ const allIds = targetHeadings.map((h) => h.id);
185
+ // (0) Re-derivation: may already be valid in current source.
186
+ if (allIds.includes(fragment))
187
+ return skip('already valid — fragment exists on target');
188
+ // Emit only a fragment that is a REAL anchor on the target (never point at a
189
+ // non-existent or wrong-kind id), reconstructing the href (no substring replace).
190
+ const emit = (candidate, reason) => {
191
+ if (!allIds.includes(candidate))
192
+ return skip('internal: candidate is not a real anchor — refusing');
193
+ return {
194
+ status: 'fixed',
195
+ edits: [{ file: warning.file, line: warning.line, oldText: warning.link, newText: linkPath + '#' + candidate }],
196
+ reason,
197
+ };
198
+ };
199
+ // (1) Cross-locale positional map FIRST (high precision for localized targets).
200
+ // Ordinals over MARKDOWN headings only (so <Step>/<Update> can't misalign them),
201
+ // but the emitted id is the target heading's REAL id (full-set dedup).
202
+ const twinPage = defaultTwinPath(targetPage, ctx.locales);
203
+ if (twinPage) {
204
+ const twinHeadings = ctx.headingsOf(twinPage);
205
+ if (twinHeadings) {
206
+ const twinMd = twinHeadings.filter(isMarkdownHeading);
207
+ const targetMd = targetHeadings.filter(isMarkdownHeading);
208
+ const ordinal = twinMd.findIndex((h) => h.id === fragment);
209
+ if (ordinal !== -1 && twinMd.length === targetMd.length && targetMd[ordinal]) {
210
+ return emit(targetMd[ordinal].id, `cross-locale positional map: #${fragment} → #${targetMd[ordinal].id}`);
211
+ }
212
+ }
213
+ }
214
+ // (2) Near-miss fallback (typos within the same language).
215
+ const near = closestSlug(fragment, allIds);
216
+ if (near)
217
+ return emit(near, `near-miss: #${fragment} → #${near}`);
218
+ return skip(`no confident deterministic fix — structures diverge or target is ambiguous ${NEEDS_AI_MARKER}`);
219
+ };
220
+ // ---- Registry + engine entry point ----------------------------------------
221
+ /** Fixer registry keyed by BuildWarningType. Phase 1: broken_link only. */
222
+ export const FIXERS = {
223
+ broken_link: fixBrokenLink,
224
+ };
225
+ /**
226
+ * Run the registered fixer for a warning. Deterministic-first is implemented
227
+ * inside each fixer (exact strategies, then 'skipped' for AI cases). The AI
228
+ * fallback tier is wired in Phase 2; until then a 'skipped' result is terminal.
229
+ */
230
+ export function runFixer(warning, ctx) {
231
+ const fixer = FIXERS[warning.type];
232
+ if (!fixer)
233
+ return skip(`no fixer registered for type "${warning.type}"`);
234
+ return fixer(warning, ctx);
235
+ }
236
+ //# sourceMappingURL=fix-engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fix-engine.js","sourceRoot":"","sources":["../../src/lib/fix-engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ /** Set a single frontmatter field, preserving the body. gray-matter round-trips
2
+ * via matter.stringify (already used elsewhere in the CLI dep tree). */
3
+ export declare function setFrontmatterField(source: string, key: string, value: string): string;
4
+ //# sourceMappingURL=frontmatter-write.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontmatter-write.d.ts","sourceRoot":"","sources":["../../src/lib/frontmatter-write.ts"],"names":[],"mappings":"AAGA;yEACyE;AACzE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAItF"}
@@ -0,0 +1,10 @@
1
+ // builder/cli/src/lib/frontmatter-write.ts
2
+ import matter from 'gray-matter';
3
+ /** Set a single frontmatter field, preserving the body. gray-matter round-trips
4
+ * via matter.stringify (already used elsewhere in the CLI dep tree). */
5
+ export function setFrontmatterField(source, key, value) {
6
+ const parsed = matter(source);
7
+ const data = { ...parsed.data, [key]: value };
8
+ return matter.stringify(parsed.content, data);
9
+ }
10
+ //# sourceMappingURL=frontmatter-write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontmatter-write.js","sourceRoot":"","sources":["../../src/lib/frontmatter-write.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAC3C,OAAO,MAAM,MAAM,aAAa,CAAC;AAEjC;yEACyE;AACzE,MAAM,UAAU,mBAAmB,CAAC,MAAc,EAAE,GAAW,EAAE,KAAa;IAC5E,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAC9B,MAAM,IAAI,GAAG,EAAE,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC;IAC9C,OAAO,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;AAChD,CAAC"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Heading Extractor (slug ids matching rehype-slug / the real DOM).
3
+ *
4
+ * AUTO-SYNCED: This file is automatically copied from build-service by vendor.js.
5
+ * DO NOT EDIT directly - edit builder/build-service/lib/heading-extractor.ts instead.
6
+ */
7
+ export interface HeadingInfo {
8
+ id: string;
9
+ text: string;
10
+ level: number;
11
+ line: number;
12
+ /** When the entry is a <Step> inside a <Steps> block, this is its 1-based index within the block. */
13
+ stepNumber?: number;
14
+ /** True when the entry came from an <Update label="..."> anchor (vs a markdown heading). */
15
+ isUpdate?: boolean;
16
+ }
17
+ /**
18
+ * Generate a URL-friendly slug from heading text.
19
+ *
20
+ * Delegates to github-slugger — the SAME library `rehype-slug` uses to assign the
21
+ * real heading `id` attributes during MDX compile (wired in render-doc-page.tsx).
22
+ * Using it here keeps the "On this page" TOC, <Update>/<Step> anchors, and the
23
+ * link validator byte-for-byte aligned with the actual DOM ids — including for
24
+ * non-ASCII headings, where the previous `[^a-z0-9]+ -> '-'` implementation
25
+ * diverged (accents/apostrophes/underscores became dashes) and produced dead
26
+ * anchors (e.g. heading "Abrir cualquier página" → real id `abrir-cualquier-página`,
27
+ * but the old slug was `abrir-cualquier-p-gina`).
28
+ *
29
+ * Stateless: repeated calls with the same text return the same slug. For
30
+ * per-document uniqueness across duplicate headings, use a `GithubSlugger`
31
+ * instance (as extractHeadings does) — it mirrors rehype-slug's `-1`/`-2`
32
+ * suffixing exactly.
33
+ *
34
+ * Mirrored in scripts/validate-links.cjs (CJS — can't import the ESM-only
35
+ * github-slugger), where the algorithm + regex are replicated and guarded by a
36
+ * drift test (`validate-links-slug.test.ts`). Keep both in sync.
37
+ */
38
+ export declare function generateSlug(text: string): string;
39
+ /**
40
+ * Extract all heading slugs from MDX content.
41
+ * Includes markdown headings, <Update label="..."> component anchors,
42
+ * and <Step title="..."> entries inside <Steps> blocks (with per-block numbering).
43
+ * Skips content inside fenced code blocks.
44
+ *
45
+ * A single page-scoped `GithubSlugger` assigns ids so duplicate titles (across
46
+ * H2s, Update labels, and Steps) get `-1`, `-2`, ... suffixes in source order —
47
+ * the same algorithm rehype-slug applies to the real markdown-heading DOM ids.
48
+ */
49
+ export declare function extractHeadings(content: string): HeadingInfo[];
50
+ /**
51
+ * Build a map of page path -> heading slug set.
52
+ * Keys are page paths without .mdx extension (matching docs.json navigation format).
53
+ * Skips API endpoint pages (they generate dynamic anchors at render time).
54
+ *
55
+ * Unlike buildHeadingMapFromFiles() in validate-links.cjs, this doesn't add dual
56
+ * keys for index pages. The CJS validator handles index resolution via
57
+ * getTargetHeadingSlugs() which derives the key from the filesystem path.
58
+ *
59
+ * @param fileContents - Map of relative file paths (with .mdx) to content.
60
+ * Keys come from build.ts fileContents Map (e.g., 'getting-started.mdx', 'api/auth.mdx').
61
+ */
62
+ export declare function buildHeadingMap(fileContents: Map<string, string>): Map<string, Set<string>>;
63
+ //# sourceMappingURL=heading-extractor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heading-extractor.d.ts","sourceRoot":"","sources":["../../src/lib/heading-extractor.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,qGAAqG;IACrG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD;AAqBD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,EAAE,CAgF9D;AAaD;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAChC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAU1B"}
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Heading Extractor (slug ids matching rehype-slug / the real DOM).
3
+ *
4
+ * AUTO-SYNCED: This file is automatically copied from build-service by vendor.js.
5
+ * DO NOT EDIT directly - edit builder/build-service/lib/heading-extractor.ts instead.
6
+ */
7
+ import GithubSlugger, { slug as githubSlug } from 'github-slugger';
8
+ /**
9
+ * Generate a URL-friendly slug from heading text.
10
+ *
11
+ * Delegates to github-slugger — the SAME library `rehype-slug` uses to assign the
12
+ * real heading `id` attributes during MDX compile (wired in render-doc-page.tsx).
13
+ * Using it here keeps the "On this page" TOC, <Update>/<Step> anchors, and the
14
+ * link validator byte-for-byte aligned with the actual DOM ids — including for
15
+ * non-ASCII headings, where the previous `[^a-z0-9]+ -> '-'` implementation
16
+ * diverged (accents/apostrophes/underscores became dashes) and produced dead
17
+ * anchors (e.g. heading "Abrir cualquier página" → real id `abrir-cualquier-página`,
18
+ * but the old slug was `abrir-cualquier-p-gina`).
19
+ *
20
+ * Stateless: repeated calls with the same text return the same slug. For
21
+ * per-document uniqueness across duplicate headings, use a `GithubSlugger`
22
+ * instance (as extractHeadings does) — it mirrors rehype-slug's `-1`/`-2`
23
+ * suffixing exactly.
24
+ *
25
+ * Mirrored in scripts/validate-links.cjs (CJS — can't import the ESM-only
26
+ * github-slugger), where the algorithm + regex are replicated and guarded by a
27
+ * drift test (`validate-links-slug.test.ts`). Keep both in sync.
28
+ */
29
+ export function generateSlug(text) {
30
+ return githubSlug(text);
31
+ }
32
+ const HEADING_REGEX = /^(#{1,6})\s+(.+)$/;
33
+ const FENCE_REGEX = /^(`{3,}|~{3,})/;
34
+ // Global + matchAll (like STEP_TITLE_REGEX below) so multiple inline <Update>
35
+ // tags on one line each get an anchor, and `[^>]*` so `label=` need not be the
36
+ // first attribute (e.g. `<Update date=".." label="..">`). Alternation (not a
37
+ // character class) keeps an apostrophe inside a double-quoted label from
38
+ // terminating the capture. MUST only be used with matchAll — direct test/exec
39
+ // would share lastIndex across calls and miscount.
40
+ const UPDATE_LABEL_REGEX = /<Update\s+[^>]*label=(?:"([^"]+)"|'([^']+)')/g;
41
+ const STEPS_OPEN_REGEX = /<Steps(\s|>)/;
42
+ const STEPS_CLOSE_REGEX = /<\/Steps>/;
43
+ // Global flag — we iterate with matchAll so authors who inline multiple
44
+ // <Step> tags on one line get every one numbered.
45
+ // Alternation (not a character class) keeps the capture from terminating on
46
+ // apostrophes inside double-quoted attribute values (e.g. `<Step title="You're Live">`).
47
+ // MUST only be used with matchAll — direct `test`/`exec` would share
48
+ // lastIndex across calls and miscount.
49
+ const STEP_TITLE_REGEX = /<Step\s+[^>]*title=(?:"([^"]+)"|'([^']+)')/g;
50
+ /**
51
+ * Extract all heading slugs from MDX content.
52
+ * Includes markdown headings, <Update label="..."> component anchors,
53
+ * and <Step title="..."> entries inside <Steps> blocks (with per-block numbering).
54
+ * Skips content inside fenced code blocks.
55
+ *
56
+ * A single page-scoped `GithubSlugger` assigns ids so duplicate titles (across
57
+ * H2s, Update labels, and Steps) get `-1`, `-2`, ... suffixes in source order —
58
+ * the same algorithm rehype-slug applies to the real markdown-heading DOM ids.
59
+ */
60
+ export function extractHeadings(content) {
61
+ const headings = [];
62
+ const lines = content.split('\n');
63
+ let inCodeBlock = false;
64
+ let fencePattern = '';
65
+ // Boolean (not depth counter) — resets on every <Steps> open so an unclosed
66
+ // block can't stick the counter across subsequent blocks.
67
+ let inStepsBlock = false;
68
+ let stepCounter = 0;
69
+ // Page-scoped slugger so duplicate titles get -1, -2, ... suffixes in source
70
+ // order, matching rehype-slug's per-document GithubSlugger.
71
+ const slugger = new GithubSlugger();
72
+ for (let i = 0; i < lines.length; i++) {
73
+ const line = lines[i];
74
+ const fenceMatch = line.match(FENCE_REGEX);
75
+ if (fenceMatch) {
76
+ if (!inCodeBlock) {
77
+ inCodeBlock = true;
78
+ fencePattern = fenceMatch[1];
79
+ continue;
80
+ }
81
+ else if (line.startsWith(fencePattern)) {
82
+ inCodeBlock = false;
83
+ fencePattern = '';
84
+ continue;
85
+ }
86
+ }
87
+ if (inCodeBlock)
88
+ continue;
89
+ // <Steps> open resets the per-block counter unconditionally. Ordering
90
+ // matters: open-reset must run BEFORE step matching on the same line so
91
+ // `<Steps><Step title="A">...</Step>` numbers "A" as 1.
92
+ if (STEPS_OPEN_REGEX.test(line)) {
93
+ inStepsBlock = true;
94
+ stepCounter = 0;
95
+ }
96
+ const headingMatch = line.match(HEADING_REGEX);
97
+ if (headingMatch) {
98
+ const level = headingMatch[1].length;
99
+ const text = headingMatch[2].trim();
100
+ if (generateSlug(text)) {
101
+ const id = slugger.slug(text);
102
+ headings.push({ id, text, level, line: i + 1 });
103
+ }
104
+ }
105
+ for (const match of line.matchAll(UPDATE_LABEL_REGEX)) {
106
+ const text = match[1] ?? match[2];
107
+ if (!generateSlug(text))
108
+ continue;
109
+ const id = slugger.slug(text);
110
+ headings.push({ id, text, level: 2, line: i + 1, isUpdate: true });
111
+ }
112
+ if (inStepsBlock) {
113
+ for (const match of line.matchAll(STEP_TITLE_REGEX)) {
114
+ const text = match[1] ?? match[2];
115
+ if (!generateSlug(text))
116
+ continue;
117
+ stepCounter += 1;
118
+ const id = slugger.slug(text);
119
+ headings.push({
120
+ id,
121
+ text,
122
+ level: 3,
123
+ line: i + 1,
124
+ stepNumber: stepCounter,
125
+ });
126
+ }
127
+ }
128
+ // </Steps> close runs AFTER step matching so `</Steps>` on the same line
129
+ // as the last <Step> still picks up that step.
130
+ if (STEPS_CLOSE_REGEX.test(line)) {
131
+ inStepsBlock = false;
132
+ }
133
+ }
134
+ return headings;
135
+ }
136
+ /**
137
+ * Check if MDX content has api: or openapi: in its frontmatter.
138
+ * API endpoint pages generate dynamic anchor IDs at render time (#param-*, etc.)
139
+ * that aren't in the MDX source, so fragment validation is skipped for them.
140
+ */
141
+ function hasApiFrontmatter(content) {
142
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
143
+ if (!match)
144
+ return false;
145
+ return /^(openapi|api):/m.test(match[1]);
146
+ }
147
+ /**
148
+ * Build a map of page path -> heading slug set.
149
+ * Keys are page paths without .mdx extension (matching docs.json navigation format).
150
+ * Skips API endpoint pages (they generate dynamic anchors at render time).
151
+ *
152
+ * Unlike buildHeadingMapFromFiles() in validate-links.cjs, this doesn't add dual
153
+ * keys for index pages. The CJS validator handles index resolution via
154
+ * getTargetHeadingSlugs() which derives the key from the filesystem path.
155
+ *
156
+ * @param fileContents - Map of relative file paths (with .mdx) to content.
157
+ * Keys come from build.ts fileContents Map (e.g., 'getting-started.mdx', 'api/auth.mdx').
158
+ */
159
+ export function buildHeadingMap(fileContents) {
160
+ const map = new Map();
161
+ for (const [filePath, content] of fileContents) {
162
+ if (hasApiFrontmatter(content))
163
+ continue;
164
+ const headings = extractHeadings(content);
165
+ const slugs = new Set(headings.map((h) => h.id));
166
+ const pagePath = filePath.replace(/\.mdx$/, '');
167
+ map.set(pagePath, slugs);
168
+ }
169
+ return map;
170
+ }
171
+ //# sourceMappingURL=heading-extractor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heading-extractor.js","sourceRoot":"","sources":["../../src/lib/heading-extractor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export interface MissingDescription {
2
+ file: string;
3
+ title: string;
4
+ pageText: string;
5
+ }
6
+ export declare function scanMissingDescriptions(pagePaths: string[], readPage: (p: string) => string | null, resolveFile?: (p: string) => string | null): MissingDescription[];
7
+ //# sourceMappingURL=missing-description-scan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-description-scan.d.ts","sourceRoot":"","sources":["../../src/lib/missing-description-scan.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,kBAAkB;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;CAAE;AAEtF,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,EAAE,EACnB,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,EAKtC,WAAW,GAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,IAAwB,GAC5D,kBAAkB,EAAE,CAiBtB"}
@@ -0,0 +1,28 @@
1
+ // builder/cli/src/lib/missing-description-scan.ts
2
+ import matter from 'gray-matter';
3
+ export function scanMissingDescriptions(pagePaths, readPage,
4
+ // Map a nav page key to its actual on-disk relative path. A page reference like
5
+ // "api" may be backed by "api.mdx" OR "api/index.mdx"; the write target must be the
6
+ // real file, not a fabricated "api.mdx" (which would miss the dir-index page).
7
+ // Defaults to "<page>.mdx" for callers (and tests) where the key IS the file base.
8
+ resolveFile = (p) => `${p}.mdx`) {
9
+ const out = [];
10
+ for (const page of pagePaths) {
11
+ const src = readPage(page);
12
+ if (src == null)
13
+ continue;
14
+ const file = resolveFile(page);
15
+ if (file == null)
16
+ continue; // page resolved to no on-disk file — nowhere to write a description
17
+ const parsed = matter(src);
18
+ const desc = parsed.data.description?.trim();
19
+ if (desc)
20
+ continue; // already has one
21
+ const title = parsed.data.title?.trim() ||
22
+ parsed.content.match(/^#\s+(.+)$/m)?.[1]?.trim() ||
23
+ page.split('/').pop() || page;
24
+ out.push({ file, title, pageText: parsed.content.trim() });
25
+ }
26
+ return out;
27
+ }
28
+ //# sourceMappingURL=missing-description-scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-description-scan.js","sourceRoot":"","sources":["../../src/lib/missing-description-scan.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,OAAO,MAAM,MAAM,aAAa,CAAC;AAIjC,MAAM,UAAU,uBAAuB,CACrC,SAAmB,EACnB,QAAsC;AACtC,gFAAgF;AAChF,oFAAoF;AACpF,+EAA+E;AAC/E,mFAAmF;AACnF,cAA4C,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM;IAE7D,MAAM,GAAG,GAAyB,EAAE,CAAC;IACrC,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC3B,IAAI,GAAG,IAAI,IAAI;YAAE,SAAS;QAC1B,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;QAC/B,IAAI,IAAI,IAAI,IAAI;YAAE,SAAS,CAAC,oEAAoE;QAChG,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAC3B,MAAM,IAAI,GAAI,MAAM,CAAC,IAAI,CAAC,WAAkC,EAAE,IAAI,EAAE,CAAC;QACrE,IAAI,IAAI;YAAE,SAAS,CAAC,kBAAkB;QACtC,MAAM,KAAK,GACR,MAAM,CAAC,IAAI,CAAC,KAA4B,EAAE,IAAI,EAAE;YACjD,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE;YAChD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC;QAChC,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jamdesk",
3
- "version": "1.1.149",
3
+ "version": "1.1.151",
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",
@@ -128,9 +128,9 @@
128
128
  "devDependencies": {
129
129
  "@mdx-js/mdx": "^3.1.1",
130
130
  "@types/fs-extra": "^11.0.0",
131
- "@types/node": "^25.8.0",
131
+ "@types/node": "^25.9.3",
132
132
  "typescript": "^6.0.2",
133
- "vitest": "^4.1.5"
133
+ "vitest": "^4.1.9"
134
134
  },
135
135
  "engines": {
136
136
  "node": ">=20.0.0"
@@ -64,7 +64,7 @@ export function ApiEndpoint({ method, path, baseUrl = 'https://api.jamdesk.com/a
64
64
  };
65
65
 
66
66
  return (
67
- <div data-api-endpoint="true" className="flex items-center gap-3 px-3 py-1.5 rounded-xl border border-[var(--color-border)] bg-[var(--color-bg-primary)] mb-4 not-prose">
67
+ <div data-api-endpoint="true" data-markdown-ignore className="flex items-center gap-3 px-3 py-1.5 rounded-xl border border-[var(--color-border)] bg-[var(--color-bg-primary)] mb-4 not-prose">
68
68
  {/* Method badge */}
69
69
  <span className={`px-2.5 py-1 text-xs font-bold rounded-md ${colors.bg} ${colors.text}`}>
70
70
  {method}
@@ -829,7 +829,7 @@ export function OpenApiEndpoint({
829
829
 
830
830
  if (playgroundOnly) {
831
831
  return (
832
- <div className="openapi-endpoint">
832
+ <div className="openapi-endpoint" data-markdown-ignore>
833
833
  <ApiEndpoint
834
834
  method={method}
835
835
  path={path}
@@ -842,7 +842,12 @@ export function OpenApiEndpoint({
842
842
  }
843
843
 
844
844
  return (
845
- <div className="openapi-endpoint">
845
+ /* data-markdown-ignore: the endpoint UI is generated from the spec
846
+ and intentionally absent from the .md export (which points agents
847
+ at api-specs.zip). afdocs strips [data-markdown-ignore] before the
848
+ markdown-content-parity comparison, so OpenAPI pages don't false-
849
+ fail parity. */
850
+ <div className="openapi-endpoint" data-markdown-ignore>
846
851
  {/* Deprecation Warning */}
847
852
  {deprecated && (
848
853
  <div className="flex items-center gap-2 p-3 mb-4 bg-amber-500/10 border border-amber-500/20 rounded-lg text-amber-600 dark:text-amber-400">
@@ -16,6 +16,14 @@ const WORDMARK_STYLE: CSSProperties = {
16
16
  backgroundColor: 'currentColor',
17
17
  WebkitMask: WORDMARK_MASK,
18
18
  mask: WORDMARK_MASK,
19
+ // The wordmark SVG's viewBox runs from the j-dot (top) to the j-descender
20
+ // (bottom), so its typographic baseline sits ~76% down the box. Under the
21
+ // link's `items-baseline` the span's box-bottom lands on the text baseline,
22
+ // which leaves the glyphs ~3.3px ABOVE it (measured live). Nudge down by that
23
+ // much so the wordmark and "Powered by" share a baseline. Inline transform
24
+ // (not a Tailwind `translate-y-*` class) because the theme's CSS build purges
25
+ // arbitrary transform utilities — that silently dropped the previous fix.
26
+ transform: 'translateY(3.5px)',
19
27
  };
20
28
 
21
29
  interface SocialFooterProps {
@@ -155,7 +163,7 @@ function BrandingLink({ projectSlug }: { projectSlug?: string }) {
155
163
  <span
156
164
  role="img"
157
165
  aria-label="Jamdesk"
158
- className="inline-block translate-y-[2px] text-[var(--color-text-muted)]/85 group-hover:text-[var(--color-text-primary)]"
166
+ className="inline-block text-[var(--color-text-muted)]/85 group-hover:text-[var(--color-text-primary)]"
159
167
  style={WORDMARK_STYLE}
160
168
  />
161
169
  </a>