@mjasnikovs/pi-task 0.39.4 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -5
- package/dist/task/auto-orchestrator.d.ts +36 -0
- package/dist/task/auto-orchestrator.js +43 -6
- package/dist/task/cancel-points.d.ts +34 -6
- package/dist/task/cancel-points.js +62 -10
- package/dist/task/child-status.js +13 -1
- package/dist/task/context-attribution.js +18 -6
- package/dist/task/external-context.d.ts +8 -1
- package/dist/task/external-context.js +52 -4
- package/dist/task/orchestrator.js +47 -5
- package/dist/task/phases.js +2 -1
- package/dist/task/plan-orchestrator.js +10 -1
- package/dist/task/prompts.d.ts +7 -1
- package/dist/task/prompts.js +14 -4
- package/dist/task/research-worker.js +11 -0
- package/dist/task/run-bracket.js +13 -1
- package/dist/task/task-gates.js +34 -0
- package/dist/workers/docs-cache.js +50 -3
- package/dist/workers/docs-chunk.d.ts +6 -3
- package/dist/workers/docs-chunk.js +8 -5
- package/dist/workers/docs-core.d.ts +27 -3
- package/dist/workers/docs-core.js +104 -41
- package/dist/workers/docs-ecosystems.d.ts +173 -0
- package/dist/workers/docs-ecosystems.js +449 -0
- package/dist/workers/docs-index.d.ts +2 -1
- package/dist/workers/docs-index.js +55 -27
- package/dist/workers/docs-project.d.ts +10 -0
- package/dist/workers/docs-project.js +86 -24
- package/dist/workers/docs-resolve.d.ts +6 -1
- package/dist/workers/docs-resolve.js +4 -3
- package/dist/workers/docs-retrieve.d.ts +2 -0
- package/dist/workers/docs-retrieve.js +11 -11
- package/dist/workers/eco-cargo.d.ts +115 -0
- package/dist/workers/eco-cargo.js +793 -0
- package/dist/workers/eco-hackage.d.ts +93 -0
- package/dist/workers/eco-hackage.js +508 -0
- package/dist/workers/npm-version.d.ts +5 -3
- package/dist/workers/npm-version.js +6 -4
- package/dist/workers/pi-worker-docs.d.ts +18 -4
- package/dist/workers/pi-worker-docs.js +57 -19
- package/dist/workers/research-cache.d.ts +2 -13
- package/dist/workers/research-cache.js +22 -46
- package/dist/workers/shared.d.ts +16 -5
- package/dist/workers/shared.js +0 -0
- package/package.json +1 -1
|
@@ -4,7 +4,8 @@ import * as os from 'node:os';
|
|
|
4
4
|
import * as path from 'node:path';
|
|
5
5
|
import { openCache as defaultOpenCache } from './docs-cache.js';
|
|
6
6
|
import { ensureIndexed as defaultEnsureIndexed } from './docs-index.js';
|
|
7
|
-
import {
|
|
7
|
+
import { npmProfile, chooseEcosystem, defaultEcosystemIo, ECOSYSTEMS } from './docs-ecosystems.js';
|
|
8
|
+
import { resolvePackage as defaultResolvePackage, ResolveError, resolveTypeSource, typesPackageName, splitRuntimeNamespace } from './docs-resolve.js';
|
|
8
9
|
import { retrieveChunks as defaultRetrieveChunks, PACKAGE_RETRIEVE_LIMIT, RETRIEVE_CONTENT_BUDGET } from './docs-retrieve.js';
|
|
9
10
|
import { npmVersionLookup as defaultNpmVersionLookup } from './npm-version.js';
|
|
10
11
|
import { runChild } from '../shared/child-process.js';
|
|
@@ -126,16 +127,29 @@ export function findDeclaredRange(parentPkg, cwd) {
|
|
|
126
127
|
* `bun`, and cannot declare `bun-types`, so a sentence about what package.json
|
|
127
128
|
* does or does not say has to be a sentence about `bun`.
|
|
128
129
|
*/
|
|
129
|
-
export function buildVersionBanner(pin, resolved, version, cwd) {
|
|
130
|
+
export function buildVersionBanner(pin, resolved, version, cwd, profile = ECOSYSTEMS.npm) {
|
|
130
131
|
if (!pin)
|
|
131
132
|
return '';
|
|
132
133
|
const asked = pin.asked ?? resolved;
|
|
133
134
|
const grounded = resolved !== asked ? ` The types this answer reads come from ${resolved}.` : '';
|
|
135
|
+
const manifest = profile.manifestLabel;
|
|
136
|
+
const registry = profile.registryLabel;
|
|
134
137
|
if (pin.source === 'declared-range') {
|
|
135
138
|
return (`[VERSION] "${asked}" resolved to this project's declared range `
|
|
136
139
|
+ `${pin.range} (installed v${version}); the answer below is pinned to that `
|
|
137
140
|
+ `version.${grounded}\n\n`);
|
|
138
141
|
}
|
|
142
|
+
// The @types redirect chain and the four dependency maps are npm ideas, so the
|
|
143
|
+
// "declared, but only as X" wordings below can only be reached for npm. Every
|
|
144
|
+
// other registry gets the plain not-declared sentence, which is the true one:
|
|
145
|
+
// its own `declaredRange` already returned null.
|
|
146
|
+
if (profile.id !== 'npm') {
|
|
147
|
+
return (`[VERSION — verify] "${asked}" is not pinned by this project's ${manifest}, so `
|
|
148
|
+
+ `this answer is based on the latest ${registry} release (v${version}). Your `
|
|
149
|
+
+ `project may target a different MAJOR — confirm the version you intend and `
|
|
150
|
+
+ `treat any API that differs across majors as unverified until you check `
|
|
151
|
+
+ `it.${grounded}\n\n`);
|
|
152
|
+
}
|
|
139
153
|
// The install fell back to npm latest. A usable declaration can still exist
|
|
140
154
|
// further along the chain (`@types/<name>`) — it did not pin THIS install, so
|
|
141
155
|
// the banner reports it as provenance, not as a pin.
|
|
@@ -227,8 +241,11 @@ export async function runAutoInstall(spawn, packageName, opts = {}) {
|
|
|
227
241
|
*/
|
|
228
242
|
export async function acquirePackage(input) {
|
|
229
243
|
const { name, cwd, spawn, resolvePackage, signal } = input;
|
|
244
|
+
const profile = input.profile ?? ECOSYSTEMS.npm;
|
|
245
|
+
const io = input.io ?? defaultEcosystemIo({ spawn, signal });
|
|
246
|
+
const resolve = (from) => resolvePackage ? resolvePackage(name, from) : profile.resolve(name, from, io);
|
|
230
247
|
try {
|
|
231
|
-
return { ok: true, pkg:
|
|
248
|
+
return { ok: true, pkg: resolve(cwd), autoInstalled: false };
|
|
232
249
|
}
|
|
233
250
|
catch (firstErr) {
|
|
234
251
|
if (!(firstErr instanceof ResolveError) || firstErr.kind !== 'not_installed') {
|
|
@@ -238,22 +255,19 @@ export async function acquirePackage(input) {
|
|
|
238
255
|
// dep is declared in the project's package.json, install that range so a
|
|
239
256
|
// scaffolding answer is grounded in the project's major, not whatever npm
|
|
240
257
|
// currently tags `latest`.
|
|
241
|
-
const asked =
|
|
242
|
-
const declaredRange =
|
|
258
|
+
const asked = profile.parentPackage(name);
|
|
259
|
+
const declaredRange = profile.declaredRange(asked, cwd);
|
|
243
260
|
const pin = declaredRange ?
|
|
244
261
|
{ source: 'declared-range', range: declaredRange, asked }
|
|
245
262
|
: { source: 'npm-latest', asked };
|
|
246
|
-
const install = await
|
|
247
|
-
signal,
|
|
248
|
-
versionRange: declaredRange ?? undefined
|
|
249
|
-
});
|
|
263
|
+
const install = await profile.acquire(asked, declaredRange, io);
|
|
250
264
|
if (!install.success) {
|
|
251
265
|
return { ok: false, stage: 'install', stderr: install.stderr, pin, asked };
|
|
252
266
|
}
|
|
253
267
|
try {
|
|
254
268
|
return {
|
|
255
269
|
ok: true,
|
|
256
|
-
pkg:
|
|
270
|
+
pkg: resolve(install.installDir),
|
|
257
271
|
autoInstalled: true,
|
|
258
272
|
pin
|
|
259
273
|
};
|
|
@@ -276,7 +290,7 @@ async function tryResolveOrInstall(name, cwd, spawn, resolvePackage, signal, onA
|
|
|
276
290
|
/** The docs pipeline's adapter over the shared redirect walk (docs-resolve.ts):
|
|
277
291
|
* hops resolve through the auto-installing lookup, so a declaration package that is
|
|
278
292
|
* declared but not yet on disk is fetched rather than abandoned. */
|
|
279
|
-
async function resolveTypeSourceForDocs(pkg, requested, cwd, spawn, resolvePackage, signal) {
|
|
293
|
+
export async function resolveTypeSourceForDocs(pkg, requested, cwd, spawn, resolvePackage, signal) {
|
|
280
294
|
// A package acquired ONLY through a hop reports neither `autoInstalled` nor a
|
|
281
295
|
// pin unless this runs, so pi-worker-docs would emit no version banner for it.
|
|
282
296
|
// Report the last hop that actually installed.
|
|
@@ -295,17 +309,53 @@ export async function docsRaw(input) {
|
|
|
295
309
|
const openCache = input.openCache ?? defaultOpenCache;
|
|
296
310
|
const spawn = input.spawn ?? defaultSpawn;
|
|
297
311
|
const npmVersionLookup = input.npmVersionLookup ?? defaultNpmVersionLookup;
|
|
312
|
+
const io = defaultEcosystemIo({ spawn, signal: input.signal, ...input.io });
|
|
298
313
|
// A runtime builtin specifier (`bun:sql`, `node:fs`) is typed by the runtime's
|
|
299
314
|
// own types package, not a literal package of that colon-name — so resolve the
|
|
300
315
|
// runtime instead. This is what turns a `bun:sql` lookup into Bun's real SQL
|
|
301
316
|
// surface (`declare module "bun"` → `const sql: SQL`) rather than an
|
|
302
317
|
// `invalid_name` error, and it lets the docs tool disprove a phantom submodule.
|
|
303
318
|
const requested = splitRuntimeNamespace(input.pkg)?.runtime ?? input.pkg;
|
|
319
|
+
// Which registry, decided by the MANIFEST before anything else runs. A refusal
|
|
320
|
+
// must reach the caller having spawned nothing and asked no registry, so this
|
|
321
|
+
// sits above the version lookup and the resolve ladder both.
|
|
322
|
+
const choice = chooseEcosystem({
|
|
323
|
+
cwd: input.cwd,
|
|
324
|
+
requested: input.ecosystem,
|
|
325
|
+
// Read-only, and deliberately so: an ambiguous name is exactly the one that
|
|
326
|
+
// would otherwise be fetched from the wrong registry.
|
|
327
|
+
resolvesLocally: candidate => {
|
|
328
|
+
try {
|
|
329
|
+
candidate.resolve(requested, input.cwd, io);
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
declaresPackage: candidate => candidate.declaredRange(candidate.parentPackage(requested), input.cwd) !== null
|
|
337
|
+
});
|
|
338
|
+
if (!choice.ok) {
|
|
339
|
+
return {
|
|
340
|
+
kind: 'error',
|
|
341
|
+
message: `${choice.message} Use pi-worker-search or pi-worker-fetch for `
|
|
342
|
+
+ `"${input.pkg}" instead.`,
|
|
343
|
+
resolveError: choice.reason === 'ambiguous' ? 'ambiguous_ecosystem' : 'unsupported_ecosystem'
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
// A per-call row for npm, not the static one: `resolvePackage` and
|
|
347
|
+
// `npmVersionLookup` are injection hooks, and a row built from the
|
|
348
|
+
// module-level exports would route straight past whatever a caller injected.
|
|
349
|
+
// They are npm's hooks, so no other row takes them.
|
|
350
|
+
const profile = choice.profile.id === 'npm' ?
|
|
351
|
+
npmProfile({ resolvePackage, npmVersionLookup })
|
|
352
|
+
: choice.profile;
|
|
353
|
+
const registryLabel = profile.registryLabel;
|
|
304
354
|
// Fire the npm registry lookup in parallel with resolve/index/retrieve.
|
|
305
355
|
// It returns null on any failure, so it never blocks the local pipeline.
|
|
306
|
-
const npmVersionPromise =
|
|
307
|
-
|
|
308
|
-
|
|
356
|
+
const npmVersionPromise = profile
|
|
357
|
+
.latest(profile.parentPackage(requested), io)
|
|
358
|
+
.catch(() => null);
|
|
309
359
|
// Step 1: acquire the package — resolve, or install-at-the-declared-range and
|
|
310
360
|
// resolve again. The ladder is `acquirePackage`; this maps its stages onto the
|
|
311
361
|
// rich error results the docs tool reports.
|
|
@@ -313,8 +363,9 @@ export async function docsRaw(input) {
|
|
|
313
363
|
name: requested,
|
|
314
364
|
cwd: input.cwd,
|
|
315
365
|
spawn,
|
|
316
|
-
|
|
317
|
-
|
|
366
|
+
signal: input.signal,
|
|
367
|
+
profile,
|
|
368
|
+
io
|
|
318
369
|
});
|
|
319
370
|
if (!got.ok) {
|
|
320
371
|
if (got.stage === 'install') {
|
|
@@ -324,7 +375,8 @@ export async function docsRaw(input) {
|
|
|
324
375
|
resolveError: 'not_installed',
|
|
325
376
|
installError: got.stderr,
|
|
326
377
|
autoInstallPin: got.pin,
|
|
327
|
-
npmVersion: await npmVersionPromise
|
|
378
|
+
npmVersion: await npmVersionPromise,
|
|
379
|
+
registryLabel
|
|
328
380
|
};
|
|
329
381
|
}
|
|
330
382
|
if (got.stage === 'reresolve') {
|
|
@@ -335,7 +387,8 @@ export async function docsRaw(input) {
|
|
|
335
387
|
resolveError: got.err.kind,
|
|
336
388
|
autoInstalled: true,
|
|
337
389
|
autoInstallPin: got.pin,
|
|
338
|
-
npmVersion: await npmVersionPromise
|
|
390
|
+
npmVersion: await npmVersionPromise,
|
|
391
|
+
registryLabel
|
|
339
392
|
};
|
|
340
393
|
}
|
|
341
394
|
return {
|
|
@@ -343,7 +396,8 @@ export async function docsRaw(input) {
|
|
|
343
396
|
message: `Could not resolve "${input.pkg}" after install: ${got.err instanceof Error ? got.err.message : String(got.err)}`,
|
|
344
397
|
autoInstalled: true,
|
|
345
398
|
autoInstallPin: got.pin,
|
|
346
|
-
npmVersion: await npmVersionPromise
|
|
399
|
+
npmVersion: await npmVersionPromise,
|
|
400
|
+
registryLabel
|
|
347
401
|
};
|
|
348
402
|
}
|
|
349
403
|
if (got.err instanceof ResolveError) {
|
|
@@ -351,13 +405,15 @@ export async function docsRaw(input) {
|
|
|
351
405
|
kind: 'error',
|
|
352
406
|
message: got.err.message,
|
|
353
407
|
resolveError: got.err.kind,
|
|
354
|
-
npmVersion: await npmVersionPromise
|
|
408
|
+
npmVersion: await npmVersionPromise,
|
|
409
|
+
registryLabel
|
|
355
410
|
};
|
|
356
411
|
}
|
|
357
412
|
return {
|
|
358
413
|
kind: 'error',
|
|
359
414
|
message: `Could not resolve "${input.pkg}": ${got.err instanceof Error ? got.err.message : String(got.err)}`,
|
|
360
|
-
npmVersion: await npmVersionPromise
|
|
415
|
+
npmVersion: await npmVersionPromise,
|
|
416
|
+
registryLabel
|
|
361
417
|
};
|
|
362
418
|
}
|
|
363
419
|
let pkg = got.pkg;
|
|
@@ -369,11 +425,13 @@ export async function docsRaw(input) {
|
|
|
369
425
|
// @types/<name> + triple-slash `<reference types>` chain to the package that
|
|
370
426
|
// actually holds the declarations (e.g. bun -> @types/bun -> bun-types).
|
|
371
427
|
// Best-effort: any failure leaves the original resolution untouched.
|
|
372
|
-
const viaTypes = await
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
428
|
+
const viaTypes = await profile.afterResolve?.(pkg, requested, input.cwd, io);
|
|
429
|
+
if (viaTypes) {
|
|
430
|
+
pkg = viaTypes.pkg;
|
|
431
|
+
if (viaTypes.installed) {
|
|
432
|
+
autoInstalled = true;
|
|
433
|
+
autoInstallPin ??= viaTypes.pin;
|
|
434
|
+
}
|
|
377
435
|
}
|
|
378
436
|
// Step 2: open cache
|
|
379
437
|
let cache = null;
|
|
@@ -385,18 +443,19 @@ export async function docsRaw(input) {
|
|
|
385
443
|
cacheError = err instanceof Error ? err.message : String(err);
|
|
386
444
|
}
|
|
387
445
|
const result = cache ?
|
|
388
|
-
docsRawCached(cache, pkg, input.query, ensureIndexed, retrieveChunks, autoInstalled)
|
|
389
|
-
: docsRawUncached(pkg, cacheError ?? 'unknown cache error', autoInstalled);
|
|
446
|
+
docsRawCached(cache, pkg, profile, input.query, ensureIndexed, retrieveChunks, autoInstalled)
|
|
447
|
+
: docsRawUncached(pkg, profile, cacheError ?? 'unknown cache error', autoInstalled);
|
|
390
448
|
result.npmVersion = await npmVersionPromise;
|
|
449
|
+
result.registryLabel = registryLabel;
|
|
391
450
|
if (autoInstallPin)
|
|
392
451
|
result.autoInstallPin = autoInstallPin;
|
|
393
452
|
return result;
|
|
394
453
|
}
|
|
395
|
-
function docsRawCached(cache, pkg, query, ensureIndexed, retrieveChunks, autoInstalled) {
|
|
454
|
+
function docsRawCached(cache, pkg, profile, query, ensureIndexed, retrieveChunks, autoInstalled) {
|
|
396
455
|
let indexResult;
|
|
397
456
|
const t0 = Date.now();
|
|
398
457
|
try {
|
|
399
|
-
indexResult = ensureIndexed(cache, pkg);
|
|
458
|
+
indexResult = ensureIndexed(cache, pkg, profile);
|
|
400
459
|
}
|
|
401
460
|
catch (err) {
|
|
402
461
|
return {
|
|
@@ -407,8 +466,8 @@ function docsRawCached(cache, pkg, query, ensureIndexed, retrieveChunks, autoIns
|
|
|
407
466
|
}
|
|
408
467
|
const indexingMs = indexResult.hitCache ? undefined : Date.now() - t0;
|
|
409
468
|
const chunkCount = cache.db
|
|
410
|
-
.prepare('SELECT count(*) AS c FROM chunks WHERE name = ? AND version = ?')
|
|
411
|
-
.get(pkg.name, pkg.version)?.c ?? 0;
|
|
469
|
+
.prepare('SELECT count(*) AS c FROM chunks WHERE ecosystem = ? AND name = ? AND version = ?')
|
|
470
|
+
.get(profile.id, pkg.name, pkg.version)?.c ?? 0;
|
|
412
471
|
if (chunkCount === 0) {
|
|
413
472
|
return {
|
|
414
473
|
kind: 'no_chunks',
|
|
@@ -421,6 +480,7 @@ function docsRawCached(cache, pkg, query, ensureIndexed, retrieveChunks, autoIns
|
|
|
421
480
|
let chunks;
|
|
422
481
|
try {
|
|
423
482
|
chunks = retrieveChunks(cache, {
|
|
483
|
+
ecosystem: profile.id,
|
|
424
484
|
name: pkg.name,
|
|
425
485
|
version: pkg.version,
|
|
426
486
|
query,
|
|
@@ -453,10 +513,10 @@ function docsRawCached(cache, pkg, query, ensureIndexed, retrieveChunks, autoIns
|
|
|
453
513
|
autoInstalled: autoInstalled ? true : undefined
|
|
454
514
|
};
|
|
455
515
|
}
|
|
456
|
-
function docsRawUncached(pkg, cacheError, autoInstalled) {
|
|
516
|
+
function docsRawUncached(pkg, profile, cacheError, autoInstalled) {
|
|
457
517
|
const parts = [];
|
|
458
|
-
const
|
|
459
|
-
const entryFirst = pkg.
|
|
518
|
+
const surfaceFiles = walkSurfaceAlpha(pkg.root, profile);
|
|
519
|
+
const entryFirst = pkg.entry ? [pkg.entry, ...surfaceFiles.filter(f => f !== pkg.entry)] : surfaceFiles;
|
|
460
520
|
for (const abs of entryFirst) {
|
|
461
521
|
let raw;
|
|
462
522
|
try {
|
|
@@ -466,7 +526,7 @@ function docsRawUncached(pkg, cacheError, autoInstalled) {
|
|
|
466
526
|
continue;
|
|
467
527
|
}
|
|
468
528
|
const rel = path.relative(pkg.root, abs);
|
|
469
|
-
parts.push(
|
|
529
|
+
parts.push(`${profile.commentPrefix} ${rel}\n${profile.surface(raw)}`);
|
|
470
530
|
}
|
|
471
531
|
if (pkg.readme) {
|
|
472
532
|
const rel = path.relative(pkg.root, pkg.readme);
|
|
@@ -502,7 +562,7 @@ function docsRawUncached(pkg, cacheError, autoInstalled) {
|
|
|
502
562
|
autoInstalled: autoInstalled ? true : undefined
|
|
503
563
|
};
|
|
504
564
|
}
|
|
505
|
-
function
|
|
565
|
+
function walkSurfaceAlpha(root, profile) {
|
|
506
566
|
const out = [];
|
|
507
567
|
const stack = [root];
|
|
508
568
|
while (stack.length) {
|
|
@@ -515,12 +575,12 @@ function walkDtsAlpha(root) {
|
|
|
515
575
|
continue;
|
|
516
576
|
}
|
|
517
577
|
for (const entry of entries) {
|
|
518
|
-
if (entry.name
|
|
578
|
+
if (profile.skipDirs.includes(entry.name))
|
|
519
579
|
continue;
|
|
520
580
|
const full = path.join(dir, entry.name);
|
|
521
581
|
if (entry.isDirectory())
|
|
522
582
|
stack.push(full);
|
|
523
|
-
else if (entry.isFile() &&
|
|
583
|
+
else if (entry.isFile() && profile.isSurfaceFile(entry.name))
|
|
524
584
|
out.push(full);
|
|
525
585
|
}
|
|
526
586
|
}
|
|
@@ -538,7 +598,8 @@ export async function docsFocused(input) {
|
|
|
538
598
|
throw new Error(rawResult.message);
|
|
539
599
|
}
|
|
540
600
|
if (rawResult.kind === 'no_chunks') {
|
|
541
|
-
throw new Error(`Package ${rawResult.pkg.name}@${rawResult.pkg.version} has no
|
|
601
|
+
throw new Error(`Package ${rawResult.pkg.name}@${rawResult.pkg.version} has no `
|
|
602
|
+
+ `${ECOSYSTEMS[rawResult.pkg.ecosystem].surfaceLabel}.`);
|
|
542
603
|
}
|
|
543
604
|
const { pkg, chunks, hitCache, indexingMs } = rawResult;
|
|
544
605
|
const r = await docsLookup({
|
|
@@ -581,7 +642,9 @@ export async function docsFocused(input) {
|
|
|
581
642
|
export function buildPrompt(pkg, query, content) {
|
|
582
643
|
return buildExtractionPrompt({
|
|
583
644
|
kind: 'package',
|
|
584
|
-
|
|
645
|
+
// The extracting child is reading Rust or Haskell whenever the row is not
|
|
646
|
+
// npm's, and this is the one sentence it is told about what it has.
|
|
647
|
+
subject: ECOSYSTEMS[pkg.ecosystem].packageSubject,
|
|
585
648
|
tag: 'package',
|
|
586
649
|
identity: `${pkg.name}@${pkg.version}`,
|
|
587
650
|
query,
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* docs-ecosystems — one row per package registry the docs Worker tool can read.
|
|
3
|
+
*
|
|
4
|
+
* The docs pipeline used to be npm all the way down with nothing saying so: it
|
|
5
|
+
* resolved through `node_modules`, chunked TypeScript, and auto-installed any
|
|
6
|
+
* unknown name from npm. Common Rust and Haskell package names also exist on
|
|
7
|
+
* npm, so a question about `aeson` or `tokio` returned a confident answer about
|
|
8
|
+
* an unrelated JavaScript package — a wrong answer, not a miss.
|
|
9
|
+
*
|
|
10
|
+
* A row states the whole of what one registry needs: how to spot its manifest,
|
|
11
|
+
* how to find a package on disk, how to fetch one that is absent, and how to cut
|
|
12
|
+
* its source into retrievable chunks. Rows live in code and arrive as pull
|
|
13
|
+
* requests, so a new ecosystem is reviewable and testable rather than a user
|
|
14
|
+
* string that either works or silently does not.
|
|
15
|
+
*/
|
|
16
|
+
import { type AutoInstallPin } from './docs-core.js';
|
|
17
|
+
import { resolvePackage, type ResolvedPackage } from './docs-resolve.js';
|
|
18
|
+
import { npmVersionLookup, type NpmVersionInfo } from './npm-version.js';
|
|
19
|
+
import { type SpawnFn } from '../shared/child-process.js';
|
|
20
|
+
export type EcosystemId = 'npm' | 'cargo' | 'hackage';
|
|
21
|
+
/**
|
|
22
|
+
* Every filesystem, process and network reach a row is allowed. Rows read no
|
|
23
|
+
* environment and call no global directly, so a test injects a fake registry and
|
|
24
|
+
* a fake extractor instead of needing a real toolchain on the machine.
|
|
25
|
+
*/
|
|
26
|
+
export interface EcosystemIo {
|
|
27
|
+
spawn: SpawnFn;
|
|
28
|
+
fetch: typeof fetch;
|
|
29
|
+
/** Where a package the project does not have is put once fetched. */
|
|
30
|
+
modulesDir: string;
|
|
31
|
+
/** Root of the cargo checkout cache — `CARGO_HOME`, or its default. */
|
|
32
|
+
cargoHome: string;
|
|
33
|
+
/** Every directory cabal may have filed a downloaded tarball under. */
|
|
34
|
+
cabalPackageDirs: readonly string[];
|
|
35
|
+
signal?: AbortSignal | undefined;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Production defaults for {@link EcosystemIo}. The environment is read HERE and
|
|
39
|
+
* nowhere in a row, so a test injects a directory instead of setting a variable
|
|
40
|
+
* that outlives it.
|
|
41
|
+
*/
|
|
42
|
+
export declare function defaultEcosystemIo(overrides?: Partial<EcosystemIo>): EcosystemIo;
|
|
43
|
+
export interface AcquireResult {
|
|
44
|
+
success: boolean;
|
|
45
|
+
installDir: string;
|
|
46
|
+
stderr: string;
|
|
47
|
+
}
|
|
48
|
+
export interface EcosystemProfile {
|
|
49
|
+
id: EcosystemId;
|
|
50
|
+
/** What this row covers, and what it deliberately does not. */
|
|
51
|
+
why: string;
|
|
52
|
+
/** The registry's name, as it appears in text the model reads. */
|
|
53
|
+
registryLabel: string;
|
|
54
|
+
/** The manifest file this row is detected by, named for a refusal message. */
|
|
55
|
+
manifestLabel: string;
|
|
56
|
+
/** True when `cwd` looks like a project of this ecosystem. */
|
|
57
|
+
detect: (cwd: string) => boolean;
|
|
58
|
+
isValidName: (name: string) => boolean;
|
|
59
|
+
/** The installable package a possibly-subpath specifier belongs to. */
|
|
60
|
+
parentPackage: (name: string) => string;
|
|
61
|
+
/** Find the package on disk. Throws `ResolveError` when it is absent. */
|
|
62
|
+
resolve: (name: string, cwd: string, io: EcosystemIo) => ResolvedPackage;
|
|
63
|
+
/** The version range the project pins this package to, if it pins one. */
|
|
64
|
+
declaredRange: (name: string, cwd: string) => string | null;
|
|
65
|
+
/** Fetch a package that is not on disk into `io.modulesDir`. */
|
|
66
|
+
acquire: (name: string, range: string | null, io: EcosystemIo) => Promise<AcquireResult>;
|
|
67
|
+
/**
|
|
68
|
+
* A second resolution hop, for ecosystems where the package that ships the
|
|
69
|
+
* documented surface is not the one that was asked for.
|
|
70
|
+
*/
|
|
71
|
+
afterResolve?: (pkg: ResolvedPackage, requested: string, cwd: string, io: EcosystemIo) => Promise<{
|
|
72
|
+
pkg: ResolvedPackage;
|
|
73
|
+
installed: boolean;
|
|
74
|
+
pin?: AutoInstallPin;
|
|
75
|
+
}>;
|
|
76
|
+
/** The registry's own newest version, for grounding an answer in the present. */
|
|
77
|
+
latest: (name: string, io: EcosystemIo) => Promise<NpmVersionInfo | null>;
|
|
78
|
+
/** True for a file that carries the package's public API surface. */
|
|
79
|
+
isSurfaceFile: (name: string) => boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Reduce a source file to its public surface. Identity where the ecosystem
|
|
82
|
+
* already ships a declarations-only file, as npm does with `.d.ts`.
|
|
83
|
+
*/
|
|
84
|
+
surface: (content: string) => string;
|
|
85
|
+
/** Where a declaration begins, so a chunk never splits a signature. */
|
|
86
|
+
declSplitRe: RegExp;
|
|
87
|
+
/** Line-comment marker, used to label a chunk with the file it came from. */
|
|
88
|
+
commentPrefix: string;
|
|
89
|
+
/** Directories the surface walk never descends into: tests, build output. */
|
|
90
|
+
skipDirs: readonly string[];
|
|
91
|
+
/** What this ecosystem's packages ship, for a "there is nothing to read" answer. */
|
|
92
|
+
surfaceLabel: string;
|
|
93
|
+
/**
|
|
94
|
+
* How the extraction child is told what it is reading. A written phrase, not
|
|
95
|
+
* a label plus an article: "a npm package" is what deriving one gives you.
|
|
96
|
+
*/
|
|
97
|
+
packageSubject: string;
|
|
98
|
+
/** Which of the PROJECT's own files this ecosystem contributes to its index. */
|
|
99
|
+
projectGlobs: readonly string[];
|
|
100
|
+
/** The project's own name from its manifest, or null when it declares none. */
|
|
101
|
+
projectName: (cwd: string) => string | null;
|
|
102
|
+
/**
|
|
103
|
+
* What the project's manifest declares, name to version. Undefined when the
|
|
104
|
+
* manifest is missing or unreadable — the caller then cannot prove any
|
|
105
|
+
* package's version, which is a different fact from "declares nothing".
|
|
106
|
+
*/
|
|
107
|
+
declaredDeps: (cwd: string) => Record<string, string> | undefined;
|
|
108
|
+
}
|
|
109
|
+
/** Overrides a caller has already been given its own copies of. */
|
|
110
|
+
export interface NpmProfileHooks {
|
|
111
|
+
resolvePackage?: typeof resolvePackage;
|
|
112
|
+
npmVersionLookup?: typeof npmVersionLookup;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The npm row, with the pieces a caller may have replaced left as parameters.
|
|
116
|
+
*
|
|
117
|
+
* `docsRaw` already takes `resolvePackage` and `npmVersionLookup` as injection
|
|
118
|
+
* hooks, and those hooks must keep reaching the resolution they are injected
|
|
119
|
+
* for. A per-call row carries them; {@link ECOSYSTEMS} holds the plain one.
|
|
120
|
+
*/
|
|
121
|
+
export declare function npmProfile(hooks?: NpmProfileHooks): EcosystemProfile;
|
|
122
|
+
/**
|
|
123
|
+
* The package.json manifest, not the lockfile: a lockfile is rewritten by
|
|
124
|
+
* installs that change no resolved version, and pruning digests on that would
|
|
125
|
+
* cost reuse for no correctness gain.
|
|
126
|
+
*/
|
|
127
|
+
export declare function npmDeclaredDeps(cwd: string): Record<string, string> | undefined;
|
|
128
|
+
/** A project's own name from its package.json, or null when it declares none. */
|
|
129
|
+
export declare function npmProjectName(cwd: string): string | null;
|
|
130
|
+
export declare const ECOSYSTEMS: {
|
|
131
|
+
readonly npm: EcosystemProfile;
|
|
132
|
+
readonly cargo: EcosystemProfile;
|
|
133
|
+
readonly hackage: EcosystemProfile;
|
|
134
|
+
};
|
|
135
|
+
/** Which ecosystems `cwd` looks like a project of, in roster order. */
|
|
136
|
+
export declare function detectEcosystems(cwd: string, roster?: readonly EcosystemProfile[]): EcosystemId[];
|
|
137
|
+
export type EcosystemChoice = {
|
|
138
|
+
ok: true;
|
|
139
|
+
profile: EcosystemProfile;
|
|
140
|
+
detected: EcosystemId[];
|
|
141
|
+
} | {
|
|
142
|
+
ok: false;
|
|
143
|
+
reason: 'none' | 'ambiguous' | 'not_detected';
|
|
144
|
+
detected: EcosystemId[];
|
|
145
|
+
message: string;
|
|
146
|
+
};
|
|
147
|
+
export interface ChooseEcosystemInput {
|
|
148
|
+
cwd: string;
|
|
149
|
+
/** An explicit ecosystem, for a repo that holds more than one manifest. */
|
|
150
|
+
requested?: EcosystemId;
|
|
151
|
+
/**
|
|
152
|
+
* Does this row already have the package on disk? The tie-break for a
|
|
153
|
+
* polyglot repo, and it must not install: an ambiguous name is exactly the
|
|
154
|
+
* one that would be installed from the wrong registry.
|
|
155
|
+
*/
|
|
156
|
+
resolvesLocally?: (profile: EcosystemProfile) => boolean;
|
|
157
|
+
/**
|
|
158
|
+
* Does this row's MANIFEST name the package? Stronger evidence than a copy
|
|
159
|
+
* on disk, and it outranks it below.
|
|
160
|
+
*/
|
|
161
|
+
declaresPackage?: (profile: EcosystemProfile) => boolean;
|
|
162
|
+
roster?: readonly EcosystemProfile[];
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Which ecosystem a lookup belongs to. The MANIFEST decides, never the model:
|
|
166
|
+
* `text`, `base`, `aeson`, `tokio` and `clap` are all real npm packages as well
|
|
167
|
+
* as Haskell or Rust ones, so a name alone cannot say which registry was meant,
|
|
168
|
+
* and guessing npm returns a confident answer about the wrong package.
|
|
169
|
+
*
|
|
170
|
+
* A refusal is a result, not a failure: the caller reports it and installs
|
|
171
|
+
* nothing.
|
|
172
|
+
*/
|
|
173
|
+
export declare function chooseEcosystem(input: ChooseEcosystemInput): EcosystemChoice;
|