@mjasnikovs/pi-task 0.38.10 → 0.38.12

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 (78) hide show
  1. package/README.md +7 -3
  2. package/dist/shared/child-process.d.ts +8 -0
  3. package/dist/shared/command-watchdog.d.ts +1 -1
  4. package/dist/shared/command-watchdog.js +1 -1
  5. package/dist/task/accept-debt.d.ts +47 -0
  6. package/dist/task/accept-debt.js +127 -28
  7. package/dist/task/auto-orchestrator.js +93 -116
  8. package/dist/task/boot-probe.d.ts +298 -0
  9. package/dist/task/boot-probe.js +806 -0
  10. package/dist/task/child-runner.d.ts +56 -25
  11. package/dist/task/child-runner.js +65 -31
  12. package/dist/task/child-status.d.ts +95 -0
  13. package/dist/task/child-status.js +99 -0
  14. package/dist/task/command-run.d.ts +36 -0
  15. package/dist/task/command-run.js +48 -1
  16. package/dist/task/command-watchdog.js +1 -1
  17. package/dist/task/context-usage.d.ts +4 -3
  18. package/dist/task/context-usage.js +4 -3
  19. package/dist/task/contracts.js +18 -35
  20. package/dist/task/deep-render-check.d.ts +47 -0
  21. package/dist/task/deep-render-check.js +110 -65
  22. package/dist/task/env-notes.d.ts +3 -3
  23. package/dist/task/env-notes.js +24 -35
  24. package/dist/task/final-gate-fix.d.ts +1 -1
  25. package/dist/task/final-gate-fix.js +1 -1
  26. package/dist/task/final-gate.d.ts +38 -432
  27. package/dist/task/final-gate.js +105 -1213
  28. package/dist/task/gate-child.d.ts +8 -10
  29. package/dist/task/gate-child.js +15 -19
  30. package/dist/task/gate-deps.d.ts +29 -0
  31. package/dist/task/gate-deps.js +192 -206
  32. package/dist/task/gate-tally.d.ts +189 -0
  33. package/dist/task/gate-tally.js +249 -0
  34. package/dist/task/implementation-turn.d.ts +201 -0
  35. package/dist/task/implementation-turn.js +263 -0
  36. package/dist/task/launch-contract.js +27 -43
  37. package/dist/task/launch-manifest.d.ts +5 -0
  38. package/dist/task/launch-manifest.js +21 -0
  39. package/dist/task/ledger.d.ts +38 -0
  40. package/dist/task/ledger.js +83 -0
  41. package/dist/task/loop-detector.d.ts +14 -8
  42. package/dist/task/loop-detector.js +36 -12
  43. package/dist/task/orchestrator.d.ts +61 -126
  44. package/dist/task/orchestrator.js +70 -297
  45. package/dist/task/phases.d.ts +18 -0
  46. package/dist/task/phases.js +4 -3
  47. package/dist/task/plan-orchestrator.js +34 -33
  48. package/dist/task/requirements.d.ts +1 -1
  49. package/dist/task/requirements.js +50 -66
  50. package/dist/task/root-cause-repair.js +20 -32
  51. package/dist/task/run-bracket.d.ts +75 -0
  52. package/dist/task/run-bracket.js +41 -0
  53. package/dist/task/stall-detector.d.ts +110 -0
  54. package/dist/task/stall-detector.js +159 -0
  55. package/dist/task/task-gates.d.ts +69 -0
  56. package/dist/task/task-gates.js +114 -90
  57. package/dist/task/verify-work.d.ts +53 -67
  58. package/dist/task/verify-work.js +15 -11
  59. package/dist/workers/docs-core.d.ts +0 -4
  60. package/dist/workers/docs-core.js +10 -34
  61. package/dist/workers/docs-project.js +3 -3
  62. package/dist/workers/docs-resolve.d.ts +18 -0
  63. package/dist/workers/docs-resolve.js +39 -0
  64. package/dist/workers/docs-retrieve.d.ts +13 -0
  65. package/dist/workers/docs-retrieve.js +17 -2
  66. package/dist/workers/fetch-core.d.ts +0 -4
  67. package/dist/workers/fetch-core.js +2 -5
  68. package/dist/workers/phantom-imports.d.ts +3 -3
  69. package/dist/workers/phantom-imports.js +16 -29
  70. package/dist/workers/pi-worker-docs.d.ts +49 -0
  71. package/dist/workers/pi-worker-docs.js +33 -9
  72. package/dist/workers/pi-worker-fetch.d.ts +18 -0
  73. package/dist/workers/pi-worker-fetch.js +19 -4
  74. package/dist/workers/single-read-extension.d.ts +1 -1
  75. package/dist/workers/single-read-extension.js +5 -4
  76. package/dist/workers/single-read-guard.d.ts +32 -10
  77. package/dist/workers/single-read-guard.js +67 -16
  78. package/package.json +1 -1
