@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.
- package/README.md +7 -3
- package/dist/shared/child-process.d.ts +8 -0
- package/dist/shared/command-watchdog.d.ts +1 -1
- package/dist/shared/command-watchdog.js +1 -1
- package/dist/task/accept-debt.d.ts +47 -0
- package/dist/task/accept-debt.js +127 -28
- package/dist/task/auto-orchestrator.js +93 -116
- package/dist/task/boot-probe.d.ts +298 -0
- package/dist/task/boot-probe.js +806 -0
- package/dist/task/child-runner.d.ts +56 -25
- package/dist/task/child-runner.js +65 -31
- package/dist/task/child-status.d.ts +95 -0
- package/dist/task/child-status.js +99 -0
- package/dist/task/command-run.d.ts +36 -0
- package/dist/task/command-run.js +48 -1
- package/dist/task/command-watchdog.js +1 -1
- package/dist/task/context-usage.d.ts +4 -3
- package/dist/task/context-usage.js +4 -3
- package/dist/task/contracts.js +18 -35
- package/dist/task/deep-render-check.d.ts +47 -0
- package/dist/task/deep-render-check.js +110 -65
- package/dist/task/env-notes.d.ts +3 -3
- package/dist/task/env-notes.js +24 -35
- package/dist/task/final-gate-fix.d.ts +1 -1
- package/dist/task/final-gate-fix.js +1 -1
- package/dist/task/final-gate.d.ts +38 -432
- package/dist/task/final-gate.js +105 -1213
- package/dist/task/gate-child.d.ts +8 -10
- package/dist/task/gate-child.js +15 -19
- package/dist/task/gate-deps.d.ts +29 -0
- package/dist/task/gate-deps.js +192 -206
- package/dist/task/gate-tally.d.ts +189 -0
- package/dist/task/gate-tally.js +249 -0
- package/dist/task/implementation-turn.d.ts +201 -0
- package/dist/task/implementation-turn.js +263 -0
- package/dist/task/launch-contract.js +27 -43
- package/dist/task/launch-manifest.d.ts +5 -0
- package/dist/task/launch-manifest.js +21 -0
- package/dist/task/ledger.d.ts +38 -0
- package/dist/task/ledger.js +83 -0
- package/dist/task/loop-detector.d.ts +14 -8
- package/dist/task/loop-detector.js +36 -12
- package/dist/task/orchestrator.d.ts +61 -126
- package/dist/task/orchestrator.js +70 -297
- package/dist/task/phases.d.ts +18 -0
- package/dist/task/phases.js +4 -3
- package/dist/task/plan-orchestrator.js +34 -33
- package/dist/task/requirements.d.ts +1 -1
- package/dist/task/requirements.js +50 -66
- package/dist/task/root-cause-repair.js +20 -32
- package/dist/task/run-bracket.d.ts +75 -0
- package/dist/task/run-bracket.js +41 -0
- package/dist/task/stall-detector.d.ts +110 -0
- package/dist/task/stall-detector.js +159 -0
- package/dist/task/task-gates.d.ts +69 -0
- package/dist/task/task-gates.js +114 -90
- package/dist/task/verify-work.d.ts +53 -67
- package/dist/task/verify-work.js +15 -11
- package/dist/workers/docs-core.d.ts +0 -4
- package/dist/workers/docs-core.js +10 -34
- package/dist/workers/docs-project.js +3 -3
- package/dist/workers/docs-resolve.d.ts +18 -0
- package/dist/workers/docs-resolve.js +39 -0
- package/dist/workers/docs-retrieve.d.ts +13 -0
- package/dist/workers/docs-retrieve.js +17 -2
- package/dist/workers/fetch-core.d.ts +0 -4
- package/dist/workers/fetch-core.js +2 -5
- package/dist/workers/phantom-imports.d.ts +3 -3
- package/dist/workers/phantom-imports.js +16 -29
- package/dist/workers/pi-worker-docs.d.ts +49 -0
- package/dist/workers/pi-worker-docs.js +33 -9
- package/dist/workers/pi-worker-fetch.d.ts +18 -0
- package/dist/workers/pi-worker-fetch.js +19 -4
- package/dist/workers/single-read-extension.d.ts +1 -1
- package/dist/workers/single-read-extension.js +5 -4
- package/dist/workers/single-read-guard.d.ts +32 -10
- package/dist/workers/single-read-guard.js +67 -16
- package/package.json +1 -1
package/dist/task/verify-work.js
CHANGED
|
@@ -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 =
|
|
133
|
-
// Probes are INDEPENDENTLY OPTIONAL: an absent
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
14
|
-
const
|
|
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
|
-
/**
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
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 =
|
|
9
|
-
const DEFAULT_BUDGET =
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
//
|
|
247
|
-
|
|
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,
|
|
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
|
-
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
85
|
+
return Promise.resolve(resolvePackage(next, cwd));
|
|
99
86
|
}
|
|
100
87
|
catch {
|
|
101
|
-
|
|
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 {
|
|
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:
|
|
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:
|
|
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:
|
|
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 {};
|