@mjasnikovs/pi-task 0.40.14 → 0.40.25
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/dist/workers/abstention.js +20 -8
- package/dist/workers/docs-chunk.d.ts +39 -1
- package/dist/workers/docs-chunk.js +66 -17
- package/dist/workers/docs-ecosystems.d.ts +6 -0
- package/dist/workers/docs-ecosystems.js +6 -3
- package/dist/workers/docs-index.js +19 -5
- package/dist/workers/docs-project.js +1 -1
- package/dist/workers/docs-retrieve.d.ts +31 -5
- package/dist/workers/docs-retrieve.js +37 -6
- package/dist/workers/eco-cargo.d.ts +2 -0
- package/dist/workers/eco-cargo.js +2 -0
- package/dist/workers/eco-hackage.d.ts +2 -0
- package/dist/workers/eco-hackage.js +2 -0
- package/dist/workers/pi-worker-docs.d.ts +17 -1
- package/dist/workers/pi-worker-docs.js +20 -2
- package/package.json +1 -1
|
@@ -39,15 +39,27 @@ export function abstentionSentence(kind) {
|
|
|
39
39
|
* Matches any corpus's abstention. Built from the same table the prompts read, so
|
|
40
40
|
* adding a corpus cannot leave a matcher behind.
|
|
41
41
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* it
|
|
47
|
-
*
|
|
48
|
-
*
|
|
42
|
+
* ANCHORED, and it was a substring match until it was measured. The docstring here
|
|
43
|
+
* used to say "rule 4 asks for this sentinel INSTEAD of an answer, so nothing
|
|
44
|
+
* sourced can contain it". That was true when it was written and defect 15's fix
|
|
45
|
+
* made it false: rule 4 now ends with "answer the parts <content> covers, and name
|
|
46
|
+
* the parts it does not", and a child naming the missing part reaches for the
|
|
47
|
+
* phrase the same prompt just taught it.
|
|
48
|
+
*
|
|
49
|
+
* Six of 73 flagged abstentions across seven runs were substantial answers that
|
|
50
|
+
* named a gap at the end — full signatures for `oneshot`, `TcpListener::bind`,
|
|
51
|
+
* `eitherDecode`, a whole zod schema — every one scored as a non-answer. Rule 4
|
|
52
|
+
* asks for the sentence ALONE, so leading with it is the abstention and trailing it
|
|
53
|
+
* behind real content is the partial answer defect 15 exists to produce.
|
|
54
|
+
*
|
|
55
|
+
* The leading `<answer>` tag is admitted because some consumers see the child's raw
|
|
56
|
+
* output rather than the parsed body.
|
|
57
|
+
*
|
|
58
|
+
* The cost, stated: a child that writes prose first and only then declines outright
|
|
59
|
+
* now scores as an answer. Across 243 recorded answers that shape does not occur —
|
|
60
|
+
* all 67 real abstentions lead with the sentence.
|
|
49
61
|
*/
|
|
50
|
-
const ABSTENTION_RE = new RegExp(
|
|
62
|
+
const ABSTENTION_RE = new RegExp(`^(?:\\s*<answer>)?[\\s"'\`\\-*]*unclear\\s+from\\s+this\\s+(${Object.values(NOUNS).join('|')})\\b`, 'i');
|
|
51
63
|
/** True when the child declined to answer rather than answering. See the header
|
|
52
64
|
* for the two decisions this drives. */
|
|
53
65
|
export function isAbstention(text) {
|
|
@@ -34,6 +34,19 @@ export declare const MAX_CHUNK_BYTES: number;
|
|
|
34
34
|
* defines `fetch`.
|
|
35
35
|
*/
|
|
36
36
|
export declare const DECL_SPLIT_RE: RegExp;
|
|
37
|
+
/**
|
|
38
|
+
* Where a member of an oversized declaration begins — the same heads, indented.
|
|
39
|
+
*
|
|
40
|
+
* `declare module "bun" { … }` is ONE top-level declaration holding a whole module,
|
|
41
|
+
* so `DECL_SPLIT_RE` matches once and everything after it was cut at byte offsets.
|
|
42
|
+
* That shape is rare and enormous: 3.8% of indexed chunks sat at the cap and held
|
|
43
|
+
* 51.1% of all indexed bytes, 86.8% of `@types/node`'s and 78.1% of `bun-types`'.
|
|
44
|
+
*
|
|
45
|
+
* Only reached when a declaration does not fit. A member split applied to every
|
|
46
|
+
* declaration would cut an interface away from its own members, which is the thing
|
|
47
|
+
* `DECL_SPLIT_RE` exists to prevent.
|
|
48
|
+
*/
|
|
49
|
+
export declare const MEMBER_SPLIT_RE: RegExp;
|
|
37
50
|
/** Where a README section starts. */
|
|
38
51
|
export declare const README_SPLIT_RE: RegExp;
|
|
39
52
|
/**
|
|
@@ -60,6 +73,18 @@ export declare function splitAtMatches(text: string, re: RegExp): string[];
|
|
|
60
73
|
* possible hallucination. Only reachable on non-ASCII text past the chunk ceiling.
|
|
61
74
|
*/
|
|
62
75
|
export declare function sliceBytes(s: string, maxBytes: number): string[];
|
|
76
|
+
/**
|
|
77
|
+
* Slice a body that will not fit, giving every piece the same header.
|
|
78
|
+
*
|
|
79
|
+
* The header was applied once and then sliced, so only the first piece said where
|
|
80
|
+
* it came from: 487 of 12,815 indexed chunks had no provenance line, 53% of
|
|
81
|
+
* `@types/node`'s and 22% of `bun-types`'. A `node:url` query for `fileURLToPath`
|
|
82
|
+
* came back as two 8,192-byte pieces that began mid-sentence inside a doc comment
|
|
83
|
+
* and named no file.
|
|
84
|
+
*
|
|
85
|
+
* The cap counts the header, so the body budget is what is left after it.
|
|
86
|
+
*/
|
|
87
|
+
export declare function headedSlices(header: string, body: string, maxBytes: number): string[];
|
|
63
88
|
/**
|
|
64
89
|
* Chunk a declaration file, one chunk per declaration, each labelled with the
|
|
65
90
|
* file it came from.
|
|
@@ -73,6 +98,19 @@ export declare function sliceBytes(s: string, maxBytes: number): string[];
|
|
|
73
98
|
* `splitRe` and `commentPrefix` default to the TypeScript pair, which is what
|
|
74
99
|
* both the project corpus and npm packages are written in.
|
|
75
100
|
*/
|
|
76
|
-
export declare function chunkDeclarations(content: string, relPath: string, splitRe?: RegExp, commentPrefix?: string): string[];
|
|
101
|
+
export declare function chunkDeclarations(content: string, relPath: string, splitRe?: RegExp, commentPrefix?: string, memberRe?: RegExp): string[];
|
|
102
|
+
/**
|
|
103
|
+
* Cut a declaration that does not fit at its own member boundaries, keeping the
|
|
104
|
+
* line that says what it is a member OF.
|
|
105
|
+
*
|
|
106
|
+
* Without that line a piece of `declare module "bun"` is an anonymous list of
|
|
107
|
+
* functions: a `Bun.file` question came back carrying slices about S3 ETags and
|
|
108
|
+
* tar archives, because bm25 was matching words in the middles of byte cuts that
|
|
109
|
+
* shared no subject.
|
|
110
|
+
*
|
|
111
|
+
* Byte slicing stays as the floor. A single member wider than the cap — one
|
|
112
|
+
* function with a 20 KB doc comment — still has to be cut somewhere.
|
|
113
|
+
*/
|
|
114
|
+
export declare function splitOversized(header: string, body: string, memberRe: RegExp): string[];
|
|
77
115
|
/** Chunk a README, one chunk per top-level section, each labelled by heading. */
|
|
78
116
|
export declare function chunkReadme(content: string): string[];
|
|
@@ -34,6 +34,19 @@ export const MAX_CHUNK_BYTES = 8 * 1024;
|
|
|
34
34
|
* defines `fetch`.
|
|
35
35
|
*/
|
|
36
36
|
export const DECL_SPLIT_RE = /^(?:export\s+)?(?:declare\s+)?(?:default\s+)?(?:abstract\s+)?(?:async\s+)?(?:function|class|interface|type|namespace|module|const|let|var|enum)\s+/m;
|
|
37
|
+
/**
|
|
38
|
+
* Where a member of an oversized declaration begins — the same heads, indented.
|
|
39
|
+
*
|
|
40
|
+
* `declare module "bun" { … }` is ONE top-level declaration holding a whole module,
|
|
41
|
+
* so `DECL_SPLIT_RE` matches once and everything after it was cut at byte offsets.
|
|
42
|
+
* That shape is rare and enormous: 3.8% of indexed chunks sat at the cap and held
|
|
43
|
+
* 51.1% of all indexed bytes, 86.8% of `@types/node`'s and 78.1% of `bun-types`'.
|
|
44
|
+
*
|
|
45
|
+
* Only reached when a declaration does not fit. A member split applied to every
|
|
46
|
+
* declaration would cut an interface away from its own members, which is the thing
|
|
47
|
+
* `DECL_SPLIT_RE` exists to prevent.
|
|
48
|
+
*/
|
|
49
|
+
export const MEMBER_SPLIT_RE = /^[ \t]+(?:export\s+)?(?:declare\s+)?(?:default\s+)?(?:abstract\s+)?(?:async\s+)?(?:function|class|interface|type|namespace|module|const|let|var|enum)\s+/m;
|
|
37
50
|
/** Where a README section starts. */
|
|
38
51
|
export const README_SPLIT_RE = /^#{1,2} /m;
|
|
39
52
|
/**
|
|
@@ -106,6 +119,24 @@ export function sliceBytes(s, maxBytes) {
|
|
|
106
119
|
out.push(buf.toString('utf8'));
|
|
107
120
|
return out;
|
|
108
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Slice a body that will not fit, giving every piece the same header.
|
|
124
|
+
*
|
|
125
|
+
* The header was applied once and then sliced, so only the first piece said where
|
|
126
|
+
* it came from: 487 of 12,815 indexed chunks had no provenance line, 53% of
|
|
127
|
+
* `@types/node`'s and 22% of `bun-types`'. A `node:url` query for `fileURLToPath`
|
|
128
|
+
* came back as two 8,192-byte pieces that began mid-sentence inside a doc comment
|
|
129
|
+
* and named no file.
|
|
130
|
+
*
|
|
131
|
+
* The cap counts the header, so the body budget is what is left after it.
|
|
132
|
+
*/
|
|
133
|
+
export function headedSlices(header, body, maxBytes) {
|
|
134
|
+
const prefixed = `${header}\n${body}`;
|
|
135
|
+
if (Buffer.byteLength(prefixed, 'utf8') <= maxBytes)
|
|
136
|
+
return [prefixed];
|
|
137
|
+
const room = maxBytes - Buffer.byteLength(`${header}\n`, 'utf8');
|
|
138
|
+
return sliceBytes(body, room).map(slice => `${header}\n${slice}`);
|
|
139
|
+
}
|
|
109
140
|
/**
|
|
110
141
|
* Chunk a declaration file, one chunk per declaration, each labelled with the
|
|
111
142
|
* file it came from.
|
|
@@ -119,23 +150,48 @@ export function sliceBytes(s, maxBytes) {
|
|
|
119
150
|
* `splitRe` and `commentPrefix` default to the TypeScript pair, which is what
|
|
120
151
|
* both the project corpus and npm packages are written in.
|
|
121
152
|
*/
|
|
122
|
-
export function chunkDeclarations(content, relPath, splitRe = DECL_SPLIT_RE, commentPrefix = '//') {
|
|
153
|
+
export function chunkDeclarations(content, relPath, splitRe = DECL_SPLIT_RE, commentPrefix = '//', memberRe = MEMBER_SPLIT_RE) {
|
|
123
154
|
const chunks = [];
|
|
155
|
+
const header = `${commentPrefix} ${relPath}`;
|
|
124
156
|
for (const part of splitAtMatches(content, new RegExp(splitRe.source, 'gm'))) {
|
|
125
157
|
const trimmed = part.trim();
|
|
126
158
|
if (!trimmed)
|
|
127
159
|
continue;
|
|
128
|
-
|
|
129
|
-
if (Buffer.byteLength(prefixed, 'utf8') > MAX_CHUNK_BYTES) {
|
|
130
|
-
for (const slice of sliceBytes(prefixed, MAX_CHUNK_BYTES))
|
|
131
|
-
chunks.push(slice);
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
chunks.push(prefixed);
|
|
135
|
-
}
|
|
160
|
+
chunks.push(...splitOversized(header, trimmed, memberRe));
|
|
136
161
|
}
|
|
137
162
|
return chunks;
|
|
138
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Cut a declaration that does not fit at its own member boundaries, keeping the
|
|
166
|
+
* line that says what it is a member OF.
|
|
167
|
+
*
|
|
168
|
+
* Without that line a piece of `declare module "bun"` is an anonymous list of
|
|
169
|
+
* functions: a `Bun.file` question came back carrying slices about S3 ETags and
|
|
170
|
+
* tar archives, because bm25 was matching words in the middles of byte cuts that
|
|
171
|
+
* shared no subject.
|
|
172
|
+
*
|
|
173
|
+
* Byte slicing stays as the floor. A single member wider than the cap — one
|
|
174
|
+
* function with a 20 KB doc comment — still has to be cut somewhere.
|
|
175
|
+
*/
|
|
176
|
+
export function splitOversized(header, body, memberRe) {
|
|
177
|
+
if (Buffer.byteLength(`${header}\n${body}`, 'utf8') <= MAX_CHUNK_BYTES) {
|
|
178
|
+
return [`${header}\n${body}`];
|
|
179
|
+
}
|
|
180
|
+
const parts = splitAtMatches(body, new RegExp(memberRe.source, 'gm'));
|
|
181
|
+
if (parts.length < 2)
|
|
182
|
+
return headedSlices(header, body, MAX_CHUNK_BYTES);
|
|
183
|
+
// The first part carries the enclosing head; every later one has to be told.
|
|
184
|
+
const enclosing = parts[0].split('\n')[0].trim();
|
|
185
|
+
const out = [];
|
|
186
|
+
for (const [i, part] of parts.entries()) {
|
|
187
|
+
const trimmed = part.replace(/\s+$/, '');
|
|
188
|
+
if (!trimmed.trim())
|
|
189
|
+
continue;
|
|
190
|
+
const withContext = i === 0 ? trimmed : `${enclosing}\n${trimmed}`;
|
|
191
|
+
out.push(...headedSlices(header, withContext, MAX_CHUNK_BYTES));
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
139
195
|
/** Chunk a README, one chunk per top-level section, each labelled by heading. */
|
|
140
196
|
export function chunkReadme(content) {
|
|
141
197
|
const chunks = [];
|
|
@@ -146,14 +202,7 @@ export function chunkReadme(content) {
|
|
|
146
202
|
continue;
|
|
147
203
|
const headingMatch = /^(#{1,2}) (.+)$/m.exec(trimmed);
|
|
148
204
|
const heading = headingMatch ? headingMatch[2] : '(intro)';
|
|
149
|
-
|
|
150
|
-
if (Buffer.byteLength(prefixed, 'utf8') > MAX_CHUNK_BYTES) {
|
|
151
|
-
for (const slice of sliceBytes(prefixed, MAX_CHUNK_BYTES))
|
|
152
|
-
chunks.push(slice);
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
chunks.push(prefixed);
|
|
156
|
-
}
|
|
205
|
+
chunks.push(...headedSlices(`<!-- README: ${heading} -->`, trimmed, MAX_CHUNK_BYTES));
|
|
157
206
|
}
|
|
158
207
|
return chunks;
|
|
159
208
|
}
|
|
@@ -107,6 +107,12 @@ export interface EcosystemProfile {
|
|
|
107
107
|
surface: (content: string) => string;
|
|
108
108
|
/** Where a declaration begins, so a chunk never splits a signature. */
|
|
109
109
|
declSplitRe: RegExp;
|
|
110
|
+
/**
|
|
111
|
+
* Where a MEMBER of a declaration begins — the same heads, indented. Reached
|
|
112
|
+
* only when one declaration does not fit a chunk, which in npm is
|
|
113
|
+
* `declare module "bun" { … }` holding a whole module.
|
|
114
|
+
*/
|
|
115
|
+
memberSplitRe: RegExp;
|
|
110
116
|
/**
|
|
111
117
|
* The keywords that INTRODUCE a named type in this language, for finding the
|
|
112
118
|
* chunk that defines a name rather than the many that use it.
|
|
@@ -19,10 +19,10 @@ import * as os from 'node:os';
|
|
|
19
19
|
import * as path from 'node:path';
|
|
20
20
|
import { runAutoInstall, findDeclaredRange, extractParentPackage, resolveTypeSourceForDocs, getDocsModulesDir } from './docs-core.js';
|
|
21
21
|
import { resolvePackage, isDtsFile, isValidModuleName } from './docs-resolve.js';
|
|
22
|
-
import { DECL_SPLIT_RE } from './docs-chunk.js';
|
|
22
|
+
import { DECL_SPLIT_RE, MEMBER_SPLIT_RE } from './docs-chunk.js';
|
|
23
23
|
import { npmVersionLookup } from './npm-version.js';
|
|
24
|
-
import { resolveCrate, cratesLatest, crateTarballUrl, crateOf, isValidCrateName, isRustFile, lockedVersion, rustSurface, cargoProjectName, childDirs, lockedDeps, manifestCrates, cargoExportGap, cargoContentFingerprint, cargoSupplementCandidates, CARGO_DECL_SPLIT_RE } from './eco-cargo.js';
|
|
25
|
-
import { resolveHackage, hackageLatest, hackageVersion, hackageTarballUrl, hackageExtractDir, hackageProjectName, supplementCandidates, hackageExportGap, hackageContentFingerprint, findCabalTarball, cachedVersions, resolvedVersions, manifestPackages, isValidHackageName, isHaskellFile, haskellSurface, HACKAGE_DECL_SPLIT_RE, HACKAGE_SKIP_DIRS } from './eco-hackage.js';
|
|
24
|
+
import { resolveCrate, cratesLatest, crateTarballUrl, crateOf, isValidCrateName, isRustFile, lockedVersion, rustSurface, cargoProjectName, childDirs, lockedDeps, manifestCrates, cargoExportGap, cargoContentFingerprint, cargoSupplementCandidates, CARGO_DECL_SPLIT_RE, CARGO_MEMBER_SPLIT_RE } from './eco-cargo.js';
|
|
25
|
+
import { resolveHackage, hackageLatest, hackageVersion, hackageTarballUrl, hackageExtractDir, hackageProjectName, supplementCandidates, hackageExportGap, hackageContentFingerprint, findCabalTarball, cachedVersions, resolvedVersions, manifestPackages, isValidHackageName, isHaskellFile, haskellSurface, HACKAGE_DECL_SPLIT_RE, HACKAGE_SKIP_DIRS, HACKAGE_MEMBER_SPLIT_RE } from './eco-hackage.js';
|
|
26
26
|
import { runChild } from '../shared/child-process.js';
|
|
27
27
|
/**
|
|
28
28
|
* Is any of `names` present at `cwd` or above it?
|
|
@@ -134,6 +134,7 @@ export function npmProfile(hooks = {}) {
|
|
|
134
134
|
// identity for a fingerprint to miss.
|
|
135
135
|
contentFingerprint: () => String(npmSurface),
|
|
136
136
|
declSplitRe: DECL_SPLIT_RE,
|
|
137
|
+
memberSplitRe: MEMBER_SPLIT_RE,
|
|
137
138
|
typeKeywords: ['interface', 'type', 'class', 'enum'],
|
|
138
139
|
commentPrefix: '//',
|
|
139
140
|
// A nested node_modules is another package's surface, never this one's.
|
|
@@ -313,6 +314,7 @@ const cargoProfile = {
|
|
|
313
314
|
// index as `insideTrait`. `contentFingerprint` is what covers its source.
|
|
314
315
|
surface: content => rustSurface(content),
|
|
315
316
|
declSplitRe: CARGO_DECL_SPLIT_RE,
|
|
317
|
+
memberSplitRe: CARGO_MEMBER_SPLIT_RE,
|
|
316
318
|
typeKeywords: ['struct', 'trait', 'enum', 'type', 'union'],
|
|
317
319
|
commentPrefix: '//',
|
|
318
320
|
skipDirs: ['tests', 'benches', 'examples', 'target'],
|
|
@@ -429,6 +431,7 @@ const hackageProfile = {
|
|
|
429
431
|
isSurfaceFile: isHaskellFile,
|
|
430
432
|
surface: haskellSurface,
|
|
431
433
|
declSplitRe: HACKAGE_DECL_SPLIT_RE,
|
|
434
|
+
memberSplitRe: HACKAGE_MEMBER_SPLIT_RE,
|
|
432
435
|
typeKeywords: ['type', 'data', 'newtype', 'class'],
|
|
433
436
|
commentPrefix: '--',
|
|
434
437
|
skipDirs: HACKAGE_SKIP_DIRS,
|
|
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import {} from './docs-resolve.js';
|
|
5
|
-
import { chunkDeclarations, chunkReadme, splitAtMatches } from './docs-chunk.js';
|
|
5
|
+
import { chunkDeclarations, chunkReadme, splitAtMatches, headedSlices, splitOversized, MEMBER_SPLIT_RE } from './docs-chunk.js';
|
|
6
6
|
import { ECOSYSTEMS } from './docs-ecosystems.js';
|
|
7
7
|
const ZERO_SEP = Buffer.from([0]);
|
|
8
8
|
/**
|
|
@@ -33,13 +33,27 @@ const ZERO_SEP = Buffer.from([0]);
|
|
|
33
33
|
* have said so.
|
|
34
34
|
*/
|
|
35
35
|
export function chunkerFingerprint() {
|
|
36
|
-
return
|
|
36
|
+
return [
|
|
37
|
+
String(splitAtMatches),
|
|
38
|
+
String(chunkDeclarations),
|
|
39
|
+
String(chunkReadme),
|
|
40
|
+
// Both chunkers now delegate their slicing, so their own source would sit
|
|
41
|
+
// still through a change to where an oversized declaration is cut. That is
|
|
42
|
+
// the third fix to hide one level below a `String(fn)` here.
|
|
43
|
+
String(headedSlices),
|
|
44
|
+
String(splitOversized),
|
|
45
|
+
// The member split is per-ecosystem, and a profile's regex is not source
|
|
46
|
+
// this function can see. `declSplitRe` is already hashed beside it in
|
|
47
|
+
// computeContentHash; this puts its sibling there too.
|
|
48
|
+
MEMBER_SPLIT_RE.source
|
|
49
|
+
].join('\u0000');
|
|
37
50
|
}
|
|
38
51
|
function computeContentHash(pkg, profile, supplements = []) {
|
|
39
52
|
const hash = createHash('sha256');
|
|
40
53
|
hash.update(Buffer.from(`${pkg.name}@${pkg.version}`, 'utf8'));
|
|
41
54
|
hash.update(ZERO_SEP);
|
|
42
|
-
hash.update(Buffer.from(`${profile.declSplitRe.source}\u0000${profile.
|
|
55
|
+
hash.update(Buffer.from(`${profile.declSplitRe.source}\u0000${profile.memberSplitRe.source}`
|
|
56
|
+
+ `\u0000${profile.commentPrefix}`, 'utf8'));
|
|
43
57
|
hash.update(ZERO_SEP);
|
|
44
58
|
hash.update(Buffer.from(chunkerFingerprint(), 'utf8'));
|
|
45
59
|
hash.update(ZERO_SEP);
|
|
@@ -205,7 +219,7 @@ function ingestBody(cache, pkg, profile, contentHash, supplements = []) {
|
|
|
205
219
|
catch {
|
|
206
220
|
continue;
|
|
207
221
|
}
|
|
208
|
-
const chunks = chunkDeclarations(profile.surface(raw), rel, profile.declSplitRe, profile.commentPrefix);
|
|
222
|
+
const chunks = chunkDeclarations(profile.surface(raw), rel, profile.declSplitRe, profile.commentPrefix, profile.memberSplitRe);
|
|
209
223
|
if (!chunks.length)
|
|
210
224
|
continue;
|
|
211
225
|
filesIngested++;
|
|
@@ -236,7 +250,7 @@ function ingestBody(cache, pkg, profile, contentHash, supplements = []) {
|
|
|
236
250
|
// wrong package is the bug this whole table exists for.
|
|
237
251
|
const rel = `${sup.name}-${sup.version}/` + path.relative(sup.root, abs).replace(/\\/g, '/');
|
|
238
252
|
const whole = gap.wholesale(rel, raw);
|
|
239
|
-
for (const c of chunkDeclarations(profile.surface(raw), rel, profile.declSplitRe, profile.commentPrefix)) {
|
|
253
|
+
for (const c of chunkDeclarations(profile.surface(raw), rel, profile.declSplitRe, profile.commentPrefix, profile.memberSplitRe)) {
|
|
240
254
|
if (!whole && !gap.fillsHole(c.replace(/^\S.*\n/, '')))
|
|
241
255
|
continue;
|
|
242
256
|
if (seen.has(c))
|
|
@@ -174,7 +174,7 @@ export function ensureProjectIndexed(cache, name, version, files, cwd) {
|
|
|
174
174
|
// `pi-worker-docs(".", …)` answers "no chunks" for the whole project.
|
|
175
175
|
// Only the CHUNK BOUNDARY is language-specific.
|
|
176
176
|
const profile = profileForFile(abs, profiles);
|
|
177
|
-
const chunks = chunkDeclarations(raw, rel, profile.declSplitRe, profile.commentPrefix);
|
|
177
|
+
const chunks = chunkDeclarations(raw, rel, profile.declSplitRe, profile.commentPrefix, profile.memberSplitRe);
|
|
178
178
|
if (!chunks.length)
|
|
179
179
|
continue;
|
|
180
180
|
filesIngested++;
|
|
@@ -30,12 +30,20 @@ export interface RetrieveOptions {
|
|
|
30
30
|
*
|
|
31
31
|
* retrieval — does a chunk DEFINE the queried symbol? 91/101 -> 97/101,
|
|
32
32
|
* monotone to 50 and flat at 100, p = 0.0703
|
|
33
|
-
* answers —
|
|
34
|
-
*
|
|
33
|
+
* answers — 67/94 -> 79/94, p = 0.0075 when first measured, and it DOES NOT
|
|
34
|
+
* replicate. That run was block-ordered, which makes arm and
|
|
35
|
+
* position the same variable; re-run as warm-up then ABBA it reads
|
|
36
|
+
* 159/206 -> 163/206, p = 0.5716, with both within-arm A/A controls
|
|
37
|
+
* at p = 1.0000. The retrieval line above is what justifies 50.
|
|
35
38
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* reached
|
|
39
|
+
* The cost is +70% of retrieved text, 14,986 characters per call to 20,882, and
|
|
40
|
+
* `RETRIEVE_CONTENT_BUDGET` now binds where it did not: at 8 only 6 of 74 calls
|
|
41
|
+
* reached the budget, at 50 it is 90 of 103.
|
|
42
|
+
*
|
|
43
|
+
* Do not raise the budget to follow it. 24,000 -> 48,000 was measured the same
|
|
44
|
+
* balanced way and reads p = 0.1360 on answers and p = 0.2500 on defines. Three
|
|
45
|
+
* measurements now agree that retrieved VOLUME is not what makes this child
|
|
46
|
+
* answer; what the text IS, is.
|
|
39
47
|
*
|
|
40
48
|
* Changing either is still a retrieval-policy change, not a tidy-up.
|
|
41
49
|
*/
|
|
@@ -43,4 +51,22 @@ export declare const PACKAGE_RETRIEVE_LIMIT = 50;
|
|
|
43
51
|
export declare const PROJECT_RETRIEVE_LIMIT = 50;
|
|
44
52
|
/** Character budget for the assembled chunk text. The same for both corpora. */
|
|
45
53
|
export declare const RETRIEVE_CONTENT_BUDGET = 24000;
|
|
54
|
+
/**
|
|
55
|
+
* The type names the retrieved text declares MEMBERS as, whose own definitions
|
|
56
|
+
* are not in hand and which the query itself names — by the member or by the
|
|
57
|
+
* type.
|
|
58
|
+
*
|
|
59
|
+
* This is the alias hop. A package that types its public surface through
|
|
60
|
+
* interface aliases puts every real signature one declaration away from the name
|
|
61
|
+
* a query matches: hono writes `get: HandlerInterface<…>` in hono-base.d.ts and
|
|
62
|
+
* keeps the call signatures in `HandlerInterface`, in types.d.ts. BM25 ranks
|
|
63
|
+
* chunks independently, so retrieval lands on the alias and the extraction child
|
|
64
|
+
* sees a name where a signature should be. Measured on hono 4.13.5: three real
|
|
65
|
+
* lookups, three abstentions, and the definition in one chunk of 708.
|
|
66
|
+
*
|
|
67
|
+
* Ranking hops by frequency does not work — `Response` and the English word
|
|
68
|
+
* `The` both outrank `HandlerInterface` in the same text. What the query names
|
|
69
|
+
* is the signal.
|
|
70
|
+
*/
|
|
71
|
+
export declare function hopNames(text: string, tokens: string[]): string[];
|
|
46
72
|
export declare function retrieveChunks(cache: CacheHandle, opts: RetrieveOptions): RetrievedChunk[];
|
|
@@ -7,12 +7,20 @@
|
|
|
7
7
|
*
|
|
8
8
|
* retrieval — does a chunk DEFINE the queried symbol? 91/101 -> 97/101,
|
|
9
9
|
* monotone to 50 and flat at 100, p = 0.0703
|
|
10
|
-
* answers —
|
|
11
|
-
*
|
|
10
|
+
* answers — 67/94 -> 79/94, p = 0.0075 when first measured, and it DOES NOT
|
|
11
|
+
* replicate. That run was block-ordered, which makes arm and
|
|
12
|
+
* position the same variable; re-run as warm-up then ABBA it reads
|
|
13
|
+
* 159/206 -> 163/206, p = 0.5716, with both within-arm A/A controls
|
|
14
|
+
* at p = 1.0000. The retrieval line above is what justifies 50.
|
|
12
15
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* reached
|
|
16
|
+
* The cost is +70% of retrieved text, 14,986 characters per call to 20,882, and
|
|
17
|
+
* `RETRIEVE_CONTENT_BUDGET` now binds where it did not: at 8 only 6 of 74 calls
|
|
18
|
+
* reached the budget, at 50 it is 90 of 103.
|
|
19
|
+
*
|
|
20
|
+
* Do not raise the budget to follow it. 24,000 -> 48,000 was measured the same
|
|
21
|
+
* balanced way and reads p = 0.1360 on answers and p = 0.2500 on defines. Three
|
|
22
|
+
* measurements now agree that retrieved VOLUME is not what makes this child
|
|
23
|
+
* answer; what the text IS, is.
|
|
16
24
|
*
|
|
17
25
|
* Changing either is still a retrieval-policy change, not a tidy-up.
|
|
18
26
|
*/
|
|
@@ -25,6 +33,25 @@ export const RETRIEVE_CONTENT_BUDGET = 24_000;
|
|
|
25
33
|
// backstop for a third caller that does not.
|
|
26
34
|
const DEFAULT_LIMIT = PROJECT_RETRIEVE_LIMIT;
|
|
27
35
|
const DEFAULT_BUDGET = RETRIEVE_CONTENT_BUDGET;
|
|
36
|
+
/**
|
|
37
|
+
* Shortest query token that reaches the FTS query. Swept twice, and 2 stays.
|
|
38
|
+
*
|
|
39
|
+
* The first sweep put the peak at 3 — one gain, no losses — because no truth entry
|
|
40
|
+
* named a two-letter symbol and the metric could not see what 3 throws away. Two-
|
|
41
|
+
* letter tokens across every recorded query are mostly English filler (`to` 97,
|
|
42
|
+
* `of` 64, `in` 57), and mixed in with them are `v4` 13, `it` 47, `IO`, `fn`, `u8`.
|
|
43
|
+
*
|
|
44
|
+
* One entry for `bun:test`'s `it` was enough to reverse it:
|
|
45
|
+
*
|
|
46
|
+
* min without a 2-letter entry with one
|
|
47
|
+
* 2 150/157 151/159 <- peak
|
|
48
|
+
* 3 151/157 +1 150/159 -1
|
|
49
|
+
* 4 150/157 146/159 p = 0.1797
|
|
50
|
+
* 5 134/157 p = 0.0015 131/159 p = 8.8e-5
|
|
51
|
+
*
|
|
52
|
+
* Raising it drops `v4` from a zod query, which is the token that distinguishes the
|
|
53
|
+
* major this whole test set exists to pin.
|
|
54
|
+
*/
|
|
28
55
|
const MIN_TOKEN_LEN = 2;
|
|
29
56
|
/**
|
|
30
57
|
* How many alias definitions one retrieval will chase. Three covers the observed
|
|
@@ -120,6 +147,10 @@ function enforceBudget(chunks, budget) {
|
|
|
120
147
|
total += c.content.length;
|
|
121
148
|
continue;
|
|
122
149
|
}
|
|
150
|
+
// `break`, not `continue`, and it is load-bearing. Packing lower-ranked
|
|
151
|
+
// chunks into the gap left by an oversized one leaves less room in the
|
|
152
|
+
// SECOND enforceBudget, where the alias hops sit — and a dropped hop costs
|
|
153
|
+
// more than a gained tail chunk. Measured: defines 150/157 -> 149/157.
|
|
123
154
|
if (total + c.content.length > budget)
|
|
124
155
|
break;
|
|
125
156
|
out.push(c);
|
|
@@ -144,7 +175,7 @@ function enforceBudget(chunks, budget) {
|
|
|
144
175
|
* `The` both outrank `HandlerInterface` in the same text. What the query names
|
|
145
176
|
* is the signal.
|
|
146
177
|
*/
|
|
147
|
-
function hopNames(text, tokens) {
|
|
178
|
+
export function hopNames(text, tokens) {
|
|
148
179
|
const declared = new Set([...text.matchAll(TYPE_DECL_RE)].map(m => m[1]));
|
|
149
180
|
const typeParams = new Set();
|
|
150
181
|
for (const m of text.matchAll(TYPE_PARAMS_RE)) {
|
|
@@ -83,6 +83,8 @@ export declare function crateTarballUrl(name: string, version: string): string;
|
|
|
83
83
|
* receiver type, carrying the next method's doc comment.
|
|
84
84
|
*/
|
|
85
85
|
export declare const CARGO_DECL_SPLIT_RE: RegExp;
|
|
86
|
+
/** The same heads, indented — a member of an oversized `impl` or `mod` block. */
|
|
87
|
+
export declare const CARGO_MEMBER_SPLIT_RE: RegExp;
|
|
86
88
|
interface Item {
|
|
87
89
|
/** Doc comments and attributes immediately above the item. */
|
|
88
90
|
pending: string;
|
|
@@ -365,6 +365,8 @@ export function crateTarballUrl(name, version) {
|
|
|
365
365
|
* receiver type, carrying the next method's doc comment.
|
|
366
366
|
*/
|
|
367
367
|
export const CARGO_DECL_SPLIT_RE = /^(?:#\[[^\n]*\]\s*)*(?:pub\s+)?(?:async\s+|unsafe\s+|const\s+|extern\s+)*(?:fn|struct|enum|union|trait|type|impl|mod|const|static)\b/m;
|
|
368
|
+
/** The same heads, indented — a member of an oversized `impl` or `mod` block. */
|
|
369
|
+
export const CARGO_MEMBER_SPLIT_RE = /^[ \t]+(?:#\[[^\n]*\]\s*)*(?:pub\s+)?(?:async\s+|unsafe\s+|const\s+|extern\s+)*(?:fn|struct|enum|union|trait|type|impl|mod|const|static)\b/m;
|
|
368
370
|
// `macro_rules!` carries its own terminator, so it sits OUTSIDE the `\b` — a word
|
|
369
371
|
// boundary after `!` requires a word character next, and what follows is a space.
|
|
370
372
|
const ITEM_HEAD_RE = /^(?:pub(?:\s*\([^)]*\))?\s+)?(?:default\s+|async\s+|unsafe\s+|const\s+|extern\s+"[^"]*"\s+|extern\s+)*((?:fn|struct|enum|union|trait|impl|mod|type|const|static|use)\b|macro_rules!)/;
|
|
@@ -67,6 +67,8 @@ export declare function resolveHackage(name: string, cwd: string, dirs: HackageR
|
|
|
67
67
|
export declare function hackageLatest(name: string, fetchFn: typeof fetch, signal?: AbortSignal): Promise<NpmVersionInfo | null>;
|
|
68
68
|
/** Where a Haskell declaration begins, so a chunk never splits a signature. */
|
|
69
69
|
export declare const HACKAGE_DECL_SPLIT_RE: RegExp;
|
|
70
|
+
/** The same heads, indented — a member of an oversized `class` or `instance`. */
|
|
71
|
+
export declare const HACKAGE_MEMBER_SPLIT_RE: RegExp;
|
|
70
72
|
/**
|
|
71
73
|
* Reduce Haskell source to its public API surface: the module header with its
|
|
72
74
|
* export list, every top-level type signature, every type and class
|
|
@@ -303,6 +303,8 @@ export async function hackageLatest(name, fetchFn, signal) {
|
|
|
303
303
|
// ── surface extraction ──────────────────────────────────────────────────────
|
|
304
304
|
/** Where a Haskell declaration begins, so a chunk never splits a signature. */
|
|
305
305
|
export const HACKAGE_DECL_SPLIT_RE = /^(?:[a-z_][\w']*\s*::|data\s|newtype\s|type\s|class\s|instance\s|pattern\s)/m;
|
|
306
|
+
/** The same heads, indented — a member of an oversized `class` or `instance`. */
|
|
307
|
+
export const HACKAGE_MEMBER_SPLIT_RE = /^[ \t]+(?:[a-z_][\w']*\s*::|data\s|newtype\s|type\s|class\s|instance\s|pattern\s)/m;
|
|
306
308
|
const SIGNATURE_RE = /^[a-z_][\w']*(?:\s*,\s*[a-z_][\w']*)*\s*::/;
|
|
307
309
|
const OPERATOR_SIGNATURE_RE = /^\([^)]+\)\s*::/;
|
|
308
310
|
/** The same heads with the `::` wrapped onto the next line. */
|
|
@@ -12,6 +12,12 @@ interface DocsDetails {
|
|
|
12
12
|
hitCache?: boolean;
|
|
13
13
|
chunksRetrieved?: number;
|
|
14
14
|
excerptVerified?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* The excerpt cites a word the source never wrote — a possible fabrication, as
|
|
17
|
+
* opposed to a quote stitched from several real spans. `excerptVerified: false`
|
|
18
|
+
* covers both and cannot tell them apart.
|
|
19
|
+
*/
|
|
20
|
+
excerptFabricated?: boolean;
|
|
15
21
|
childExitCode?: number;
|
|
16
22
|
indexingMs?: number;
|
|
17
23
|
indexedFiles?: number;
|
|
@@ -73,7 +79,17 @@ export declare function registerPiWorkerDocs(pi: ExtensionAPI, internals?: PiWor
|
|
|
73
79
|
* assert against its own copy — green even after the shipped rule changed. Exported,
|
|
74
80
|
* the test imports the rule it is checking.
|
|
75
81
|
*/
|
|
76
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Did the excerpt cite a word the source never wrote?
|
|
84
|
+
*
|
|
85
|
+
* `verified` is false for a stitched quote too, and the two must not share a gate:
|
|
86
|
+
* one is a possible fabrication and the other is what the extraction prompt asks
|
|
87
|
+
* for. Absent of a check, nothing was verified and nothing is claimed.
|
|
88
|
+
*/
|
|
89
|
+
export declare function excerptFabricated(check: {
|
|
90
|
+
absent: readonly string[];
|
|
91
|
+
} | undefined): boolean;
|
|
92
|
+
export declare function docsCacheable(d: Pick<DocsDetails, 'typeOnly' | 'excerptVerified' | 'excerptFabricated'>, text: string): boolean;
|
|
77
93
|
/** The docs cache key: a package's answer is per (module, question), with the question
|
|
78
94
|
* lowercased and its whitespace collapsed so phrasing variants share one entry. Returns
|
|
79
95
|
* null for the project-source `.` lookup, which is never cached — the working tree
|
|
@@ -218,7 +218,8 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
218
218
|
});
|
|
219
219
|
return workerAnswer(text, {
|
|
220
220
|
...baseDetails,
|
|
221
|
-
excerptVerified: verified
|
|
221
|
+
excerptVerified: verified,
|
|
222
|
+
excerptFabricated: excerptFabricated(extraction.excerptCheck)
|
|
222
223
|
});
|
|
223
224
|
}
|
|
224
225
|
// ── npm package lookup (existing path) ──────────────────────────
|
|
@@ -353,6 +354,7 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
353
354
|
return workerAnswer(text, {
|
|
354
355
|
...baseDetails,
|
|
355
356
|
excerptVerified: verified,
|
|
357
|
+
excerptFabricated: excerptFabricated(extraction.excerptCheck),
|
|
356
358
|
...(typeOnly.typeOnly ? { typeOnly: true } : {})
|
|
357
359
|
});
|
|
358
360
|
},
|
|
@@ -397,12 +399,28 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
397
399
|
* assert against its own copy — green even after the shipped rule changed. Exported,
|
|
398
400
|
* the test imports the rule it is checking.
|
|
399
401
|
*/
|
|
402
|
+
/**
|
|
403
|
+
* Did the excerpt cite a word the source never wrote?
|
|
404
|
+
*
|
|
405
|
+
* `verified` is false for a stitched quote too, and the two must not share a gate:
|
|
406
|
+
* one is a possible fabrication and the other is what the extraction prompt asks
|
|
407
|
+
* for. Absent of a check, nothing was verified and nothing is claimed.
|
|
408
|
+
*/
|
|
409
|
+
export function excerptFabricated(check) {
|
|
410
|
+
return check !== undefined && check.absent.length > 0;
|
|
411
|
+
}
|
|
400
412
|
export function docsCacheable(d, text) {
|
|
401
413
|
// Answer QUALITY only. Whether there IS an answer is `WorkerOutcome.kind`, and
|
|
402
414
|
// `makeWorkerTool` has already refused an `unavailable` before reaching here —
|
|
403
415
|
// opening this with `childExitCode === 0` memoises an aborted lookup for the
|
|
404
416
|
// whole run, because a signal-killed child satisfies it.
|
|
405
|
-
|
|
417
|
+
//
|
|
418
|
+
// It reads `excerptFabricated`, not `excerptVerified`. A quarter of every run's
|
|
419
|
+
// answers come back unverified and this refused all of them; re-checked with the
|
|
420
|
+
// classifier defect 18 added, 41 of 41 unverified excerpts across seven runs are
|
|
421
|
+
// STITCHED — every span verbatim, not one absent word. The gate was refusing
|
|
422
|
+
// non-contiguous quoting, and every sibling paid a fresh child for it.
|
|
423
|
+
return d.typeOnly !== true && d.excerptFabricated !== true && !isAbstention(text);
|
|
406
424
|
}
|
|
407
425
|
/** The docs cache key: a package's answer is per (module, question), with the question
|
|
408
426
|
* lowercased and its whitespace collapsed so phrasing variants share one entry. Returns
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.25",
|
|
4
4
|
"description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|