@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.
Files changed (45) hide show
  1. package/README.md +20 -5
  2. package/dist/task/auto-orchestrator.d.ts +36 -0
  3. package/dist/task/auto-orchestrator.js +43 -6
  4. package/dist/task/cancel-points.d.ts +34 -6
  5. package/dist/task/cancel-points.js +62 -10
  6. package/dist/task/child-status.js +13 -1
  7. package/dist/task/context-attribution.js +18 -6
  8. package/dist/task/external-context.d.ts +8 -1
  9. package/dist/task/external-context.js +52 -4
  10. package/dist/task/orchestrator.js +47 -5
  11. package/dist/task/phases.js +2 -1
  12. package/dist/task/plan-orchestrator.js +10 -1
  13. package/dist/task/prompts.d.ts +7 -1
  14. package/dist/task/prompts.js +14 -4
  15. package/dist/task/research-worker.js +11 -0
  16. package/dist/task/run-bracket.js +13 -1
  17. package/dist/task/task-gates.js +34 -0
  18. package/dist/workers/docs-cache.js +50 -3
  19. package/dist/workers/docs-chunk.d.ts +6 -3
  20. package/dist/workers/docs-chunk.js +8 -5
  21. package/dist/workers/docs-core.d.ts +27 -3
  22. package/dist/workers/docs-core.js +104 -41
  23. package/dist/workers/docs-ecosystems.d.ts +173 -0
  24. package/dist/workers/docs-ecosystems.js +449 -0
  25. package/dist/workers/docs-index.d.ts +2 -1
  26. package/dist/workers/docs-index.js +55 -27
  27. package/dist/workers/docs-project.d.ts +10 -0
  28. package/dist/workers/docs-project.js +86 -24
  29. package/dist/workers/docs-resolve.d.ts +6 -1
  30. package/dist/workers/docs-resolve.js +4 -3
  31. package/dist/workers/docs-retrieve.d.ts +2 -0
  32. package/dist/workers/docs-retrieve.js +11 -11
  33. package/dist/workers/eco-cargo.d.ts +115 -0
  34. package/dist/workers/eco-cargo.js +793 -0
  35. package/dist/workers/eco-hackage.d.ts +93 -0
  36. package/dist/workers/eco-hackage.js +508 -0
  37. package/dist/workers/npm-version.d.ts +5 -3
  38. package/dist/workers/npm-version.js +6 -4
  39. package/dist/workers/pi-worker-docs.d.ts +18 -4
  40. package/dist/workers/pi-worker-docs.js +57 -19
  41. package/dist/workers/research-cache.d.ts +2 -13
  42. package/dist/workers/research-cache.js +22 -46
  43. package/dist/workers/shared.d.ts +16 -5
  44. package/dist/workers/shared.js +0 -0
  45. 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 { resolvePackage as defaultResolvePackage, ResolveError, isDtsFile, resolveTypeSource, typesPackageName, splitRuntimeNamespace } from './docs-resolve.js';
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: resolvePackage(name, cwd), autoInstalled: false };
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 = extractParentPackage(name);
242
- const declaredRange = findDeclaredRange(asked, cwd);
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 runAutoInstall(spawn, asked, {
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: resolvePackage(name, install.installDir),
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 = npmVersionLookup(extractParentPackage(requested), {
307
- signal: input.signal
308
- }).catch(() => null);
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
- resolvePackage,
317
- signal: input.signal
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 resolveTypeSourceForDocs(pkg, requested, input.cwd, spawn, resolvePackage, input.signal);
373
- pkg = viaTypes.pkg;
374
- if (viaTypes.installed) {
375
- autoInstalled = true;
376
- autoInstallPin ??= viaTypes.pin;
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 dtsFiles = walkDtsAlpha(pkg.root);
459
- const entryFirst = pkg.entryDts ? [pkg.entryDts, ...dtsFiles.filter(f => f !== pkg.entryDts)] : dtsFiles;
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(`// ${rel}\n${raw}`);
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 walkDtsAlpha(root) {
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 === 'node_modules')
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() && isDtsFile(entry.name))
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 .d.ts files or README.`);
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
- subject: 'an npm package',
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;