@sanity/ailf 4.2.0 → 4.3.1
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/config/package-surface.ts +37 -0
- package/config/preflight-scoring.ts +26 -0
- package/dist/_vendor/ailf-core/artifact-registry.d.ts +1 -1
- package/dist/_vendor/ailf-core/artifact-registry.js +47 -0
- package/dist/_vendor/ailf-core/config-helpers.d.ts +35 -0
- package/dist/_vendor/ailf-core/config-helpers.js +67 -0
- package/dist/_vendor/ailf-core/index.d.ts +1 -1
- package/dist/_vendor/ailf-core/index.js +1 -1
- package/dist/_vendor/ailf-core/ports/context.d.ts +18 -0
- package/dist/_vendor/ailf-core/ports/doc-fetcher.d.ts +30 -0
- package/dist/_vendor/ailf-core/ports/index.d.ts +3 -1
- package/dist/_vendor/ailf-core/ports/index.js +1 -0
- package/dist/_vendor/ailf-core/ports/mode-handler.d.ts +23 -0
- package/dist/_vendor/ailf-core/ports/package-surface-resolver.d.ts +71 -0
- package/dist/_vendor/ailf-core/ports/package-surface-resolver.js +36 -0
- package/dist/_vendor/ailf-core/schemas/eval-config.d.ts +6 -0
- package/dist/_vendor/ailf-core/schemas/eval-config.js +14 -0
- package/dist/_vendor/ailf-core/schemas/index.d.ts +1 -0
- package/dist/_vendor/ailf-core/schemas/index.js +1 -0
- package/dist/_vendor/ailf-core/schemas/pipeline-request.d.ts +4 -0
- package/dist/_vendor/ailf-core/schemas/pipeline-request.js +7 -0
- package/dist/_vendor/ailf-core/schemas/symbol-preflight-report.d.ts +51 -0
- package/dist/_vendor/ailf-core/schemas/symbol-preflight-report.js +57 -0
- package/dist/_vendor/ailf-core/types/index.d.ts +12 -0
- package/dist/_vendor/ailf-core/types/index.js +1 -0
- package/dist/_vendor/ailf-core/types/package-surface.d.ts +36 -0
- package/dist/_vendor/ailf-core/types/package-surface.js +13 -0
- package/dist/_vendor/ailf-core/types/pipeline-request.d.ts +1 -0
- package/dist/_vendor/ailf-core/types/preflight-scoring.d.ts +52 -0
- package/dist/_vendor/ailf-core/types/preflight-scoring.js +18 -0
- package/dist/_vendor/ailf-core/types/repo-config.d.ts +14 -0
- package/dist/_vendor/ailf-core/types/symbol-preflight-report.d.ts +66 -0
- package/dist/_vendor/ailf-core/types/symbol-preflight-report.js +25 -0
- package/dist/adapters/api-client/build-request.d.ts +1 -0
- package/dist/adapters/api-client/build-request.js +3 -0
- package/dist/adapters/config-sources/file-config-adapter.js +1 -0
- package/dist/adapters/doc-fetchers/sanity-doc-fetcher.d.ts +4 -0
- package/dist/adapters/doc-fetchers/sanity-doc-fetcher.js +159 -82
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/package-surface/dts-package-surface.d.ts +46 -0
- package/dist/adapters/package-surface/dts-package-surface.js +173 -0
- package/dist/adapters/package-surface/in-memory-package-surface.d.ts +15 -0
- package/dist/adapters/package-surface/in-memory-package-surface.js +28 -0
- package/dist/adapters/package-surface/index.d.ts +9 -0
- package/dist/adapters/package-surface/index.js +8 -0
- package/dist/adapters/package-surface/parse-dts-exports.d.ts +31 -0
- package/dist/adapters/package-surface/parse-dts-exports.js +54 -0
- package/dist/adapters/task-sources/repo-schemas.d.ts +6 -0
- package/dist/adapters/task-sources/repo-schemas.js +15 -0
- package/dist/commands/pipeline-action.d.ts +2 -0
- package/dist/commands/pipeline-action.js +12 -0
- package/dist/commands/remote-pipeline.js +10 -2
- package/dist/commands/remote-results.d.ts +12 -1
- package/dist/commands/remote-results.js +25 -5
- package/dist/composition-root.js +9 -0
- package/dist/config/package-surface.ts +37 -0
- package/dist/config/preflight-scoring.ts +26 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/orchestration/build-app-context.js +1 -0
- package/dist/orchestration/pipeline-orchestrator.d.ts +19 -1
- package/dist/orchestration/pipeline-orchestrator.js +38 -0
- package/dist/orchestration/steps/calculate-scores-step.js +11 -0
- package/dist/orchestration/steps/generate-configs-step.js +16 -1
- package/dist/orchestration/steps/run-eval-step.js +27 -0
- package/dist/pipeline/calculate-scores.d.ts +66 -5
- package/dist/pipeline/calculate-scores.js +141 -27
- package/dist/pipeline/compiler/index.d.ts +1 -1
- package/dist/pipeline/compiler/index.js +1 -1
- package/dist/pipeline/compiler/literacy-bridge.d.ts +9 -0
- package/dist/pipeline/compiler/literacy-bridge.js +2 -0
- package/dist/pipeline/compiler/mode-handlers/literacy/assertions.d.ts +1 -1
- package/dist/pipeline/compiler/mode-handlers/literacy/assertions.js +31 -4
- package/dist/pipeline/compiler/mode-handlers/literacy/compiler.js +146 -1
- package/dist/pipeline/compiler/mode-handlers/literacy/index.js +2 -0
- package/dist/pipeline/compiler/mode-handlers/literacy/types.d.ts +17 -2
- package/dist/pipeline/compiler/rubric-resolution.d.ts +17 -1
- package/dist/pipeline/compiler/rubric-resolution.js +78 -2
- package/dist/pipeline/compiler/scoring-bridge.d.ts +49 -2
- package/dist/pipeline/compiler/scoring-bridge.js +104 -10
- package/dist/pipeline/eval-fingerprint.d.ts +9 -0
- package/dist/pipeline/eval-fingerprint.js +7 -1
- package/dist/pipeline/map-request-to-config.js +1 -0
- package/dist/pipeline/preflight/compute-preflight.d.ts +67 -0
- package/dist/pipeline/preflight/compute-preflight.js +118 -0
- package/dist/pipeline/preflight/emit-symbol-preflight.d.ts +51 -0
- package/dist/pipeline/preflight/emit-symbol-preflight.js +102 -0
- package/dist/pipeline/preflight/load-package-surface.d.ts +14 -0
- package/dist/pipeline/preflight/load-package-surface.js +19 -0
- package/dist/pipeline/preflight/load-preflight-context.d.ts +13 -0
- package/dist/pipeline/preflight/load-preflight-context.js +25 -0
- package/dist/pipeline/preflight/load-preflight-scoring.d.ts +12 -0
- package/dist/pipeline/preflight/load-preflight-scoring.js +17 -0
- package/dist/pipeline/preflight/parse-imports.d.ts +62 -0
- package/dist/pipeline/preflight/parse-imports.js +125 -0
- package/dist/report-store.d.ts +8 -0
- package/dist/report-store.js +55 -6
- package/dist/sanity/document-renderers.d.ts +45 -7
- package/dist/sanity/document-renderers.js +99 -13
- package/dist/sanity/queries.d.ts +11 -11
- package/dist/sanity/queries.js +7 -0
- package/dist/sanity/symbol-index.d.ts +98 -0
- package/dist/sanity/symbol-index.js +615 -0
- package/package.json +2 -1
|
@@ -18,8 +18,8 @@ import { join } from "path";
|
|
|
18
18
|
import { canonicalDocRefLabel, isIdRef, isPathRef, isPerspectiveRef, isSlugRef, } from "../../_vendor/ailf-core/index.js";
|
|
19
19
|
import { fetchUrlContent, } from "../../pipeline/fetch-url-content.js";
|
|
20
20
|
import { createPerspectiveClient, createPublishedClient, getSanityClient, } from "../../sanity/client.js";
|
|
21
|
-
import { renderDocument, } from "../../sanity/document-renderers.js";
|
|
22
|
-
import {
|
|
21
|
+
import { extractSymbolsForDoc, renderDocument, } from "../../sanity/document-renderers.js";
|
|
22
|
+
import { mergeSymbolIndexes, renderSymbolIndex, symbolIndexTierBreakdown, } from "../../sanity/symbol-index.js";
|
|
23
23
|
import { ALL_ARTICLES_QUERY, ALL_FEATURE_AREAS, ARTICLE_BY_ID_QUERY, ARTICLE_BY_SLUG_QUERY, ARTICLE_BY_SLUG_WITH_PERSPECTIVE_QUERY, ARTICLE_SLUG_BY_PATH_QUERY, ARTICLE_SLUG_BY_SECTION_PATH_QUERY, ARTICLES_IN_RELEASE_QUERY, ARTICLES_METADATA_BY_SLUGS_QUERY, DOCS_BY_IDS_QUERY, FEATURE_AREA_QUERIES, } from "../../sanity/queries.js";
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
25
25
|
// Helpers
|
|
@@ -35,11 +35,14 @@ function escapeNunjucks(text) {
|
|
|
35
35
|
function estimateTokens(text) {
|
|
36
36
|
return Math.ceil(text.length / 4);
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Read a `DocumentForRender` field that the GROQ projection set as a
|
|
40
|
+
* scalar string, gracefully returning `undefined` when the projection
|
|
41
|
+
* shape is empty/unexpected (defensive for partial overlay docs).
|
|
42
|
+
*/
|
|
43
|
+
function readString(doc, key) {
|
|
44
|
+
const value = doc[key];
|
|
45
|
+
return typeof value === "string" ? value : undefined;
|
|
43
46
|
}
|
|
44
47
|
/**
|
|
45
48
|
* Bridge DocSourceConfig (domain type) to getSanityClient overrides.
|
|
@@ -154,12 +157,12 @@ export class SanityDocFetcher {
|
|
|
154
157
|
console.log(` Resolving ${source.documentIds.length} document ID(s) against canonical set...`);
|
|
155
158
|
documentOverlay = await this.resolveDocumentOverlay(source.documentIds, allSlugs, source);
|
|
156
159
|
const summary = {
|
|
157
|
-
appendedCount: documentOverlay.
|
|
160
|
+
appendedCount: documentOverlay.appendedDocs.length,
|
|
158
161
|
documentIds: source.documentIds,
|
|
159
162
|
replacedSlugs: [...documentOverlay.replacements.keys()],
|
|
160
163
|
};
|
|
161
164
|
metadata.documentOverlay = summary;
|
|
162
|
-
console.log(` Document overlay: ${documentOverlay.replacements.size} replacement(s), ${documentOverlay.
|
|
165
|
+
console.log(` Document overlay: ${documentOverlay.replacements.size} replacement(s), ${documentOverlay.appendedDocs.length} appended`);
|
|
163
166
|
}
|
|
164
167
|
// 5. URL content fetch — fetch direct URLs
|
|
165
168
|
const urlContent = [];
|
|
@@ -219,37 +222,62 @@ export class SanityDocFetcher {
|
|
|
219
222
|
}
|
|
220
223
|
const affectedSlugs = new Set(slugPerspective.keys());
|
|
221
224
|
const removedSlugs = new Set(releaseImpact?.removed ?? []);
|
|
222
|
-
const
|
|
223
|
-
// 7. Assemble per-task context
|
|
225
|
+
const docsBySlug = await this.fetchCanonicalDocs(allSlugs, affectedSlugs, removedSlugs, documentOverlay, source, slugPerspective);
|
|
226
|
+
// 7. Assemble per-task context + symbol index. Both surfaces dispatch
|
|
227
|
+
// through the renderer registry uniformly — the slug-flow, id-ref flow,
|
|
228
|
+
// and overlay flow all produce `DocumentForRender` objects here, and the
|
|
229
|
+
// registry decides how to render Markdown and extract symbols per
|
|
230
|
+
// `_type`. Per-task assembly aggregates symbols across all references
|
|
231
|
+
// attached to the task, writes:
|
|
232
|
+
// - `contexts/canonical/<task.id>.md` — existing rendered-doc
|
|
233
|
+
// artifact, consumed by Promptfoo `vars.docs`.
|
|
234
|
+
// - `contexts/canonical-symbols/<task.id>.md` — W0197 symbol-index
|
|
235
|
+
// artifact, written only when the task produced ≥1 symbol.
|
|
236
|
+
// - `contexts/canonical-symbols/manifest.json` — per-task manifest
|
|
237
|
+
// listing which tasks produced indexes (with counts + tier
|
|
238
|
+
// breakdowns). The grader-context consumer reads this manifest
|
|
239
|
+
// to decide between symbol-index and full-doc injection.
|
|
224
240
|
const canonicalDir = join(this.rootDir, "contexts", "canonical");
|
|
241
|
+
const symbolsDir = join(this.rootDir, "contexts", "canonical-symbols");
|
|
225
242
|
mkdirSync(canonicalDir, { recursive: true });
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
243
|
+
mkdirSync(symbolsDir, { recursive: true });
|
|
244
|
+
const renderCtx = { fetchUrl: this.fetchAttachmentBody };
|
|
245
|
+
const renderAndExtract = async (doc) => {
|
|
246
|
+
const rendered = await renderDocument(doc, renderCtx);
|
|
247
|
+
const symbols = await extractSymbolsForDoc(doc, renderCtx);
|
|
248
|
+
return { markdown: rendered.content, symbols, slug: rendered.slug };
|
|
249
|
+
};
|
|
250
|
+
const consumeDoc = async (doc, fallbackSlug) => {
|
|
251
|
+
const { markdown, symbols, slug } = await renderAndExtract(doc);
|
|
252
|
+
if (!markdown)
|
|
253
|
+
return null;
|
|
254
|
+
return { markdown, slug: fallbackSlug ?? slug, symbols };
|
|
255
|
+
};
|
|
256
|
+
const symbolIndexManifest = [];
|
|
257
|
+
const contexts = await Promise.all(tasks.map(async (task) => {
|
|
258
|
+
// Resolve each ref to a "consumed" record (or null). Refs that
|
|
259
|
+
// hit the slug-flow / overlay path render+extract via the
|
|
260
|
+
// registry; non-article id-refs were already rendered+extracted
|
|
261
|
+
// in resolveIdRefs and live in contentById.
|
|
262
|
+
const refResolutions = await Promise.all((task.context?.docs ?? []).map(async (ref) => {
|
|
231
263
|
if (isPerspectiveRef(ref)) {
|
|
232
264
|
const expanded = perspectiveToSlugs.get(ref.perspective) ?? [];
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
slugs.push(slug);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
continue;
|
|
265
|
+
const docs = expanded
|
|
266
|
+
.filter((slug) => !removedSlugs.has(slug))
|
|
267
|
+
.map((slug) => ({ slug, doc: docsBySlug.get(slug) }))
|
|
268
|
+
.filter((e) => e.doc !== undefined);
|
|
269
|
+
const consumed = await Promise.all(docs.map((e) => consumeDoc(e.doc, e.slug)));
|
|
270
|
+
return consumed.filter((c) => c !== null);
|
|
243
271
|
}
|
|
244
|
-
// Non-article id-refs bypass the slug-keyed content map: their
|
|
245
|
-
// rendered Markdown was produced by `resolveIdRefs` and is cached
|
|
246
|
-
// by document `_id`. Article id-refs continue to go through the
|
|
247
|
-
// slug-flow below (consistent with overlay/perspective handling).
|
|
248
272
|
if (isIdRef(ref) && idResolution.contentById.has(ref.id)) {
|
|
249
273
|
const entry = idResolution.contentById.get(ref.id);
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
274
|
+
return [
|
|
275
|
+
{
|
|
276
|
+
markdown: entry.content,
|
|
277
|
+
slug: entry.slug,
|
|
278
|
+
symbols: entry.symbolIndex,
|
|
279
|
+
},
|
|
280
|
+
];
|
|
253
281
|
}
|
|
254
282
|
let slug;
|
|
255
283
|
if (isSlugRef(ref)) {
|
|
@@ -262,33 +290,77 @@ export class SanityDocFetcher {
|
|
|
262
290
|
slug = pathToSlug.get(ref.path);
|
|
263
291
|
}
|
|
264
292
|
if (!slug)
|
|
265
|
-
|
|
293
|
+
return null;
|
|
266
294
|
if (removedSlugs.has(slug))
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
|
|
295
|
+
return null;
|
|
296
|
+
const doc = docsBySlug.get(slug);
|
|
297
|
+
if (!doc)
|
|
298
|
+
return null;
|
|
299
|
+
const consumed = await consumeDoc(doc, slug);
|
|
300
|
+
return consumed ? [consumed] : null;
|
|
301
|
+
}));
|
|
302
|
+
// Overlay docs (appended) flow through the same registry dispatch.
|
|
303
|
+
const overlayConsumed = [];
|
|
304
|
+
if (documentOverlay && documentOverlay.appendedDocs.length > 0) {
|
|
305
|
+
const consumed = await Promise.all(documentOverlay.appendedDocs.map((doc) => consumeDoc(doc)));
|
|
306
|
+
for (const c of consumed) {
|
|
307
|
+
if (c)
|
|
308
|
+
overlayConsumed.push(c);
|
|
272
309
|
}
|
|
273
310
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
311
|
+
const parts = [];
|
|
312
|
+
const slugs = [];
|
|
313
|
+
const taskIndexes = [];
|
|
314
|
+
const consumed = [
|
|
315
|
+
...refResolutions.flat().filter((c) => c !== null),
|
|
316
|
+
...overlayConsumed,
|
|
317
|
+
];
|
|
318
|
+
for (const c of consumed) {
|
|
319
|
+
parts.push(c.markdown);
|
|
320
|
+
slugs.push(c.slug);
|
|
321
|
+
if (c.symbols.symbols.length > 0)
|
|
322
|
+
taskIndexes.push(c.symbols);
|
|
277
323
|
}
|
|
278
|
-
// Append URL-fetched content
|
|
324
|
+
// Append URL-fetched content. URLs are raw markdown blobs from
|
|
325
|
+
// external sources (no Sanity doc shape), so they don't contribute
|
|
326
|
+
// to the per-task symbol index.
|
|
279
327
|
if (urlContent.length > 0) {
|
|
280
328
|
parts.push(...urlContent);
|
|
281
329
|
}
|
|
282
330
|
const combined = escapeNunjucks(parts.join("\n\n---\n\n"));
|
|
283
331
|
const contextPath = join(canonicalDir, `${task.id}.md`);
|
|
284
332
|
writeFileSync(contextPath, combined, "utf-8");
|
|
333
|
+
// Aggregate per-task symbol index. Always emit a manifest entry —
|
|
334
|
+
// entry presence is the consumer's signal that extraction ran.
|
|
335
|
+
// The per-task .md file is only written when the index is
|
|
336
|
+
// non-empty; a missing file under an entry-present manifest is
|
|
337
|
+
// permitted (consumer treats it the same as count zero).
|
|
338
|
+
const merged = mergeSymbolIndexes(taskIndexes);
|
|
339
|
+
const tierBreakdown = symbolIndexTierBreakdown(merged);
|
|
340
|
+
const symbolsRelativePath = `contexts/canonical-symbols/${task.id}.md`;
|
|
341
|
+
if (merged.symbols.length > 0) {
|
|
342
|
+
const indexMarkdown = renderSymbolIndex(merged, task.title);
|
|
343
|
+
writeFileSync(join(symbolsDir, `${task.id}.md`), indexMarkdown, "utf-8");
|
|
344
|
+
}
|
|
345
|
+
symbolIndexManifest.push({
|
|
346
|
+
taskId: task.id,
|
|
347
|
+
path: symbolsRelativePath,
|
|
348
|
+
symbolCount: merged.symbols.length,
|
|
349
|
+
tierBreakdown,
|
|
350
|
+
});
|
|
285
351
|
return {
|
|
286
352
|
taskId: task.id,
|
|
287
353
|
content: combined,
|
|
288
354
|
slugs,
|
|
289
355
|
tokenCount: estimateTokens(combined),
|
|
290
356
|
};
|
|
291
|
-
});
|
|
357
|
+
}));
|
|
358
|
+
// Persist the symbol-index manifest so the literacy compiler can read
|
|
359
|
+
// it to decide between symbol-index and full-doc grader-context
|
|
360
|
+
// injection per task. Sorted for stable diffs across runs.
|
|
361
|
+
symbolIndexManifest.sort((a, b) => a.taskId.localeCompare(b.taskId));
|
|
362
|
+
writeFileSync(join(symbolsDir, "manifest.json"), JSON.stringify({ entries: symbolIndexManifest }, null, 2), "utf-8");
|
|
363
|
+
metadata.symbolIndexes = symbolIndexManifest;
|
|
292
364
|
const hasMetadata = Object.keys(metadata).length > 0;
|
|
293
365
|
return {
|
|
294
366
|
contexts,
|
|
@@ -370,17 +442,18 @@ export class SanityDocFetcher {
|
|
|
370
442
|
}
|
|
371
443
|
continue;
|
|
372
444
|
}
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
});
|
|
445
|
+
const renderCtx = { fetchUrl: this.fetchAttachmentBody };
|
|
446
|
+
const rendered = await renderDocument(doc, renderCtx);
|
|
376
447
|
if (!rendered.content) {
|
|
377
448
|
console.warn(` [warn] doc "${id}" resolved as "${doc._type}" but produced empty context (referenced by task(s): ${taskIds.join(", ")})`);
|
|
378
449
|
continue;
|
|
379
450
|
}
|
|
451
|
+
const symbolIndex = await extractSymbolsForDoc(doc, renderCtx);
|
|
380
452
|
contentById.set(id, {
|
|
381
453
|
content: rendered.content,
|
|
382
454
|
slug: rendered.slug,
|
|
383
455
|
type: doc._type,
|
|
456
|
+
symbolIndex,
|
|
384
457
|
});
|
|
385
458
|
const _rev = doc._rev;
|
|
386
459
|
const title = doc.title;
|
|
@@ -411,10 +484,14 @@ export class SanityDocFetcher {
|
|
|
411
484
|
* `typesReference` renderer to inline typedoc JSON. Returns `null`
|
|
412
485
|
* on any HTTP/network failure rather than throwing — the renderer
|
|
413
486
|
* surfaces a placeholder so the rest of the context still renders.
|
|
487
|
+
*
|
|
488
|
+
* Wrapped in a 30s `AbortSignal.timeout` so a slow CDN can't hang the
|
|
489
|
+
* eval pipeline indefinitely. Timeouts surface as a single `null`
|
|
490
|
+
* return like any other fetch failure.
|
|
414
491
|
*/
|
|
415
492
|
fetchAttachmentBody = async (url) => {
|
|
416
493
|
try {
|
|
417
|
-
const response = await fetch(url);
|
|
494
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(30_000) });
|
|
418
495
|
if (!response.ok) {
|
|
419
496
|
console.warn(` [warn] attachment fetch failed for ${url}: HTTP ${response.status}`);
|
|
420
497
|
return null;
|
|
@@ -574,7 +651,7 @@ export class SanityDocFetcher {
|
|
|
574
651
|
// -----------------------------------------------------------------------
|
|
575
652
|
async resolveDocumentOverlay(documentIds, canonicalSlugs, source) {
|
|
576
653
|
const overlay = {
|
|
577
|
-
|
|
654
|
+
appendedDocs: [],
|
|
578
655
|
replacements: new Map(),
|
|
579
656
|
};
|
|
580
657
|
if (documentIds.length === 0)
|
|
@@ -588,16 +665,14 @@ export class SanityDocFetcher {
|
|
|
588
665
|
console.warn(` [warn] No article found for document ID "${id}"`);
|
|
589
666
|
continue;
|
|
590
667
|
}
|
|
591
|
-
const
|
|
592
|
-
if (
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
overlay.replacements.set(doc.slug, content);
|
|
596
|
-
console.log(` 📄 Document ${id} → replaces canonical doc "${doc.slug}"`);
|
|
668
|
+
const slug = readString(doc, "slug");
|
|
669
|
+
if (slug && canonicalSlugs.has(slug)) {
|
|
670
|
+
overlay.replacements.set(slug, doc);
|
|
671
|
+
console.log(` 📄 Document ${id} → replaces canonical doc "${slug}"`);
|
|
597
672
|
}
|
|
598
673
|
else {
|
|
599
|
-
overlay.
|
|
600
|
-
const slugInfo =
|
|
674
|
+
overlay.appendedDocs.push(doc);
|
|
675
|
+
const slugInfo = slug ? ` (slug: "${slug}")` : "";
|
|
601
676
|
console.log(` 📄 Document ${id} → appended as additional context${slugInfo}`);
|
|
602
677
|
}
|
|
603
678
|
}
|
|
@@ -617,9 +692,9 @@ export class SanityDocFetcher {
|
|
|
617
692
|
const doc = await client.fetch(ARTICLE_BY_SLUG_QUERY, { slug });
|
|
618
693
|
if (!doc) {
|
|
619
694
|
console.warn(` [warn] No article found for slug "${slug}"`);
|
|
620
|
-
return
|
|
695
|
+
return null;
|
|
621
696
|
}
|
|
622
|
-
return
|
|
697
|
+
return doc;
|
|
623
698
|
}
|
|
624
699
|
async fetchArticleBySlugWithPerspective(slug, source) {
|
|
625
700
|
if (!source.perspective) {
|
|
@@ -629,24 +704,26 @@ export class SanityDocFetcher {
|
|
|
629
704
|
const doc = await client.fetch(ARTICLE_BY_SLUG_WITH_PERSPECTIVE_QUERY, { slug });
|
|
630
705
|
if (!doc) {
|
|
631
706
|
console.warn(` [warn] No article found for slug "${slug}" in perspective "${source.perspective}"`);
|
|
632
|
-
return
|
|
707
|
+
return null;
|
|
633
708
|
}
|
|
634
|
-
return
|
|
709
|
+
return doc;
|
|
635
710
|
}
|
|
636
711
|
// -----------------------------------------------------------------------
|
|
637
712
|
// Private: Fetch all canonical docs with perspective/overlay awareness
|
|
638
713
|
// -----------------------------------------------------------------------
|
|
639
714
|
async fetchCanonicalDocs(allSlugs, affectedSlugs, removedSlugs, overlay, source, slugPerspective) {
|
|
640
|
-
const
|
|
715
|
+
const docsBySlug = new Map();
|
|
641
716
|
for (const slug of allSlugs) {
|
|
642
717
|
if (removedSlugs.has(slug))
|
|
643
718
|
continue;
|
|
644
719
|
// Check if this slug has a document overlay replacement
|
|
645
720
|
if (overlay?.replacements.has(slug)) {
|
|
646
721
|
console.log(` Fetching: ${slug} (from document overlay)`);
|
|
647
|
-
|
|
722
|
+
docsBySlug.set(slug, overlay.replacements.get(slug));
|
|
723
|
+
continue;
|
|
648
724
|
}
|
|
649
|
-
|
|
725
|
+
let doc;
|
|
726
|
+
if (affectedSlugs.has(slug)) {
|
|
650
727
|
// Use the per-slug perspective when available; fall back to
|
|
651
728
|
// source-level perspective. This ensures perspective-ref-expanded
|
|
652
729
|
// slugs are fetched from their specific release even when no
|
|
@@ -655,23 +732,22 @@ export class SanityDocFetcher {
|
|
|
655
732
|
if (perspective && source) {
|
|
656
733
|
const perspectiveSource = { ...source, perspective };
|
|
657
734
|
console.log(` Fetching: ${slug} (from perspective: ${perspective})`);
|
|
658
|
-
|
|
659
|
-
contentBySlug.set(slug, content);
|
|
735
|
+
doc = await this.fetchArticleBySlugWithPerspective(slug, perspectiveSource);
|
|
660
736
|
}
|
|
661
737
|
else {
|
|
662
738
|
// No perspective available — fetch from published
|
|
663
739
|
console.log(` Fetching: ${slug}`);
|
|
664
|
-
|
|
665
|
-
contentBySlug.set(slug, content);
|
|
740
|
+
doc = await this.fetchArticleBySlug(slug);
|
|
666
741
|
}
|
|
667
742
|
}
|
|
668
743
|
else {
|
|
669
744
|
console.log(` Fetching: ${slug}`);
|
|
670
|
-
|
|
671
|
-
contentBySlug.set(slug, content);
|
|
745
|
+
doc = await this.fetchArticleBySlug(slug);
|
|
672
746
|
}
|
|
747
|
+
if (doc)
|
|
748
|
+
docsBySlug.set(slug, doc);
|
|
673
749
|
}
|
|
674
|
-
return
|
|
750
|
+
return docsBySlug;
|
|
675
751
|
}
|
|
676
752
|
// -----------------------------------------------------------------------
|
|
677
753
|
// Public non-port methods — opt-in CLI features
|
|
@@ -694,7 +770,8 @@ export class SanityDocFetcher {
|
|
|
694
770
|
console.warn(` [warn] No articles found for "${feature}"`);
|
|
695
771
|
continue;
|
|
696
772
|
}
|
|
697
|
-
const
|
|
773
|
+
const rendered = await Promise.all(docs.map((d) => renderDocument(d, { fetchUrl: this.fetchAttachmentBody })));
|
|
774
|
+
const combined = rendered.map((r) => r.content).join("\n\n---\n\n");
|
|
698
775
|
const outPath = join(contextsDir, `${feature}.md`);
|
|
699
776
|
writeFileSync(outPath, escapeNunjucks(combined), "utf-8");
|
|
700
777
|
console.log(` ${feature}: ${docs.length} articles, ~${estimateTokens(combined)} tokens`);
|
|
@@ -711,16 +788,16 @@ export class SanityDocFetcher {
|
|
|
711
788
|
const baseUrl = source?.baseUrl ?? "https://www.sanity.io/docs";
|
|
712
789
|
console.log("\nGenerating full corpus...");
|
|
713
790
|
const docs = await client.fetch(ALL_ARTICLES_QUERY);
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
})
|
|
723
|
-
|
|
791
|
+
const rendered = await Promise.all(docs.map(async (d) => {
|
|
792
|
+
const result = await renderDocument(d, {
|
|
793
|
+
fetchUrl: this.fetchAttachmentBody,
|
|
794
|
+
});
|
|
795
|
+
// Corpus output prepends the article URL line so the dataset can
|
|
796
|
+
// be consumed by RAG retrievers that key on canonical URL.
|
|
797
|
+
const slug = readString(d, "slug") ?? result.slug;
|
|
798
|
+
return `URL: ${baseUrl}/${slug}\n\n${result.content}`;
|
|
799
|
+
}));
|
|
800
|
+
const corpus = rendered.join("\n\n---\n\n");
|
|
724
801
|
const outDir = join(this.rootDir, "contexts");
|
|
725
802
|
mkdirSync(outDir, { recursive: true });
|
|
726
803
|
writeFileSync(join(outDir, "full-corpus.md"), escapeNunjucks(corpus), "utf-8");
|
package/dist/adapters/index.d.ts
CHANGED
|
@@ -9,3 +9,4 @@ export { SanityDocFetcher } from "./doc-fetchers/index.js";
|
|
|
9
9
|
export { PromptfooEvalAdapter } from "./eval-runners/index.js";
|
|
10
10
|
export { ConsoleLogger, type ConsoleLoggerOptions, JsonLogger, QuietLogger, } from "./loggers/index.js";
|
|
11
11
|
export { CliConfigAdapter, FileConfigAdapter } from "./config-sources/index.js";
|
|
12
|
+
export { DtsPackageSurface, InMemoryPackageSurface, type DtsPackageSurfaceOptions, type PackageRootResolver, parseDtsExports, type ParsedDtsExports, } from "./package-surface/index.js";
|
package/dist/adapters/index.js
CHANGED
|
@@ -9,3 +9,4 @@ export { SanityDocFetcher } from "./doc-fetchers/index.js";
|
|
|
9
9
|
export { PromptfooEvalAdapter } from "./eval-runners/index.js";
|
|
10
10
|
export { ConsoleLogger, JsonLogger, QuietLogger, } from "./loggers/index.js";
|
|
11
11
|
export { CliConfigAdapter, FileConfigAdapter } from "./config-sources/index.js";
|
|
12
|
+
export { DtsPackageSurface, InMemoryPackageSurface, parseDtsExports, } from "./package-surface/index.js";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DtsPackageSurface — `PackageSurfaceResolver` adapter that reads installed
|
|
3
|
+
* package `.d.ts` files from `node_modules`.
|
|
4
|
+
*
|
|
5
|
+
* Resolution flow:
|
|
6
|
+
* 1. Find the package's root directory via the configured resolver
|
|
7
|
+
* (default uses `createRequire` against the working directory).
|
|
8
|
+
* 2. Read its `package.json` to capture the resolved `version` and the
|
|
9
|
+
* `.d.ts` entry path (`types`, `typings`, or `exports["."].types`).
|
|
10
|
+
* 3. Parse the entry `.d.ts` for top-level export declarations.
|
|
11
|
+
* 4. Follow ONE hop of `export * from "./relative"` re-exports — direct
|
|
12
|
+
* siblings only, no transitive walking.
|
|
13
|
+
* 5. Cache the result by package name for the resolver's lifetime.
|
|
14
|
+
*
|
|
15
|
+
* Throws `PackageSurfaceResolverError` with a typed `reason` when the
|
|
16
|
+
* package isn't installed or its types entry can't be resolved. Callers
|
|
17
|
+
* (the W0198 preflight) catch the error and convert it into per-binding
|
|
18
|
+
* `unresolved` findings rather than `missing` deductions.
|
|
19
|
+
*/
|
|
20
|
+
import { type PackageSurface, type PackageSurfaceResolver } from "../../_vendor/ailf-core/index.d.ts";
|
|
21
|
+
/**
|
|
22
|
+
* Resolves a package's installed root directory (the directory whose
|
|
23
|
+
* `package.json` describes it). Returns `null` when the package isn't
|
|
24
|
+
* installed in the configured lookup path.
|
|
25
|
+
*/
|
|
26
|
+
export type PackageRootResolver = (pkg: string) => string | null;
|
|
27
|
+
export interface DtsPackageSurfaceOptions {
|
|
28
|
+
/**
|
|
29
|
+
* How to find a package's root directory. Defaults to a `createRequire`
|
|
30
|
+
* walk from `resolveFromDir` (or the current working directory).
|
|
31
|
+
* Tests pass a fixture-aware resolver so they don't depend on real
|
|
32
|
+
* `node_modules` contents.
|
|
33
|
+
*/
|
|
34
|
+
resolvePackageRoot?: PackageRootResolver;
|
|
35
|
+
/**
|
|
36
|
+
* Directory whose `node_modules` chain is searched by the default
|
|
37
|
+
* resolver. Ignored when `resolvePackageRoot` is supplied.
|
|
38
|
+
*/
|
|
39
|
+
resolveFromDir?: string;
|
|
40
|
+
}
|
|
41
|
+
export declare class DtsPackageSurface implements PackageSurfaceResolver {
|
|
42
|
+
private readonly resolveRoot;
|
|
43
|
+
private readonly cache;
|
|
44
|
+
constructor(opts?: DtsPackageSurfaceOptions);
|
|
45
|
+
resolveExports(pkg: string): Promise<PackageSurface>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DtsPackageSurface — `PackageSurfaceResolver` adapter that reads installed
|
|
3
|
+
* package `.d.ts` files from `node_modules`.
|
|
4
|
+
*
|
|
5
|
+
* Resolution flow:
|
|
6
|
+
* 1. Find the package's root directory via the configured resolver
|
|
7
|
+
* (default uses `createRequire` against the working directory).
|
|
8
|
+
* 2. Read its `package.json` to capture the resolved `version` and the
|
|
9
|
+
* `.d.ts` entry path (`types`, `typings`, or `exports["."].types`).
|
|
10
|
+
* 3. Parse the entry `.d.ts` for top-level export declarations.
|
|
11
|
+
* 4. Follow ONE hop of `export * from "./relative"` re-exports — direct
|
|
12
|
+
* siblings only, no transitive walking.
|
|
13
|
+
* 5. Cache the result by package name for the resolver's lifetime.
|
|
14
|
+
*
|
|
15
|
+
* Throws `PackageSurfaceResolverError` with a typed `reason` when the
|
|
16
|
+
* package isn't installed or its types entry can't be resolved. Callers
|
|
17
|
+
* (the W0198 preflight) catch the error and convert it into per-binding
|
|
18
|
+
* `unresolved` findings rather than `missing` deductions.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
21
|
+
import { createRequire } from "node:module";
|
|
22
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
23
|
+
import { pathToFileURL } from "node:url";
|
|
24
|
+
import { PackageSurfaceResolverError, } from "../../_vendor/ailf-core/index.js";
|
|
25
|
+
import { parseDtsExports } from "./parse-dts-exports.js";
|
|
26
|
+
export class DtsPackageSurface {
|
|
27
|
+
resolveRoot;
|
|
28
|
+
cache = new Map();
|
|
29
|
+
constructor(opts = {}) {
|
|
30
|
+
this.resolveRoot =
|
|
31
|
+
opts.resolvePackageRoot ??
|
|
32
|
+
makeDefaultPackageRootResolver(opts.resolveFromDir ?? process.cwd());
|
|
33
|
+
}
|
|
34
|
+
async resolveExports(pkg) {
|
|
35
|
+
const cached = this.cache.get(pkg);
|
|
36
|
+
if (cached)
|
|
37
|
+
return cached;
|
|
38
|
+
const root = this.resolveRoot(pkg);
|
|
39
|
+
if (!root) {
|
|
40
|
+
throw new PackageSurfaceResolverError("package-not-installed", pkg, `Package "${pkg}" was not resolvable from the configured lookup path.`);
|
|
41
|
+
}
|
|
42
|
+
const pkgJsonPath = join(root, "package.json");
|
|
43
|
+
if (!existsSync(pkgJsonPath)) {
|
|
44
|
+
throw new PackageSurfaceResolverError("package-not-installed", pkg, `Package "${pkg}" has no package.json at "${pkgJsonPath}".`);
|
|
45
|
+
}
|
|
46
|
+
const pkgJson = readPackageJson(pkgJsonPath, pkg);
|
|
47
|
+
const version = pkgJson.version ?? "0.0.0";
|
|
48
|
+
const typesEntry = resolveTypesEntry(pkgJson, root);
|
|
49
|
+
if (!typesEntry) {
|
|
50
|
+
throw new PackageSurfaceResolverError("types-entry-missing", pkg, `Package "${pkg}@${version}" does not declare a \`types\` entry ` +
|
|
51
|
+
`(checked package.json \`types\`, \`typings\`, and \`exports["."].types\`).`);
|
|
52
|
+
}
|
|
53
|
+
const symbols = readSurface(pkg, version, typesEntry);
|
|
54
|
+
const surface = {
|
|
55
|
+
pkg,
|
|
56
|
+
version,
|
|
57
|
+
symbols,
|
|
58
|
+
};
|
|
59
|
+
this.cache.set(pkg, surface);
|
|
60
|
+
return surface;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function readPackageJson(path, pkg) {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
throw new PackageSurfaceResolverError("parse-failed", pkg, `Failed to parse "${path}": ${err instanceof Error ? err.message : String(err)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Pick the `.d.ts` entry the resolver should parse. Order:
|
|
73
|
+
* 1. `package.json#types`
|
|
74
|
+
* 2. `package.json#typings`
|
|
75
|
+
* 3. `exports["."].types` (string or object form)
|
|
76
|
+
*/
|
|
77
|
+
function resolveTypesEntry(pkgJson, root) {
|
|
78
|
+
const candidates = [];
|
|
79
|
+
if (typeof pkgJson.types === "string")
|
|
80
|
+
candidates.push(pkgJson.types);
|
|
81
|
+
if (typeof pkgJson.typings === "string")
|
|
82
|
+
candidates.push(pkgJson.typings);
|
|
83
|
+
const dotExport = pkgJson.exports && typeof pkgJson.exports === "object"
|
|
84
|
+
? pkgJson.exports["."]
|
|
85
|
+
: undefined;
|
|
86
|
+
if (typeof dotExport === "string") {
|
|
87
|
+
if (dotExport.endsWith(".d.ts"))
|
|
88
|
+
candidates.push(dotExport);
|
|
89
|
+
}
|
|
90
|
+
else if (dotExport && typeof dotExport === "object") {
|
|
91
|
+
const typesField = dotExport.types;
|
|
92
|
+
if (typeof typesField === "string")
|
|
93
|
+
candidates.push(typesField);
|
|
94
|
+
else if (typesField && typeof typesField === "object") {
|
|
95
|
+
// Conditional `types` entry (rare) — pick any string leaf.
|
|
96
|
+
for (const v of Object.values(typesField)) {
|
|
97
|
+
if (typeof v === "string") {
|
|
98
|
+
candidates.push(v);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
for (const candidate of candidates) {
|
|
105
|
+
const abs = isAbsolute(candidate) ? candidate : join(root, candidate);
|
|
106
|
+
if (existsSync(abs))
|
|
107
|
+
return abs;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
function readSurface(pkg, version, entryPath) {
|
|
112
|
+
const names = new Set();
|
|
113
|
+
const visited = new Set();
|
|
114
|
+
const parseFile = (path, hops) => {
|
|
115
|
+
if (visited.has(path))
|
|
116
|
+
return;
|
|
117
|
+
visited.add(path);
|
|
118
|
+
let src;
|
|
119
|
+
try {
|
|
120
|
+
src = readFileSync(path, "utf-8");
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
throw new PackageSurfaceResolverError("parse-failed", pkg, `Failed to read "${path}" for "${pkg}@${version}": ${err instanceof Error ? err.message : String(err)}`);
|
|
124
|
+
}
|
|
125
|
+
const parsed = parseDtsExports(src);
|
|
126
|
+
for (const name of parsed.names)
|
|
127
|
+
names.add(name);
|
|
128
|
+
if (hops <= 0)
|
|
129
|
+
return;
|
|
130
|
+
const baseDir = dirname(path);
|
|
131
|
+
for (const spec of parsed.reExports) {
|
|
132
|
+
if (!spec.startsWith("."))
|
|
133
|
+
continue; // bare specifier — out of scope
|
|
134
|
+
const resolved = resolveRelativeDts(baseDir, spec);
|
|
135
|
+
if (resolved)
|
|
136
|
+
parseFile(resolved, hops - 1);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
parseFile(entryPath, /* hops */ 1);
|
|
140
|
+
return [...names].sort().map((name) => ({ name, source: "types" }));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Resolve a relative re-export specifier to an existing `.d.ts` file.
|
|
144
|
+
* Tries `<spec>.d.ts`, `<spec>/index.d.ts`, and `<spec>` literally.
|
|
145
|
+
*/
|
|
146
|
+
function resolveRelativeDts(baseDir, spec) {
|
|
147
|
+
const base = resolve(baseDir, spec);
|
|
148
|
+
const candidates = [
|
|
149
|
+
base.endsWith(".d.ts") ? base : null,
|
|
150
|
+
`${base}.d.ts`,
|
|
151
|
+
join(base, "index.d.ts"),
|
|
152
|
+
].filter((p) => p !== null);
|
|
153
|
+
for (const path of candidates) {
|
|
154
|
+
if (existsSync(path))
|
|
155
|
+
return path;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
function makeDefaultPackageRootResolver(fromDir) {
|
|
160
|
+
// `createRequire` needs a file URL or path that ends with a slash so
|
|
161
|
+
// it knows it's a directory, not a file.
|
|
162
|
+
const anchor = fromDir.endsWith("/") ? fromDir : `${fromDir}/`;
|
|
163
|
+
const req = createRequire(pathToFileURL(anchor));
|
|
164
|
+
return (pkg) => {
|
|
165
|
+
try {
|
|
166
|
+
const pkgJsonPath = req.resolve(`${pkg}/package.json`);
|
|
167
|
+
return dirname(pkgJsonPath);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InMemoryPackageSurface — `PackageSurfaceResolver` test double.
|
|
3
|
+
*
|
|
4
|
+
* Backed by a plain `Map<string, PackageSurface>`; calls for unknown
|
|
5
|
+
* packages throw the same `package-not-installed` error the
|
|
6
|
+
* `DtsPackageSurface` adapter throws, so test scenarios for the
|
|
7
|
+
* `unresolved` path need no special handling.
|
|
8
|
+
*/
|
|
9
|
+
import { type PackageSurface, type PackageSurfaceResolver } from "../../_vendor/ailf-core/index.d.ts";
|
|
10
|
+
export declare class InMemoryPackageSurface implements PackageSurfaceResolver {
|
|
11
|
+
private readonly surfaces;
|
|
12
|
+
constructor(surfaces?: Iterable<PackageSurface>);
|
|
13
|
+
set(surface: PackageSurface): void;
|
|
14
|
+
resolveExports(pkg: string): Promise<PackageSurface>;
|
|
15
|
+
}
|