@@ -122,15 +122,21 @@ export function extractSpecForVerification(taskBody) {
122
122
  */
123
123
  function probeAdapter(row) {
124
124
  const { key, stage, block, ruleId, rule } = row;
125
+ const source = row.source
126
+ ?? ((deps) =>
127
+ // One channel per key, so the bag's entry for this key IS this row's
128
+ // thunk; the cast only re-narrows what the mapped type already says.
129
+ deps.probes?.[key]);
125
130
  return {
126
131
  key,
132
+ bound: !row.source,
127
133
  stage,
128
134
  block,
129
135
  ruleId,
130
136
  rule,
131
137
  run: async (deps, onStage) => {
132
- const probe = row.dep(deps);
133
- // Probes are INDEPENDENTLY OPTIONAL: an absent dep is "skipped", and a
138
+ const probe = source(deps);
139
+ // Probes are INDEPENDENTLY OPTIONAL: an absent probe is "skipped", and a
134
140
  // probe that throws degrades to its own empty value — it is a sharpener,
135
141
  // never a blocker, so a fault in one can neither block the gate nor
136
142
  // leak into another row.
@@ -165,7 +171,6 @@ const PROBE_ADAPTERS = [
165
171
  probeAdapter({
166
172
  key: 'substitution',
167
173
  stage: 'substitution probe',
168
- dep: deps => deps.probe,
169
174
  empty: [],
170
175
  findings: asLines,
171
176
  ruleId: '3b',
@@ -198,7 +203,6 @@ const PROBE_ADAPTERS = [
198
203
  probeAdapter({
199
204
  key: 'prohibition',
200
205
  stage: 'prohibition probe',
201
- dep: deps => deps.prohibitionProbe,
202
206
  empty: [],
203
207
  findings: asLines,
204
208
  ruleId: '4b',
@@ -238,7 +242,6 @@ const PROBE_ADAPTERS = [
238
242
  probeAdapter({
239
243
  key: 'crossTaskDeletion',
240
244
  stage: 'cross-task deletion probe',
241
- dep: deps => deps.crossTaskDeletionProbe,
242
245
  empty: [],
243
246
  findings: crossTaskDeletionVerifyFindings,
244
247
  ruleId: '4d',
@@ -277,7 +280,6 @@ const PROBE_ADAPTERS = [
277
280
  probeAdapter({
278
281
  key: 'probeGaming',
279
282
  stage: 'probe-gaming probe',
280
- dep: deps => deps.probeGamingProbe,
281
283
  empty: [],
282
284
  findings: asLines,
283
285
  ruleId: '4c',
@@ -322,7 +324,7 @@ const PROBE_ADAPTERS = [
322
324
  */
323
325
  probeAdapter({
324
326
  key: 'skipEscape',
325
- dep: deps => () => Promise.resolve(skipEscapeVerifyFindings(findSkipEscapes(deps.spec ?? ''))),
327
+ source: deps => () => Promise.resolve(skipEscapeVerifyFindings(findSkipEscapes(deps.spec ?? ''))),
326
328
  empty: [],
327
329
  findings: asLines,
328
330
  ruleId: '5c',
@@ -349,7 +351,6 @@ const PROBE_ADAPTERS = [
349
351
  probeAdapter({
350
352
  key: 'foreignPath',
351
353
  stage: 'foreign-path probe',
352
- dep: deps => deps.foreignPathProbe,
353
354
  empty: [],
354
355
  findings: asLines,
355
356
  ruleId: '4e',
@@ -392,7 +393,6 @@ const PROBE_ADAPTERS = [
392
393
  probeAdapter({
393
394
  key: 'scriptEscape',
394
395
  stage: 'script-escape probe',
395
- dep: deps => deps.scriptEscapeProbe,
396
396
  empty: [],
397
397
  findings: asLines,
398
398
  ruleId: '4f',
@@ -431,7 +431,6 @@ const PROBE_ADAPTERS = [
431
431
  probeAdapter({
432
432
  key: 'runnerGlob',
433
433
  stage: 'runner-glob probe',
434
- dep: deps => deps.runnerGlobProbe,
435
434
  empty: [],
436
435
  findings: asLines,
437
436
  ruleId: '4g',
@@ -466,7 +465,6 @@ const PROBE_ADAPTERS = [
466
465
  probeAdapter({
467
466
  key: 'testAssembly',
468
467
  stage: 'test-assembly probe',
469
- dep: deps => deps.testAssemblyProbe,
470
468
  empty: [],
471
469
  findings: asLines,
472
470
  ruleId: '3f',
@@ -487,6 +485,12 @@ const PROBE_ADAPTERS = [
487
485
  ]
488
486
  })
489
487
  ];
488
+ /**
489
+ * The channels a caller must bind — the table rows that read `deps.probes`, in
490
+ * table order. Exported so the one binder (gate-deps' `buildVerifyProbes`) can be
491
+ * checked against the table rather than against a hand-kept list.
492
+ */
493
+ export const BOUND_PROBE_KEYS = PROBE_ADAPTERS.filter(a => a.bound).map(a => a.key);
490
494
  /**
491
495
  * Build the verification child's prompt. Kept pure so the wording is unit-tested
492
496
  * without spawning pi. The contract: run the spec's own verification in the real
@@ -183,9 +183,5 @@ export declare function buildPrompt(pkg: ResolvedPackage, query: string, content
183
183
  * (`{name, version: 'local', root, entryDts: null, readme: null}`) purely to make
184
184
  * this call compile, with three of the five fields existing only for that.
185
185
  */
186
- export declare function formatResultText(header: string, parsed: {
187
- answer: string;
188
- excerpt?: string;
189
- }, verified: boolean | undefined): string;
190
186
  /** The header for an npm package answer. */
191
187
  export declare function packageHeader(pkg: ResolvedPackage): string;
@@ -4,15 +4,14 @@ 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, detectTypesRedirect, typesPackageName, hasTypeFiles, isDtsFile, splitRuntimeNamespace } from './docs-resolve.js';
8
- import { retrieveChunks as defaultRetrieveChunks } from './docs-retrieve.js';
7
+ import { resolvePackage as defaultResolvePackage, ResolveError, isDtsFile, resolveTypeSource, typesPackageName, splitRuntimeNamespace } from './docs-resolve.js';
8
+ import { retrieveChunks as defaultRetrieveChunks, PACKAGE_RETRIEVE_LIMIT, RETRIEVE_CONTENT_BUDGET } from './docs-retrieve.js';
9
9
  import { npmVersionLookup as defaultNpmVersionLookup } from './npm-version.js';
10
10
  import { runChild } from '../shared/child-process.js';
11
11
  import { runFocusedExtraction } from './focused-extractor.js';
12
12
  import { buildExtractionPrompt } from './abstention.js';
13
- import { formatResultText as formatResultTextShared } from '../shared/child-output.js';
14
- const DEFAULT_LIMIT = 8;
15
- const DEFAULT_BUDGET = 24_000;
13
+ const DEFAULT_LIMIT = PACKAGE_RETRIEVE_LIMIT;
14
+ const DEFAULT_BUDGET = RETRIEVE_CONTENT_BUDGET;
16
15
  const NO_CACHE_HEAD = 25_000;
17
16
  const NO_CACHE_TAIL = 5_000;
18
17
  const NO_CACHE_TOTAL = NO_CACHE_HEAD + NO_CACHE_TAIL;
@@ -224,31 +223,11 @@ async function tryResolveOrInstall(name, cwd, spawn, resolvePackage, signal) {
224
223
  }
225
224
  }
226
225
  }
227
- /** Follow the @types/<name> + triple-slash `<reference types>` redirect chain
228
- * from a package that ships no usable types of its own to the one that actually
229
- * holds the declarations (e.g. bun -> @types/bun -> bun-types). Bounded to a few
230
- * hops; returns the original package if no better source is found. */
231
- async function resolveTypeSource(pkg, requested, cwd, spawn, resolvePackage, signal) {
232
- const visited = new Set([pkg.name, extractParentPackage(requested)]);
233
- let cur = pkg;
234
- for (let depth = 0; depth < 3; depth++) {
235
- let next = detectTypesRedirect(cur);
236
- if (next && visited.has(next))
237
- next = null;
238
- if (!next && !hasTypeFiles(cur.root)) {
239
- const types = typesPackageName(cur.name);
240
- if (types && !visited.has(types))
241
- next = types;
242
- }
243
- if (!next)
244
- break;
245
- visited.add(next);
246
- const resolved = await tryResolveOrInstall(next, cwd, spawn, resolvePackage, signal);
247
- if (!resolved)
248
- break;
249
- cur = resolved;
250
- }
251
- return cur;
226
+ /** The docs pipeline's adapter over the shared redirect walk (docs-resolve.ts):
227
+ * hops resolve through the auto-installing lookup, so a declaration package that is
228
+ * declared but not yet on disk is fetched rather than abandoned. */
229
+ function resolveTypeSourceForDocs(pkg, requested, cwd, spawn, resolvePackage, signal) {
230
+ return resolveTypeSource(pkg, extractParentPackage(requested), next => tryResolveOrInstall(next, cwd, spawn, resolvePackage, signal));
252
231
  }
253
232
  export async function docsRaw(input) {
254
233
  const resolvePackage = input.resolvePackage ?? defaultResolvePackage;
@@ -344,7 +323,7 @@ export async function docsRaw(input) {
344
323
  // @types/<name> + triple-slash `<reference types>` chain to the package that
345
324
  // actually holds the declarations (e.g. bun -> @types/bun -> bun-types).
346
325
  // Best-effort: any failure leaves the original resolution untouched.
347
- pkg = await resolveTypeSource(pkg, requested, input.cwd, spawn, resolvePackage, input.signal);
326
+ pkg = await resolveTypeSourceForDocs(pkg, requested, input.cwd, spawn, resolvePackage, input.signal);
348
327
  // Step 2: open cache
349
328
  let cache = null;
350
329
  let cacheError;
@@ -569,9 +548,6 @@ export function buildPrompt(pkg, query, content) {
569
548
  * (`{name, version: 'local', root, entryDts: null, readme: null}`) purely to make
570
549
  * this call compile, with three of the five fields existing only for that.
571
550
  */
572
- export function formatResultText(header, parsed, verified) {
573
- return formatResultTextShared(header, parsed, verified);
574
- }
575
551
  /** The header for an npm package answer. */
576
552
  export function packageHeader(pkg) {
577
553
  return `Per ${pkg.name}@${pkg.version}:`;
@@ -2,11 +2,11 @@ import { createHash } from 'node:crypto';
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import * as fs from 'node:fs';
4
4
  import * as path from 'node:path';
5
- import { retrieveChunks as defaultRetrieveChunks } from './docs-retrieve.js';
5
+ import { retrieveChunks as defaultRetrieveChunks, PROJECT_RETRIEVE_LIMIT, RETRIEVE_CONTENT_BUDGET } from './docs-retrieve.js';
6
6
  import { buildExtractionPrompt } from './abstention.js';
7
7
  import { chunkDeclarations } from './docs-chunk.js';
8
- const DEFAULT_LIMIT = 50;
9
- const DEFAULT_BUDGET = 24_000;
8
+ const DEFAULT_LIMIT = PROJECT_RETRIEVE_LIMIT;
9
+ const DEFAULT_BUDGET = RETRIEVE_CONTENT_BUDGET;
10
10
  export function getProjectName(cwd) {
11
11
  try {
12
12
  const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
@@ -54,3 +54,21 @@ export declare function countEntryDeclarations(content: string): number;
54
54
  * local `/// <reference path=... />` aggregator entry, or an entry file that
55
55
  * declares anything of its own). */
56
56
  export declare function detectTypesRedirect(pkg: ResolvedPackage): string | null;
57
+ /**
58
+ * Follow the `@types/<name>` + triple-slash `<reference types>` redirect chain from
59
+ * a package that ships no usable types of its own to the one that actually holds
60
+ * the declarations — `bun` → `@types/bun` → `bun-types`. Bounded to three hops;
61
+ * returns the package it started from when no better source is found.
62
+ *
63
+ * This is what the four predicates above EXIST for. They were exported and heavily
64
+ * tested (35 references between them) while the loop that calls them lived in two
65
+ * byte-identical copies — `docs-core.ts` and `phantom-imports.ts` — and NEITHER was
66
+ * covered: both of their tests pin only the zero-hop case, so the multi-hop
67
+ * behaviour cited by name in five doc comments was asserted nowhere.
68
+ *
69
+ * `resolveHop` is the one thing the two call sites genuinely disagree about: the
70
+ * docs pipeline resolves the next hop through an auto-installing async lookup, the
71
+ * phantom-import checker through a bare sync resolve that must never install. It
72
+ * returns null to stop the walk.
73
+ */
74
+ export declare function resolveTypeSource(start: ResolvedPackage, seed: string, resolveHop: (name: string) => Promise<ResolvedPackage | null>): Promise<ResolvedPackage>;
@@ -304,3 +304,42 @@ export function detectTypesRedirect(pkg) {
304
304
  return null;
305
305
  return target;
306
306
  }
307
+ /**
308
+ * Follow the `@types/<name>` + triple-slash `<reference types>` redirect chain from
309
+ * a package that ships no usable types of its own to the one that actually holds
310
+ * the declarations — `bun` → `@types/bun` → `bun-types`. Bounded to three hops;
311
+ * returns the package it started from when no better source is found.
312
+ *
313
+ * This is what the four predicates above EXIST for. They were exported and heavily
314
+ * tested (35 references between them) while the loop that calls them lived in two
315
+ * byte-identical copies — `docs-core.ts` and `phantom-imports.ts` — and NEITHER was
316
+ * covered: both of their tests pin only the zero-hop case, so the multi-hop
317
+ * behaviour cited by name in five doc comments was asserted nowhere.
318
+ *
319
+ * `resolveHop` is the one thing the two call sites genuinely disagree about: the
320
+ * docs pipeline resolves the next hop through an auto-installing async lookup, the
321
+ * phantom-import checker through a bare sync resolve that must never install. It
322
+ * returns null to stop the walk.
323
+ */
324
+ export async function resolveTypeSource(start, seed, resolveHop) {
325
+ const visited = new Set([start.name, seed]);
326
+ let cur = start;
327
+ for (let hop = 0; hop < 3; hop++) {
328
+ let next = detectTypesRedirect(cur);
329
+ if (next && visited.has(next))
330
+ next = null;
331
+ if (!next && !hasTypeFiles(cur.root)) {
332
+ const types = typesPackageName(cur.name);
333
+ if (types && !visited.has(types))
334
+ next = types;
335
+ }
336
+ if (!next)
337
+ break;
338
+ visited.add(next);
339
+ const resolved = await resolveHop(next);
340
+ if (!resolved)
341
+ break;
342
+ cur = resolved;
343
+ }
344
+ return cur;
345
+ }
@@ -12,4 +12,17 @@ export interface RetrieveOptions {
12
12
  limit?: number;
13
13
  contentBudget?: number;
14
14
  }
15
+ /**
16
+ * How many chunks a retrieval returns, per corpus.
17
+ *
18
+ * The two values differ and always have; this is the first place they sit side by
19
+ * side, and no comment in the history explains WHY an npm package gets 8 and
20
+ * project source gets 50. Recorded as-is rather than harmonised: changing either is
21
+ * a retrieval-policy change with its own A/B, not a tidy-up. Before this they were
22
+ * three declarations across three files, so the divergence was invisible.
23
+ */
24
+ export declare const PACKAGE_RETRIEVE_LIMIT = 8;
25
+ export declare const PROJECT_RETRIEVE_LIMIT = 50;
26
+ /** Character budget for the assembled chunk text. The same for both corpora. */
27
+ export declare const RETRIEVE_CONTENT_BUDGET = 24000;
15
28
  export declare function retrieveChunks(cache: CacheHandle, opts: RetrieveOptions): RetrievedChunk[];
@@ -1,5 +1,20 @@
1
- const DEFAULT_LIMIT = 50;
2
- const DEFAULT_BUDGET = 24_000;
1
+ /**
2
+ * How many chunks a retrieval returns, per corpus.
3
+ *
4
+ * The two values differ and always have; this is the first place they sit side by
5
+ * side, and no comment in the history explains WHY an npm package gets 8 and
6
+ * project source gets 50. Recorded as-is rather than harmonised: changing either is
7
+ * a retrieval-policy change with its own A/B, not a tidy-up. Before this they were
8
+ * three declarations across three files, so the divergence was invisible.
9
+ */
10
+ export const PACKAGE_RETRIEVE_LIMIT = 8;
11
+ export const PROJECT_RETRIEVE_LIMIT = 50;
12
+ /** Character budget for the assembled chunk text. The same for both corpora. */
13
+ export const RETRIEVE_CONTENT_BUDGET = 24_000;
14
+ // Both callers always pass `limit`/`contentBudget` explicitly, so these defaults are
15
+ // only a backstop for a third caller that does not.
16
+ const DEFAULT_LIMIT = PROJECT_RETRIEVE_LIMIT;
17
+ const DEFAULT_BUDGET = RETRIEVE_CONTENT_BUDGET;
3
18
  const MIN_TOKEN_LEN = 2;
4
19
  const FALLBACK_DTS_CHARS = 12_000;
5
20
  const FALLBACK_README_CHARS = 4_000;
@@ -104,7 +104,3 @@ export interface SelectedContent {
104
104
  export declare function selectContent(markdown: string, requestedUrl: string): SelectedContent;
105
105
  /** The shipped strategy: fragment-aware selection + the recalibrated prompt. */
106
106
  export declare const shippedStrategy: PromptStrategy;
107
- export declare function formatResultText(parsed: {
108
- answer: string;
109
- excerpt?: string;
110
- }, verified: boolean | undefined): string;
@@ -1,7 +1,6 @@
1
1
  import { fetchAndClean as defaultFetchAndClean } from './html-clean.js';
2
2
  import { runFocusedExtraction } from './focused-extractor.js';
3
3
  import { abstentionSentence } from './abstention.js';
4
- import { formatResultText as formatResultTextShared } from '../shared/child-output.js';
5
4
  const CONTENT_BUDGET = 30_000;
6
5
  const HEAD_CHARS = 25_000;
7
6
  const TAIL_CHARS = 5_000;
@@ -243,7 +242,5 @@ function buildPrompt(args) {
243
242
  }
244
243
  /** The shipped strategy: fragment-aware selection + the recalibrated prompt. */
245
244
  export const shippedStrategy = { selectContent, buildPrompt };
246
- // ─── Thin wrapper: fetch-core formatResultText (no header) ───────────────────
247
- export function formatResultText(parsed, verified) {
248
- return formatResultTextShared('', parsed, verified);
249
- }
245
+ // A fetch answer carries no package header, so the shared formatter is bound with an
246
+ // empty one at the single call site rather than behind a wrapper of its own.
@@ -26,14 +26,14 @@ export interface RuntimeImportVerdict {
26
26
  */
27
27
  export declare function classifyRuntimeImport(spec: string, runtime: string, sub: string, typeText: string): RuntimeImportVerdict;
28
28
  /** Default loader: resolve the runtime's installed types and read their .d.ts. */
29
- export declare function loadRuntimeTypeText(runtime: string, cwd: string): string | null;
29
+ export declare function loadRuntimeTypeText(runtime: string, cwd: string): Promise<string | null>;
30
30
  /**
31
31
  * Scan `text` for runtime-namespace specifiers and return the ones the installed
32
32
  * types do not declare. `loadText` is injectable for tests; in production it reads
33
33
  * the runtime's type package. A runtime whose types can't be loaded is skipped
34
34
  * (we never flag what we can't verify), so this is silent when types are absent.
35
35
  */
36
- export declare function findPhantomImports(text: string, cwd: string, loadText?: (runtime: string, cwd: string) => string | null): PhantomImport[];
36
+ export declare function findPhantomImports(text: string, cwd: string, loadText?: (runtime: string, cwd: string) => string | null | Promise<string | null>): Promise<PhantomImport[]>;
37
37
  /** Render flagged phantoms as an authoritative research section, or '' if none. */
38
38
  export declare function formatApiCorrections(phantoms: PhantomImport[]): string;
39
39
  /**
@@ -54,7 +54,7 @@ export declare function formatApiOverrideBanner(phantoms: PhantomImport[]): stri
54
54
  * Unreadable mentions are skipped; silent when types are absent or nothing is flagged.
55
55
  * Drives the impl-handoff override banner (Layer B).
56
56
  */
57
- export declare function findDeliveryPhantoms(spec: string, cwd: string): PhantomImport[];
57
+ export declare function findDeliveryPhantoms(spec: string, cwd: string): Promise<PhantomImport[]>;
58
58
  /**
59
59
  * Subtractively rewrite every flagged phantom specifier in `text` to the canonical
60
60
  * import the installed types prove, so NO affirmative occurrence of the non-existent
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import * as fs from 'node:fs';
15
15
  import * as path from 'node:path';
16
- import { resolvePackage, splitRuntimeNamespace, detectTypesRedirect, typesPackageName, hasTypeFiles, isDtsFile, ResolveError } from './docs-resolve.js';
16
+ import { resolvePackage, splitRuntimeNamespace, hasTypeFiles, isDtsFile, resolveTypeSource, ResolveError } from './docs-resolve.js';
17
17
  // Runtime builtin specifiers as they appear in prose/code: `bun:sql`,
18
18
  // `node:fs/promises`. Bounded to the runtimes splitRuntimeNamespace accepts.
19
19
  const SPEC_RE = /\b(?:bun|node|deno):[a-z0-9][a-z0-9/_-]*/gi;
@@ -67,40 +67,27 @@ function suggestionFor(v, runtime) {
67
67
  return (`\`${v.spec}\` is NOT a real module.${list} Import the needed symbol from `
68
68
  + `"${runtime}" or verify the correct specifier with pi-worker-docs; do not declare a module for it.`);
69
69
  }
70
- /** Sync resolution of a runtime to the package that actually holds its type
71
- * declarations (bun -> @types/bun -> bun-types), bounded to a few hops. No
72
- * auto-install: a runtime whose types aren't installed simply can't be verified
73
- * (returns null), so we never flag what we can't prove. */
74
- function resolveRuntimeTypesRoot(runtime, cwd) {
75
- let cur;
70
+ /** The phantom-import checker's adapter over the shared redirect walk: hops resolve
71
+ * SYNCHRONOUSLY and never install. A runtime whose types aren't on disk simply
72
+ * can't be verified (null), so we never flag what we can't prove. */
73
+ async function resolveRuntimeTypesRoot(runtime, cwd) {
74
+ let start;
76
75
  try {
77
- cur = resolvePackage(runtime, cwd);
76
+ start = resolvePackage(runtime, cwd);
78
77
  }
79
78
  catch (err) {
80
79
  if (err instanceof ResolveError)
81
80
  return null;
82
81
  throw err;
83
82
  }
84
- const visited = new Set([cur.name, runtime]);
85
- for (let hop = 0; hop < 3; hop++) {
86
- let next = detectTypesRedirect(cur);
87
- if (next && visited.has(next))
88
- next = null;
89
- if (!next && !hasTypeFiles(cur.root)) {
90
- const types = typesPackageName(cur.name);
91
- if (types && !visited.has(types))
92
- next = types;
93
- }
94
- if (!next)
95
- break;
96
- visited.add(next);
83
+ const cur = await resolveTypeSource(start, runtime, next => {
97
84
  try {
98
- cur = resolvePackage(next, cwd);
85
+ return Promise.resolve(resolvePackage(next, cwd));
99
86
  }
100
87
  catch {
101
- break;
88
+ return Promise.resolve(null);
102
89
  }
103
- }
90
+ });
104
91
  return hasTypeFiles(cur.root) ? cur.root : null;
105
92
  }
106
93
  const MAX_TYPE_BYTES = 4_000_000;
@@ -141,8 +128,8 @@ function readRuntimeTypeText(root) {
141
128
  return parts.join('\n');
142
129
  }
143
130
  /** Default loader: resolve the runtime's installed types and read their .d.ts. */
144
- export function loadRuntimeTypeText(runtime, cwd) {
145
- const root = resolveRuntimeTypesRoot(runtime, cwd);
131
+ export async function loadRuntimeTypeText(runtime, cwd) {
132
+ const root = await resolveRuntimeTypesRoot(runtime, cwd);
146
133
  if (!root)
147
134
  return null;
148
135
  const text = readRuntimeTypeText(root);
@@ -154,7 +141,7 @@ export function loadRuntimeTypeText(runtime, cwd) {
154
141
  * the runtime's type package. A runtime whose types can't be loaded is skipped
155
142
  * (we never flag what we can't verify), so this is silent when types are absent.
156
143
  */
157
- export function findPhantomImports(text, cwd, loadText = loadRuntimeTypeText) {
144
+ export async function findPhantomImports(text, cwd, loadText = loadRuntimeTypeText) {
158
145
  const out = [];
159
146
  const typeTextByRuntime = new Map();
160
147
  for (const spec of extractRuntimeSpecifiers(text)) {
@@ -162,7 +149,7 @@ export function findPhantomImports(text, cwd, loadText = loadRuntimeTypeText) {
162
149
  if (!ns)
163
150
  continue;
164
151
  if (!typeTextByRuntime.has(ns.runtime)) {
165
- typeTextByRuntime.set(ns.runtime, loadText(ns.runtime, cwd));
152
+ typeTextByRuntime.set(ns.runtime, await loadText(ns.runtime, cwd));
166
153
  }
167
154
  const typeText = typeTextByRuntime.get(ns.runtime);
168
155
  if (!typeText)
@@ -219,7 +206,7 @@ const MENTION_TRAILING_PUNCT = /[.,;:!?)\]}>"']+$/;
219
206
  * Unreadable mentions are skipped; silent when types are absent or nothing is flagged.
220
207
  * Drives the impl-handoff override banner (Layer B).
221
208
  */
222
- export function findDeliveryPhantoms(spec, cwd) {
209
+ export async function findDeliveryPhantoms(spec, cwd) {
223
210
  let text = spec;
224
211
  const seen = new Set();
225
212
  for (const m of spec.matchAll(MENTION_RE)) {
@@ -5,6 +5,32 @@ import { resolvePackage as defaultResolvePackage } from './docs-resolve.js';
5
5
  import { retrieveChunks as defaultRetrieveChunks } from './docs-retrieve.js';
6
6
  import { npmVersionLookup as defaultNpmVersionLookup } from './npm-version.js';
7
7
  import type { SpawnFn } from '../shared/child-process.js';
8
+ interface DocsDetails {
9
+ version?: string;
10
+ hitCache?: boolean;
11
+ chunksRetrieved?: number;
12
+ excerptVerified?: boolean;
13
+ childExitCode?: number;
14
+ indexingMs?: number;
15
+ indexedFiles?: number;
16
+ resolveError?: 'not_installed' | 'invalid_name';
17
+ cacheError?: string;
18
+ aborted?: boolean;
19
+ autoInstalled?: boolean;
20
+ installError?: string;
21
+ npmLatest?: string;
22
+ npmPublishedAt?: string;
23
+ versionSource?: 'declared-range' | 'npm-latest';
24
+ declaredRange?: string;
25
+ /**
26
+ * The answer restated a declaration for a question that needed usage semantics, so it
27
+ * is UNANSWERED (F-2). Set by isTypeOnlyAnswer; read by `cacheable` so a non-answer is
28
+ * never memoised and re-served to a later sibling task.
29
+ */
30
+ typeOnly?: boolean;
31
+ /** The 5B CAP arm refused this call: the attempt's project-lookup budget is spent. */
32
+ budgetSpent?: boolean;
33
+ }
8
34
  /**
9
35
  * Pull `@see {@link https://…}` pointers out of retrieved .d.ts/README text.
10
36
  *
@@ -30,3 +56,26 @@ export interface PiWorkerDocsInternals {
30
56
  npmVersionLookup?: typeof defaultNpmVersionLookup;
31
57
  }
32
58
  export declare function registerPiWorkerDocs(pi: ExtensionAPI, internals?: PiWorkerDocsInternals): void;
59
+ /**
60
+ * The F-2(e) cache rule for the docs channel, as a NAMED export rather than an
61
+ * anonymous property of an adapter literal.
62
+ *
63
+ * It was reachable only through `registerTool → execute()`, so
64
+ * pi-worker-docs-typeonly.test.ts gave up and hand-retyped it under a
65
+ * "keep in sync" comment — six tests asserting against a copy that a change to the
66
+ * shipped rule would leave green. That is the same drift class the rule itself
67
+ * exists to prevent: four regexes matching three phrasings, documented at length in
68
+ * abstention.ts, which cost a real bug.
69
+ */
70
+ export declare function docsCacheable(d: Pick<DocsDetails, 'childExitCode' | 'typeOnly' | 'excerptVerified'>, text: string): boolean;
71
+ /** The docs cache key: a package's answer is per (module, question). A project-source
72
+ * `.` lookup is never cached — the working tree mutates as tasks implement. */
73
+ export declare function docsCacheKey(params: {
74
+ module: string;
75
+ query: string;
76
+ }): string | null;
77
+ /** Package provenance for per-entry resume invalidation. */
78
+ export declare function docsCachePkg(params: {
79
+ module: string;
80
+ }): string | undefined;
81
+ export {};
@@ -3,7 +3,8 @@ import { Type } from '@sinclair/typebox';
3
3
  import { Text } from '@earendil-works/pi-tui';
4
4
  import { openCache as defaultOpenCache } from './docs-cache.js';
5
5
  import { retrieveChunks as defaultRetrieveChunks } from './docs-retrieve.js';
6
- import { docsRaw, formatResultText, packageHeader, buildPrompt, buildVersionBanner } from './docs-core.js';
6
+ import { formatResultText } from '../shared/child-output.js';
7
+ import { docsRaw, packageHeader, buildPrompt, buildVersionBanner } from './docs-core.js';
7
8
  import { formatNpmVersionSection } from './npm-version.js';
8
9
  import { runFocusedExtraction } from './focused-extractor.js';
9
10
  import { makeWorkerTool } from './shared.js';
@@ -374,15 +375,13 @@ export function registerPiWorkerDocs(pi, internals = {}) {
374
375
  // version do not change within a run). A project-source `.` lookup is NOT cached:
375
376
  // the working tree mutates as tasks implement, so its answer can go stale mid-run
376
377
  // (the docs SQLite index already keys those on file mtime).
377
- cacheKey: params => params.module === '.' ?
378
- null
379
- : `${normalizeQuery(params.module)}::${normalizeQuery(params.query)}`,
378
+ cacheKey: docsCacheKey,
380
379
  // Package provenance for per-entry resume invalidation: a docs digest describes
381
380
  // one package at one declared version, so a resume drops it only when THAT
382
381
  // package moves — an unrelated install no longer discards it. Package names are
383
382
  // matched against package.json verbatim (npm names are case-sensitive), unlike
384
383
  // the cache key, which normalises for phrasing collisions.
385
- cachePkg: params => (params.module === '.' ? undefined : packageRootOf(params.module)),
384
+ cachePkg: docsCachePkg,
386
385
  // Only a completed lookup (child exited 0) is a real answer; not-installed,
387
386
  // no-chunks, resolve/cache errors, and aborts omit childExitCode:0 and fall
388
387
  // through to a live retry next time.
@@ -396,9 +395,34 @@ export function registerPiWorkerDocs(pi, internals = {}) {
396
395
  //
397
396
  // `text` is supplied by makeWorkerTool (shared.ts) alongside details, so the
398
397
  // content check needs no new plumbing.
399
- cacheable: (d, text) => d.childExitCode === 0
400
- && d.typeOnly !== true
401
- && d.excerptVerified !== false
402
- && !isAbstention(text)
398
+ cacheable: docsCacheable
403
399
  });
404
400
  }
401
+ /**
402
+ * The F-2(e) cache rule for the docs channel, as a NAMED export rather than an
403
+ * anonymous property of an adapter literal.
404
+ *
405
+ * It was reachable only through `registerTool → execute()`, so
406
+ * pi-worker-docs-typeonly.test.ts gave up and hand-retyped it under a
407
+ * "keep in sync" comment — six tests asserting against a copy that a change to the
408
+ * shipped rule would leave green. That is the same drift class the rule itself
409
+ * exists to prevent: four regexes matching three phrasings, documented at length in
410
+ * abstention.ts, which cost a real bug.
411
+ */
412
+ export function docsCacheable(d, text) {
413
+ return (d.childExitCode === 0
414
+ && d.typeOnly !== true
415
+ && d.excerptVerified !== false
416
+ && !isAbstention(text));
417
+ }
418
+ /** The docs cache key: a package's answer is per (module, question). A project-source
419
+ * `.` lookup is never cached — the working tree mutates as tasks implement. */
420
+ export function docsCacheKey(params) {
421
+ return params.module === '.' ?
422
+ null
423
+ : `${normalizeQuery(params.module)}::${normalizeQuery(params.query)}`;
424
+ }
425
+ /** Package provenance for per-entry resume invalidation. */
426
+ export function docsCachePkg(params) {
427
+ return params.module === '.' ? undefined : packageRootOf(params.module);
428
+ }
@@ -1,6 +1,12 @@
1
1
  import type { EventEmitter } from 'node:events';
2
2
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
3
3
  import { fetchAndClean as defaultFetchAndClean } from './html-clean.js';
4
+ interface FetchDetails {
5
+ childExitCode?: number;
6
+ answer?: string;
7
+ excerpt?: string;
8
+ excerptVerified?: boolean;
9
+ }
4
10
  interface ProcLike extends EventEmitter {
5
11
  stdout: EventEmitter | null;
6
12
  stderr: EventEmitter | null;
@@ -17,4 +23,16 @@ export interface PiWorkerFetchInternals {
17
23
  spawn?: SpawnFn;
18
24
  }
19
25
  export declare function registerPiWorkerFetch(pi: ExtensionAPI, internals?: PiWorkerFetchInternals): void;
26
+ /**
27
+ * The F-2(e) cache rule for the fetch channel, named for the same reason as
28
+ * `docsCacheable`: pi-worker-fetch.test.ts carried a hand-retyped copy driving four
29
+ * tests, which a change to the shipped rule would leave green.
30
+ */
31
+ export declare function fetchCacheable(d: Pick<FetchDetails, 'childExitCode'>, text: string): boolean;
32
+ /** The fetch cache key. URL verbatim (path case can matter), question normalised —
33
+ * same page, different question is a different answer. */
34
+ export declare function fetchCacheKey(params: {
35
+ url: string;
36
+ query: string;
37
+ }): string;
20
38
  export {};