@mjasnikovs/pi-task 0.40.14 → 0.40.26
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 +71 -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 +23 -1
- package/dist/workers/pi-worker-docs.js +29 -7
- package/dist/workers/pi-worker-fetch.d.ts +1 -1
- package/dist/workers/pi-worker-fetch.js +10 -6
- package/dist/workers/shared.d.ts +5 -1
- package/dist/workers/shared.js +0 -0
- 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,29 @@ 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
|
+
// A header that fills the cap on its own leaves no room, and `sliceBytes` with a
|
|
139
|
+
// non-positive cap never shrinks its buffer. Slice the prefixed string instead:
|
|
140
|
+
// the first piece still names the source, which is all the header buys.
|
|
141
|
+
if (room <= 0)
|
|
142
|
+
return sliceBytes(prefixed, maxBytes);
|
|
143
|
+
return sliceBytes(body, room).map(slice => `${header}\n${slice}`);
|
|
144
|
+
}
|
|
109
145
|
/**
|
|
110
146
|
* Chunk a declaration file, one chunk per declaration, each labelled with the
|
|
111
147
|
* file it came from.
|
|
@@ -119,23 +155,48 @@ export function sliceBytes(s, maxBytes) {
|
|
|
119
155
|
* `splitRe` and `commentPrefix` default to the TypeScript pair, which is what
|
|
120
156
|
* both the project corpus and npm packages are written in.
|
|
121
157
|
*/
|
|
122
|
-
export function chunkDeclarations(content, relPath, splitRe = DECL_SPLIT_RE, commentPrefix = '//') {
|
|
158
|
+
export function chunkDeclarations(content, relPath, splitRe = DECL_SPLIT_RE, commentPrefix = '//', memberRe = MEMBER_SPLIT_RE) {
|
|
123
159
|
const chunks = [];
|
|
160
|
+
const header = `${commentPrefix} ${relPath}`;
|
|
124
161
|
for (const part of splitAtMatches(content, new RegExp(splitRe.source, 'gm'))) {
|
|
125
162
|
const trimmed = part.trim();
|
|
126
163
|
if (!trimmed)
|
|
127
164
|
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
|
-
}
|
|
165
|
+
chunks.push(...splitOversized(header, trimmed, memberRe));
|
|
136
166
|
}
|
|
137
167
|
return chunks;
|
|
138
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Cut a declaration that does not fit at its own member boundaries, keeping the
|
|
171
|
+
* line that says what it is a member OF.
|
|
172
|
+
*
|
|
173
|
+
* Without that line a piece of `declare module "bun"` is an anonymous list of
|
|
174
|
+
* functions: a `Bun.file` question came back carrying slices about S3 ETags and
|
|
175
|
+
* tar archives, because bm25 was matching words in the middles of byte cuts that
|
|
176
|
+
* shared no subject.
|
|
177
|
+
*
|
|
178
|
+
* Byte slicing stays as the floor. A single member wider than the cap — one
|
|
179
|
+
* function with a 20 KB doc comment — still has to be cut somewhere.
|
|
180
|
+
*/
|
|
181
|
+
export function splitOversized(header, body, memberRe) {
|
|
182
|
+
if (Buffer.byteLength(`${header}\n${body}`, 'utf8') <= MAX_CHUNK_BYTES) {
|
|
183
|
+
return [`${header}\n${body}`];
|
|
184
|
+
}
|
|
185
|
+
const parts = splitAtMatches(body, new RegExp(memberRe.source, 'gm'));
|
|
186
|
+
if (parts.length < 2)
|
|
187
|
+
return headedSlices(header, body, MAX_CHUNK_BYTES);
|
|
188
|
+
// The first part carries the enclosing head; every later one has to be told.
|
|
189
|
+
const enclosing = parts[0].split('\n')[0].trim();
|
|
190
|
+
const out = [];
|
|
191
|
+
for (const [i, part] of parts.entries()) {
|
|
192
|
+
const trimmed = part.replace(/\s+$/, '');
|
|
193
|
+
if (!trimmed.trim())
|
|
194
|
+
continue;
|
|
195
|
+
const withContext = i === 0 ? trimmed : `${enclosing}\n${trimmed}`;
|
|
196
|
+
out.push(...headedSlices(header, withContext, MAX_CHUNK_BYTES));
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
139
200
|
/** Chunk a README, one chunk per top-level section, each labelled by heading. */
|
|
140
201
|
export function chunkReadme(content) {
|
|
141
202
|
const chunks = [];
|
|
@@ -146,14 +207,7 @@ export function chunkReadme(content) {
|
|
|
146
207
|
continue;
|
|
147
208
|
const headingMatch = /^(#{1,2}) (.+)$/m.exec(trimmed);
|
|
148
209
|
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
|
-
}
|
|
210
|
+
chunks.push(...headedSlices(`<!-- README: ${heading} -->`, trimmed, MAX_CHUNK_BYTES));
|
|
157
211
|
}
|
|
158
212
|
return chunks;
|
|
159
213
|
}
|
|
@@ -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;
|
|
@@ -35,6 +41,12 @@ interface DocsDetails {
|
|
|
35
41
|
/** The project-lookup budget for this attempt is spent, so the call was refused
|
|
36
42
|
* before any work. Only set when PI_TASK_PROJECT_DOCS_BUDGET is configured. */
|
|
37
43
|
budgetSpent?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* The child declined to answer. Recorded HERE, at the one place the bare
|
|
46
|
+
* `<answer>` is in hand: the tool text a cache predicate is handed leads with a
|
|
47
|
+
* provenance header, and the anchored matcher scores that as a real answer.
|
|
48
|
+
*/
|
|
49
|
+
abstained?: boolean;
|
|
38
50
|
}
|
|
39
51
|
/**
|
|
40
52
|
* Pull `@see {@link https://…}` pointers out of retrieved .d.ts/README text.
|
|
@@ -73,7 +85,17 @@ export declare function registerPiWorkerDocs(pi: ExtensionAPI, internals?: PiWor
|
|
|
73
85
|
* assert against its own copy — green even after the shipped rule changed. Exported,
|
|
74
86
|
* the test imports the rule it is checking.
|
|
75
87
|
*/
|
|
76
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Did the excerpt cite a word the source never wrote?
|
|
90
|
+
*
|
|
91
|
+
* `verified` is false for a stitched quote too, and the two must not share a gate:
|
|
92
|
+
* one is a possible fabrication and the other is what the extraction prompt asks
|
|
93
|
+
* for. Absent of a check, nothing was verified and nothing is claimed.
|
|
94
|
+
*/
|
|
95
|
+
export declare function excerptFabricated(check: {
|
|
96
|
+
absent: readonly string[];
|
|
97
|
+
} | undefined): boolean;
|
|
98
|
+
export declare function docsCacheable(d: Pick<DocsDetails, 'typeOnly' | 'excerptVerified' | 'excerptFabricated' | 'abstained'>): boolean;
|
|
77
99
|
/** The docs cache key: a package's answer is per (module, question), with the question
|
|
78
100
|
* lowercased and its whitespace collapsed so phrasing variants share one entry. Returns
|
|
79
101
|
* null for the project-source `.` lookup, which is never cached — the working tree
|
|
@@ -218,7 +218,9 @@ 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),
|
|
223
|
+
...(isAbstention(extraction.answer) ? { abstained: true } : {})
|
|
222
224
|
});
|
|
223
225
|
}
|
|
224
226
|
// ── npm package lookup (existing path) ──────────────────────────
|
|
@@ -353,7 +355,9 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
353
355
|
return workerAnswer(text, {
|
|
354
356
|
...baseDetails,
|
|
355
357
|
excerptVerified: verified,
|
|
356
|
-
|
|
358
|
+
excerptFabricated: excerptFabricated(extraction.excerptCheck),
|
|
359
|
+
...(typeOnly.typeOnly ? { typeOnly: true } : {}),
|
|
360
|
+
...(isAbstention(extraction.answer) ? { abstained: true } : {})
|
|
357
361
|
});
|
|
358
362
|
},
|
|
359
363
|
renderCall(args, theme) {
|
|
@@ -382,9 +386,6 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
382
386
|
// child that ran fine and answered "unclear from this package" exits 0 — so a rule
|
|
383
387
|
// keyed on exit code would memoise that non-answer and re-serve it as a hit to
|
|
384
388
|
// every later sibling, with nothing left to re-trigger an escalation.
|
|
385
|
-
//
|
|
386
|
-
// `text` is supplied by makeWorkerTool (shared.ts) alongside details, so the
|
|
387
|
-
// content check needs no new plumbing.
|
|
388
389
|
cacheable: docsCacheable
|
|
389
390
|
});
|
|
390
391
|
}
|
|
@@ -397,12 +398,33 @@ export function registerPiWorkerDocs(pi, internals = {}) {
|
|
|
397
398
|
* assert against its own copy — green even after the shipped rule changed. Exported,
|
|
398
399
|
* the test imports the rule it is checking.
|
|
399
400
|
*/
|
|
400
|
-
|
|
401
|
+
/**
|
|
402
|
+
* Did the excerpt cite a word the source never wrote?
|
|
403
|
+
*
|
|
404
|
+
* `verified` is false for a stitched quote too, and the two must not share a gate:
|
|
405
|
+
* one is a possible fabrication and the other is what the extraction prompt asks
|
|
406
|
+
* for. Absent of a check, nothing was verified and nothing is claimed.
|
|
407
|
+
*/
|
|
408
|
+
export function excerptFabricated(check) {
|
|
409
|
+
return check !== undefined && check.absent.length > 0;
|
|
410
|
+
}
|
|
411
|
+
export function docsCacheable(d) {
|
|
401
412
|
// Answer QUALITY only. Whether there IS an answer is `WorkerOutcome.kind`, and
|
|
402
413
|
// `makeWorkerTool` has already refused an `unavailable` before reaching here —
|
|
403
414
|
// opening this with `childExitCode === 0` memoises an aborted lookup for the
|
|
404
415
|
// whole run, because a signal-killed child satisfies it.
|
|
405
|
-
|
|
416
|
+
//
|
|
417
|
+
// It reads `excerptFabricated`, not `excerptVerified`. A quarter of every run's
|
|
418
|
+
// answers come back unverified and this refused all of them; re-checked with the
|
|
419
|
+
// classifier defect 18 added, 41 of 41 unverified excerpts across seven runs are
|
|
420
|
+
// STITCHED — every span verbatim, not one absent word. The gate was refusing
|
|
421
|
+
// non-contiguous quoting, and every sibling paid a fresh child for it.
|
|
422
|
+
//
|
|
423
|
+
// The abstention is read off DETAILS, not off the tool text. `makeWorkerTool`
|
|
424
|
+
// hands this the FINAL text, which leads with `Per <pkg>@<version>:` — and
|
|
425
|
+
// `isAbstention` is anchored, so testing that text scored every abstention as a
|
|
426
|
+
// real answer and memoised the dead end for the whole run.
|
|
427
|
+
return d.typeOnly !== true && d.excerptFabricated !== true && d.abstained !== true;
|
|
406
428
|
}
|
|
407
429
|
/** The docs cache key: a package's answer is per (module, question), with the question
|
|
408
430
|
* lowercased and its whitespace collapsed so phrasing variants share one entry. Returns
|
|
@@ -32,7 +32,7 @@ export declare function registerPiWorkerFetch(pi: ExtensionAPI, internals?: PiWo
|
|
|
32
32
|
* ABOUT that page, and re-fetching cannot change it — only the abstention sentinel is
|
|
33
33
|
* refused.
|
|
34
34
|
*/
|
|
35
|
-
export declare function fetchCacheable(
|
|
35
|
+
export declare function fetchCacheable(d: Pick<FetchDetails, 'answer'>): boolean;
|
|
36
36
|
/** The fetch cache key. URL verbatim (path case can matter), question normalised —
|
|
37
37
|
* same page, different question is a different answer. */
|
|
38
38
|
export declare function fetchCacheKey(params: {
|
|
@@ -112,12 +112,16 @@ export function registerPiWorkerFetch(pi, internals = {}) {
|
|
|
112
112
|
* ABOUT that page, and re-fetching cannot change it — only the abstention sentinel is
|
|
113
113
|
* refused.
|
|
114
114
|
*/
|
|
115
|
-
export function fetchCacheable(
|
|
116
|
-
// Answer QUALITY only — see docsCacheable.
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
|
|
115
|
+
export function fetchCacheable(d) {
|
|
116
|
+
// Answer QUALITY only — see docsCacheable. An aborted fetch is kept out of the
|
|
117
|
+
// cache by its `unavailable` outcome upstream, never by this rule; leading with
|
|
118
|
+
// `childExitCode === 0` would not work, because an aborted child settles at 0.
|
|
119
|
+
//
|
|
120
|
+
// It reads the child's bare answer, not the rendered tool text: `isAbstention` is
|
|
121
|
+
// anchored, and the text leads with an excerpt NOTE/WARNING whenever the excerpt
|
|
122
|
+
// did not verify — which, since rule 4 asks for the closest related text, is the
|
|
123
|
+
// ordinary shape of an abstention.
|
|
124
|
+
return d.answer !== undefined && !isAbstention(d.answer);
|
|
121
125
|
}
|
|
122
126
|
/** The fetch cache key. URL verbatim (path case can matter), question normalised —
|
|
123
127
|
* same page, different question is a different answer. */
|
package/dist/workers/shared.d.ts
CHANGED
|
@@ -121,8 +121,12 @@ export interface WorkerToolSpec<TParams extends TSchema, TDetails> {
|
|
|
121
121
|
* (type-only, an abstention, an unverified excerpt), never about process
|
|
122
122
|
* health. An `unavailable` outcome never reaches this: `makeWorkerTool` has
|
|
123
123
|
* already refused it. Defaults to always-cacheable when omitted.
|
|
124
|
+
*
|
|
125
|
+
* It sees `details` only. Handed the rendered tool text as well, a rule reaches
|
|
126
|
+
* for it and reads a string that leads with provenance and excerpt notes — and an
|
|
127
|
+
* anchored abstention matcher then scores every abstention as an answer.
|
|
124
128
|
*/
|
|
125
|
-
cacheable?(details: TDetails
|
|
129
|
+
cacheable?(details: TDetails): boolean;
|
|
126
130
|
}
|
|
127
131
|
/** Register a worker tool from its spec, supplying the shared registration ritual. */
|
|
128
132
|
export declare function makeWorkerTool<TParams extends TSchema, TDetails>(pi: ExtensionAPI, spec: WorkerToolSpec<TParams, TDetails>): void;
|
package/dist/workers/shared.js
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.26",
|
|
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",
|