@mjasnikovs/pi-task 0.18.49 → 0.18.50
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/shared/child-output.d.ts +21 -7
- package/dist/shared/child-output.js +11 -0
- package/dist/task/apis-contract.d.ts +67 -0
- package/dist/task/apis-contract.js +77 -0
- package/dist/task/context-attribution.d.ts +123 -0
- package/dist/task/context-attribution.js +285 -0
- package/dist/task/context-silence.d.ts +53 -0
- package/dist/task/context-silence.js +93 -0
- package/dist/task/phases.js +187 -5
- package/dist/task/prompts.js +13 -0
- package/dist/task/spec-urls.d.ts +117 -0
- package/dist/task/spec-urls.js +214 -0
- package/dist/task/type-only-answer.d.ts +95 -0
- package/dist/task/type-only-answer.js +249 -0
- package/dist/workers/fetch-core.d.ts +50 -0
- package/dist/workers/fetch-core.js +118 -16
- package/dist/workers/pi-worker-core.d.ts +11 -0
- package/dist/workers/pi-worker-core.js +30 -0
- package/dist/workers/pi-worker-docs.d.ts +9 -0
- package/dist/workers/pi-worker-docs.js +119 -7
- package/dist/workers/pi-worker-fetch.js +3 -1
- package/dist/workers/typeonly-log.d.ts +45 -0
- package/dist/workers/typeonly-log.js +88 -0
- package/package.json +1 -1
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PROMPT 4 / F-2(b) — the design document's own cited URLs, ranked as fetch candidates for
|
|
3
|
+
* the APIS research worker.
|
|
4
|
+
*
|
|
5
|
+
* THE FACT THIS CLOSES. mx5's DESIGN/PROJECT.md is a literal reference list: §5 line 184
|
|
6
|
+
* cites https://hono.dev/docs/guides/rpc and §13 lists four hono.dev URLs plus
|
|
7
|
+
* https://bun.com/docs/runtime/sql. Neither of the two pages that document the semantics
|
|
8
|
+
* behind run 15's two fatal defects was ever fetched — worker:apis' 6 distinct fetches went
|
|
9
|
+
* to bun.com/reference/*, tailwindcss.com/* and nothing on hono.dev at all. Measured
|
|
10
|
+
* afterwards (scripts/spec-url-reach.ts): 31 of 44 tasks asked pi-worker-docs about a package
|
|
11
|
+
* for which the design cites a page, 215 (task, URL) pairs, and 15 of the 17 reachable cited
|
|
12
|
+
* URLs were never fetched by anyone. The pages were named, in the project's own spec, and the
|
|
13
|
+
* worker went looking somewhere else.
|
|
14
|
+
*
|
|
15
|
+
* WHY THIS IS A POPULATION LEVER AND THE TYPE-ONLY GUARD WAS NOT. PROMPT 2's detector fires
|
|
16
|
+
* on 0.54% of docs answers (9/1680 live) because it has to RECOGNISE something about an
|
|
17
|
+
* answer. This one recognises nothing: the URLs are already sitting in the spec text, so its
|
|
18
|
+
* reach is "every task whose design cites a page for a package the task uses" — the 31/44
|
|
19
|
+
* above, measured before any of this was written.
|
|
20
|
+
*
|
|
21
|
+
* RANKING, NOT REPLACING. The block says the cited pages outrank a page the model would pick
|
|
22
|
+
* itself, and says in terms that they are not the only pages it may fetch. A worker that can
|
|
23
|
+
* no longer follow a question off the design's reference list has been narrowed, not
|
|
24
|
+
* improved; PROMPT 4 invariant 3 asserts that explicitly in the A/B.
|
|
25
|
+
*
|
|
26
|
+
* ── *** NOT WIRED. THE LIVE A/B FAILED. READ THIS BEFORE RE-ENABLING IT. *** ─────────────
|
|
27
|
+
*
|
|
28
|
+
* scripts/live-spec-url-fetch-ab.ts, 2026-07-22, 40 reps, both arms in one process, real
|
|
29
|
+
* phaseResearch, offline fixture web, metric = WHICH URL WAS FETCHED at the tool layer:
|
|
30
|
+
*
|
|
31
|
+
* task27-hono baseline 2/10 treatment 3/10 Fisher one-tailed p = 0.50
|
|
32
|
+
* task28-wouter baseline 0/10 treatment 0/10 p = 1.00
|
|
33
|
+
* POOLED baseline 2/20 treatment 3/20 p = 0.50
|
|
34
|
+
*
|
|
35
|
+
* Everything that could have made that a false negative was ruled out, not assumed:
|
|
36
|
+
* - the surgery held in every rep (baseline block 0 chars 20/20, treatment non-empty 20/20);
|
|
37
|
+
* - a positive control proved a real child can reach the stubbed pi-worker-fetch;
|
|
38
|
+
* - the block was proven to REACH the APIS prompt, not merely to be built — the prompt is
|
|
39
|
+
* 19,473 chars and ends with the six ranked URLs
|
|
40
|
+
* (scripts/spec-url-prompt-delivery-check.ts).
|
|
41
|
+
* So the model reads the instruction and does not act on it.
|
|
42
|
+
*
|
|
43
|
+
* AND THE PREMISE ITSELF DID NOT SURVIVE. PROMPT 4 rests on run 15 having fetched the WRONG
|
|
44
|
+
* pages. Across all 40 reps the only URL any worker ever fetched, in either arm, was
|
|
45
|
+
* https://hono.dev/docs/guides/rpc — the cited one. The worker does not choose badly between
|
|
46
|
+
* pages; it almost never fetches at all (5 of 40 reps). A lever that improves URL RANKING is
|
|
47
|
+
* aimed at a decision this worker rarely makes.
|
|
48
|
+
*
|
|
49
|
+
* The module is kept — deterministic, unit-tested against the real run-15 design text, and
|
|
50
|
+
* the A/B harness's string surgery targets it — so the experiment can be re-run cheaply if
|
|
51
|
+
* the fetch rate itself is ever moved. It is NOT called from phases.ts, deliberately.
|
|
52
|
+
*/
|
|
53
|
+
/** Never candidates: a local dev URL is not documentation. */
|
|
54
|
+
const LOCAL_HOST = /^(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\]|.*\.local)$/i;
|
|
55
|
+
const PLACEHOLDER_HOST = /^(example\.(com|org|net)|your-.*|<.*)$/i;
|
|
56
|
+
/**
|
|
57
|
+
* How many cited pages the block may name. The design cites 21 URLs; listing all of them in
|
|
58
|
+
* every APIS prompt would be prefill spent on pages the task has no use for, and a list long
|
|
59
|
+
* enough to skim past is a list the model ignores. Eight is above the highest per-task
|
|
60
|
+
* reachable count measured on run 15 (5 hono.dev pages for a hono task) with headroom.
|
|
61
|
+
*/
|
|
62
|
+
export const MAX_SPEC_URLS = 8;
|
|
63
|
+
/** http(s) URLs in a text, deduped, with local and placeholder hosts dropped. */
|
|
64
|
+
export function extractSpecUrls(text) {
|
|
65
|
+
const out = [];
|
|
66
|
+
const seen = new Set();
|
|
67
|
+
for (const raw of text.match(/https?:\/\/[^\s)>`"']+/g) ?? []) {
|
|
68
|
+
// Trailing sentence punctuation is not part of the URL; a #fragment and a query are.
|
|
69
|
+
const url = raw.replace(/[.,;:]+$/, '');
|
|
70
|
+
if (seen.has(url))
|
|
71
|
+
continue;
|
|
72
|
+
let host;
|
|
73
|
+
try {
|
|
74
|
+
host = new URL(url).hostname;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (LOCAL_HOST.test(host) || PLACEHOLDER_HOST.test(host))
|
|
80
|
+
continue;
|
|
81
|
+
seen.add(url);
|
|
82
|
+
out.push(url);
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Tokens of a package specifier that could plausibly appear in its documentation URL: the
|
|
88
|
+
* root name plus every scope/path/hyphen segment. `hono/client` ⇒ {hono, client};
|
|
89
|
+
* `@hono/zod-validator` ⇒ {hono, zod, validator, zod-validator}. Segments of TWO characters or
|
|
90
|
+
* fewer are dropped — `pg` would match half the documentation web. Three is the floor rather
|
|
91
|
+
* than four because `zod` is a real dependency with a real cited page (zod.dev), and a rule
|
|
92
|
+
* that silently drops it is a rule that silently drops reach.
|
|
93
|
+
*/
|
|
94
|
+
export function packageTokens(pkg) {
|
|
95
|
+
const m = pkg.toLowerCase().trim();
|
|
96
|
+
if (m === '.' || m.length === 0)
|
|
97
|
+
return [];
|
|
98
|
+
const out = new Set();
|
|
99
|
+
for (const seg of m.split('/').filter(Boolean)) {
|
|
100
|
+
const bare = seg.replace(/^@/, '');
|
|
101
|
+
if (bare.length > 2)
|
|
102
|
+
out.add(bare);
|
|
103
|
+
for (const part of bare.split('-'))
|
|
104
|
+
if (part.length > 2)
|
|
105
|
+
out.add(part);
|
|
106
|
+
}
|
|
107
|
+
return [...out];
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Does this cited URL document this package? Host OR path, deliberately: wouter's page is
|
|
111
|
+
* github.com/molefrog/wouter#readme and @hono/zod-validator's is
|
|
112
|
+
* github.com/honojs/middleware/tree/main/packages/zod-validator — both real associations that
|
|
113
|
+
* live in the path. A host-only rule scored 0 tasks for either.
|
|
114
|
+
*/
|
|
115
|
+
export function urlDocumentsPackage(url, pkg) {
|
|
116
|
+
const toks = packageTokens(pkg);
|
|
117
|
+
if (toks.length === 0)
|
|
118
|
+
return false;
|
|
119
|
+
let scope;
|
|
120
|
+
try {
|
|
121
|
+
const u = new URL(url);
|
|
122
|
+
scope = `${u.hostname} ${u.pathname}`;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const inUrl = new Set(scope
|
|
128
|
+
.toLowerCase()
|
|
129
|
+
.split(/[^a-z0-9]+/)
|
|
130
|
+
.filter(Boolean));
|
|
131
|
+
return toks.some(t => inUrl.has(t));
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Cited URLs that document a package this task touches, most relevant first.
|
|
135
|
+
*
|
|
136
|
+
* `packages` is ordered by relevance by the CALLER — the task's own named dependencies
|
|
137
|
+
* before the rest of the manifest — and a URL inherits the rank of the earliest package it
|
|
138
|
+
* documents. A URL documenting no supplied package is dropped entirely rather than ranked
|
|
139
|
+
* last: the design cites pages for the whole project, and a task about the router has no use
|
|
140
|
+
* for the image-processing reference.
|
|
141
|
+
*/
|
|
142
|
+
export function rankSpecUrls(urls, packages) {
|
|
143
|
+
const ranked = [];
|
|
144
|
+
for (const url of urls) {
|
|
145
|
+
const matched = packages.filter(p => urlDocumentsPackage(url, p));
|
|
146
|
+
if (matched.length === 0)
|
|
147
|
+
continue;
|
|
148
|
+
ranked.push({ r: { url, packages: matched }, rank: packages.indexOf(matched[0]) });
|
|
149
|
+
}
|
|
150
|
+
ranked.sort((a, b) => a.rank - b.rank || a.r.url.localeCompare(b.r.url));
|
|
151
|
+
return ranked.slice(0, MAX_SPEC_URLS).map(x => x.r);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The prompt block, or '' when nothing is cited for anything this task uses.
|
|
155
|
+
*
|
|
156
|
+
* NOTE FOR ANYONE EDITING THE GUARD CLAUSE BELOW: scripts/live-spec-url-fetch-ab.ts strips
|
|
157
|
+
* this lever for its baseline arm by replacing that exact statement in the compiled output,
|
|
158
|
+
* and asserts it occurs exactly once. Reshaping it (an early `return` on the caller's side, a
|
|
159
|
+
* ternary, a different variable name) silently makes both arms identical, which reads as
|
|
160
|
+
* "the lever had no effect". Update the harness's anchor in the same commit.
|
|
161
|
+
*/
|
|
162
|
+
export function buildSpecUrlBlock(urls, packages) {
|
|
163
|
+
const ranked = rankSpecUrls(urls, packages);
|
|
164
|
+
if (ranked.length === 0)
|
|
165
|
+
return '';
|
|
166
|
+
// Grouped under the package rather than one package-name-per-URL: five consecutive lines
|
|
167
|
+
// reading "hono" is the shape a reader — and a small model — skims past, and the grouping
|
|
168
|
+
// is also what makes the ranking legible as a ranking.
|
|
169
|
+
const byPackage = new Map();
|
|
170
|
+
for (const r of ranked) {
|
|
171
|
+
const key = r.packages[0];
|
|
172
|
+
byPackage.set(key, [...(byPackage.get(key) ?? []), r.url]);
|
|
173
|
+
}
|
|
174
|
+
const rows = [...byPackage]
|
|
175
|
+
.map(([pkg, us]) => ` ${pkg}\n${us.map(u => ` ${u}`).join('\n')}`)
|
|
176
|
+
.join('\n');
|
|
177
|
+
return ("SPEC-CITED DOCUMENTATION — this project's own design document names these pages, by "
|
|
178
|
+
+ "URL, for packages this task uses. They are the project's chosen references, so they "
|
|
179
|
+
+ 'OUTRANK any page you would pick from a search result.\n'
|
|
180
|
+
+ 'WHEN TO USE THEM: the moment a `pi-worker-docs` answer for one of these packages '
|
|
181
|
+
+ 'does not actually answer your question — including when it hands you a type '
|
|
182
|
+
+ 'signature instead of behaviour — call `pi-worker-fetch(<the URL below>)` and re-ask '
|
|
183
|
+
+ 'the same question. Do NOT search for a page first, and do NOT fill the gap from '
|
|
184
|
+
+ 'memory.\n'
|
|
185
|
+
+ `${rows}\n`
|
|
186
|
+
+ 'NOT AN ALLOW-LIST: these are ranked first, not exclusive. If your question is about '
|
|
187
|
+
+ 'something these pages do not cover, use pi-worker-search / pi-worker-fetch on '
|
|
188
|
+
+ 'whatever page does.\n\n');
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The manifest dependencies this task's refined text actually names — the relevance signal
|
|
192
|
+
* the ranking needs, and the reason the block does not simply list all 21 cited URLs.
|
|
193
|
+
*
|
|
194
|
+
* WHY NOT extractEnrichTargets. That parser is tuned for the EXTERNAL-DEPENDENCIES section
|
|
195
|
+
* and, run over TASK_0027's refined text, returns ["any", "api", "hc", "package.json",
|
|
196
|
+
* "tsconfig.json", "eslint.config.js"] — and NOT "hono". Ranking off that would drop the one
|
|
197
|
+
* package the task is about while promoting URL noise. The manifest is the authoritative list
|
|
198
|
+
* of what the project actually depends on; matching it against the task text is both
|
|
199
|
+
* deterministic and impossible to fool with prose.
|
|
200
|
+
*
|
|
201
|
+
* Word-boundary matched, so `react` does not match inside `react-dom` or `@types/react`, and
|
|
202
|
+
* a package genuinely named twice is still listed once.
|
|
203
|
+
*/
|
|
204
|
+
export function mentionedPackages(refined, manifest) {
|
|
205
|
+
const text = refined.toLowerCase();
|
|
206
|
+
const out = [];
|
|
207
|
+
for (const dep of manifest) {
|
|
208
|
+
const d = dep.toLowerCase();
|
|
209
|
+
const esc = d.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
210
|
+
if (new RegExp(`(^|[^a-z0-9@/._-])${esc}([^a-z0-9-]|$)`, 'i').test(text))
|
|
211
|
+
out.push(dep);
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic detector for the F-2 shape: a pi-worker-docs answer that RESTATES a type
|
|
3
|
+
* signature or declaration for a USAGE question, without ever saying what the API DOES.
|
|
4
|
+
*
|
|
5
|
+
* THE FATAL CASE, from mx5 run 15 (research-cache.json, verbatim):
|
|
6
|
+
*
|
|
7
|
+
* query : "hc factory function signature base url parameter types exported from hono/client"
|
|
8
|
+
* answer: "The `hc` factory function exported from `hono/client` accepts a generic type
|
|
9
|
+
* `T` extending `Hono`, an optional string prefix type `Prefix` (defaulting to
|
|
10
|
+
* `string`), and takes two parameters: `baseUrl` of type `Prefix` and an optional
|
|
11
|
+
* `options` of type `ClientRequestOptions`. It returns a
|
|
12
|
+
* `UnionToIntersection<Client<T, Prefix>>`."
|
|
13
|
+
*
|
|
14
|
+
* Every clause is type-level. "`baseUrl` of type `Prefix`" names the parameter's TYPE — and
|
|
15
|
+
* `Prefix` is itself an opaque type variable — while saying nothing about what `baseUrl`
|
|
16
|
+
* MEANS (is it an origin, or a mount prefix?). Escalation to search/fetch never fired
|
|
17
|
+
* because the answer looked complete: it named the very parameter asked about. worker:context
|
|
18
|
+
* then filled the semantic gap from memory and shipped `hc<AppType>('/api')`, so every request
|
|
19
|
+
* went to `/api/api/...` ⇒ 404 ⇒ the product's entire API surface was dead (F-2 → F-1).
|
|
20
|
+
*
|
|
21
|
+
* The lever (PROMPT 2 DO item 1): when a docs answer for a usage question is ONLY a
|
|
22
|
+
* signature/declaration restatement with no behavioural statement, treat it as UNANSWERED so
|
|
23
|
+
* the caller escalates (follow the `@see` pointer / fetch the spec-cited URL) instead of
|
|
24
|
+
* accepting a type as the answer.
|
|
25
|
+
*
|
|
26
|
+
* ── PRECISION/RECALL TRADEOFF (chosen deliberately; documented per the task) ────────────────
|
|
27
|
+
* A FALSE POSITIVE here (flagging a real answer as type-only) forces a needless escalation:
|
|
28
|
+
* wall-clock cost against PROMPT 2 invariant 1, which caps added child spawns. A false
|
|
29
|
+
* NEGATIVE (missing a type-only answer) merely leaves the pre-existing bug unfixed for that
|
|
30
|
+
* one borderline case. So this detector is tuned for HIGH PRECISION on real answers, accepting
|
|
31
|
+
* low recall — "better to miss a borderline type-only answer than to flag a real one." Three
|
|
32
|
+
* independent gates must ALL hold before an answer is called type-only; any one failing clears
|
|
33
|
+
* it. Calibrated against the run-15 corpus: of the 149 valid (non-"unclear") pi-worker-docs
|
|
34
|
+
* answers, this rule flags EXACTLY ONE — the recorded `hc` case — and clears the other 148,
|
|
35
|
+
* including every legitimate signature answer to an explicit "give me the type/signature"
|
|
36
|
+
* question (bun.password.hash, toBuffer, BuildOutput, …). See type-only-answer.test.ts.
|
|
37
|
+
*
|
|
38
|
+
* THE THREE GATES (all required):
|
|
39
|
+
* 1. USAGE QUESTION. The question must seek usage/semantics — "how", "use", "work", "chain",
|
|
40
|
+
* "example", "call", "rpc", "mean", or it names a usage concept whose meaning is being
|
|
41
|
+
* sought ("base url"). A question that asks only for a TYPE / SIGNATURE / DEFINITION /
|
|
42
|
+
* FIELDS is legitimately answered by a signature, so it is NOT gated in. (The recorded hc
|
|
43
|
+
* query is signature-shaped but names "base url", the concept whose meaning was needed.)
|
|
44
|
+
* 2. NO BEHAVIOURAL CONTENT IN PROSE. The answer must contain no statement of what the API
|
|
45
|
+
* DOES: no usage verb ("use/call/pass/import"), no runtime-effect verb ("executes/
|
|
46
|
+
* prepends/enables/wraps"), no concrete usage example, no semantic meaning ("means/
|
|
47
|
+
* represents/so pass"), no concrete default VALUE or value RANGE ("default of 80", "from
|
|
48
|
+
* 1 to 100"). Any one clears. This scan runs over `proseOf(answer)`, NOT the raw answer:
|
|
49
|
+
* a verb that is really a method name inside a declaration ("methods route(url, handler)
|
|
50
|
+
* and use(...handlers)") is an identifier, not behaviour, and must not clear the answer.
|
|
51
|
+
* 3. SIGNATURE RESTATEMENT. The answer must actually be a declaration/signature restatement —
|
|
52
|
+
* "of type", "takes N parameters", "accepts", "returns a…", "extends", "interface",
|
|
53
|
+
* "declare const/function". Without this it is not type-only, it is just terse prose.
|
|
54
|
+
*
|
|
55
|
+
* An explicit "unclear from this package/page" is NOT type-only — it is the HONEST non-answer,
|
|
56
|
+
* already handled by the escalation path (PROMPT 2 DO item 2 escalates BOTH). It is cleared
|
|
57
|
+
* here with a distinct reason so the caller can route it through the existing unclear channel.
|
|
58
|
+
*
|
|
59
|
+
* Pure and side-effect free; unit-tested in type-only-answer.test.ts against real run-15 text.
|
|
60
|
+
*/
|
|
61
|
+
/** The verdict, with a human-readable reason for logging and for the escalation channel. */
|
|
62
|
+
export interface TypeOnlyVerdict {
|
|
63
|
+
/** True iff the answer restates a signature for a usage question with no behaviour. */
|
|
64
|
+
typeOnly: boolean;
|
|
65
|
+
/** Why — names the gate that decided it, quoting the matched marker where useful. */
|
|
66
|
+
reason: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Strip signature/declaration fragments so the behavioural scan sees only PROSE.
|
|
70
|
+
*
|
|
71
|
+
* A behavioural verb is a signal ONLY when it is a verb in prose ("the base URL is
|
|
72
|
+
* prepended", "you use X to Y"), NEVER when it is a method/identifier token inside a
|
|
73
|
+
* declaration. Without this, a bare type-only answer like
|
|
74
|
+
* "It has methods route(url, handler) and use(...handlers)."
|
|
75
|
+
* is wrongly cleared, because `use(...handlers)` matches /\buse\b/ and `handler` matches
|
|
76
|
+
* /handle/ — the F-3(a) router-fixture shape. So before scanning for behaviour we remove:
|
|
77
|
+
* - `name(args)` call fragments, whose args are identifiers, not prose (this is what
|
|
78
|
+
* carries the method-name verbs);
|
|
79
|
+
* - `declare const|function|module|class|namespace …` declaration lines.
|
|
80
|
+
* The SIGNATURE gate still runs against the ORIGINAL answer, so stripping never weakens
|
|
81
|
+
* gate 3 — it only prevents an identifier inside a signature from masquerading as prose.
|
|
82
|
+
* Deliberately conservative: it does NOT strip `: Type` annotations or backtick spans,
|
|
83
|
+
* because a colon or backtick often precedes real behavioural prose ("Usage: call …",
|
|
84
|
+
* "`sql` is prepended …"), and removing those would silently drop behaviour and MANUFACTURE
|
|
85
|
+
* false positives — worse than the recall gap this fixes.
|
|
86
|
+
*/
|
|
87
|
+
export declare function proseOf(answer: string): string;
|
|
88
|
+
/**
|
|
89
|
+
* Decide whether a pi-worker-docs `answer` to `question` is type-only — a signature/declaration
|
|
90
|
+
* restatement with no behavioural statement, for a question that needed usage semantics.
|
|
91
|
+
*
|
|
92
|
+
* @param answer the child's <answer> prose (NOT the version banner or the source excerpt).
|
|
93
|
+
* @param question the query the answer was produced for.
|
|
94
|
+
*/
|
|
95
|
+
export declare function isTypeOnlyAnswer(answer: string, question: string): TypeOnlyVerdict;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic detector for the F-2 shape: a pi-worker-docs answer that RESTATES a type
|
|
3
|
+
* signature or declaration for a USAGE question, without ever saying what the API DOES.
|
|
4
|
+
*
|
|
5
|
+
* THE FATAL CASE, from mx5 run 15 (research-cache.json, verbatim):
|
|
6
|
+
*
|
|
7
|
+
* query : "hc factory function signature base url parameter types exported from hono/client"
|
|
8
|
+
* answer: "The `hc` factory function exported from `hono/client` accepts a generic type
|
|
9
|
+
* `T` extending `Hono`, an optional string prefix type `Prefix` (defaulting to
|
|
10
|
+
* `string`), and takes two parameters: `baseUrl` of type `Prefix` and an optional
|
|
11
|
+
* `options` of type `ClientRequestOptions`. It returns a
|
|
12
|
+
* `UnionToIntersection<Client<T, Prefix>>`."
|
|
13
|
+
*
|
|
14
|
+
* Every clause is type-level. "`baseUrl` of type `Prefix`" names the parameter's TYPE — and
|
|
15
|
+
* `Prefix` is itself an opaque type variable — while saying nothing about what `baseUrl`
|
|
16
|
+
* MEANS (is it an origin, or a mount prefix?). Escalation to search/fetch never fired
|
|
17
|
+
* because the answer looked complete: it named the very parameter asked about. worker:context
|
|
18
|
+
* then filled the semantic gap from memory and shipped `hc<AppType>('/api')`, so every request
|
|
19
|
+
* went to `/api/api/...` ⇒ 404 ⇒ the product's entire API surface was dead (F-2 → F-1).
|
|
20
|
+
*
|
|
21
|
+
* The lever (PROMPT 2 DO item 1): when a docs answer for a usage question is ONLY a
|
|
22
|
+
* signature/declaration restatement with no behavioural statement, treat it as UNANSWERED so
|
|
23
|
+
* the caller escalates (follow the `@see` pointer / fetch the spec-cited URL) instead of
|
|
24
|
+
* accepting a type as the answer.
|
|
25
|
+
*
|
|
26
|
+
* ── PRECISION/RECALL TRADEOFF (chosen deliberately; documented per the task) ────────────────
|
|
27
|
+
* A FALSE POSITIVE here (flagging a real answer as type-only) forces a needless escalation:
|
|
28
|
+
* wall-clock cost against PROMPT 2 invariant 1, which caps added child spawns. A false
|
|
29
|
+
* NEGATIVE (missing a type-only answer) merely leaves the pre-existing bug unfixed for that
|
|
30
|
+
* one borderline case. So this detector is tuned for HIGH PRECISION on real answers, accepting
|
|
31
|
+
* low recall — "better to miss a borderline type-only answer than to flag a real one." Three
|
|
32
|
+
* independent gates must ALL hold before an answer is called type-only; any one failing clears
|
|
33
|
+
* it. Calibrated against the run-15 corpus: of the 149 valid (non-"unclear") pi-worker-docs
|
|
34
|
+
* answers, this rule flags EXACTLY ONE — the recorded `hc` case — and clears the other 148,
|
|
35
|
+
* including every legitimate signature answer to an explicit "give me the type/signature"
|
|
36
|
+
* question (bun.password.hash, toBuffer, BuildOutput, …). See type-only-answer.test.ts.
|
|
37
|
+
*
|
|
38
|
+
* THE THREE GATES (all required):
|
|
39
|
+
* 1. USAGE QUESTION. The question must seek usage/semantics — "how", "use", "work", "chain",
|
|
40
|
+
* "example", "call", "rpc", "mean", or it names a usage concept whose meaning is being
|
|
41
|
+
* sought ("base url"). A question that asks only for a TYPE / SIGNATURE / DEFINITION /
|
|
42
|
+
* FIELDS is legitimately answered by a signature, so it is NOT gated in. (The recorded hc
|
|
43
|
+
* query is signature-shaped but names "base url", the concept whose meaning was needed.)
|
|
44
|
+
* 2. NO BEHAVIOURAL CONTENT IN PROSE. The answer must contain no statement of what the API
|
|
45
|
+
* DOES: no usage verb ("use/call/pass/import"), no runtime-effect verb ("executes/
|
|
46
|
+
* prepends/enables/wraps"), no concrete usage example, no semantic meaning ("means/
|
|
47
|
+
* represents/so pass"), no concrete default VALUE or value RANGE ("default of 80", "from
|
|
48
|
+
* 1 to 100"). Any one clears. This scan runs over `proseOf(answer)`, NOT the raw answer:
|
|
49
|
+
* a verb that is really a method name inside a declaration ("methods route(url, handler)
|
|
50
|
+
* and use(...handlers)") is an identifier, not behaviour, and must not clear the answer.
|
|
51
|
+
* 3. SIGNATURE RESTATEMENT. The answer must actually be a declaration/signature restatement —
|
|
52
|
+
* "of type", "takes N parameters", "accepts", "returns a…", "extends", "interface",
|
|
53
|
+
* "declare const/function". Without this it is not type-only, it is just terse prose.
|
|
54
|
+
*
|
|
55
|
+
* An explicit "unclear from this package/page" is NOT type-only — it is the HONEST non-answer,
|
|
56
|
+
* already handled by the escalation path (PROMPT 2 DO item 2 escalates BOTH). It is cleared
|
|
57
|
+
* here with a distinct reason so the caller can route it through the existing unclear channel.
|
|
58
|
+
*
|
|
59
|
+
* Pure and side-effect free; unit-tested in type-only-answer.test.ts against real run-15 text.
|
|
60
|
+
*/
|
|
61
|
+
/**
|
|
62
|
+
* Question seeks usage/semantics rather than a bare type. A usage concept whose MEANING is
|
|
63
|
+
* being asked about ("base url") counts, because the F-2 defect is exactly a semantics need
|
|
64
|
+
* dressed in signature words. A question that asks only for the "type"/"signature"/"definition"
|
|
65
|
+
* is deliberately NOT here — a signature answer is responsive to it.
|
|
66
|
+
*/
|
|
67
|
+
const USAGE_INTENT = [
|
|
68
|
+
/\bhow\b/i,
|
|
69
|
+
/\buse\b/i,
|
|
70
|
+
/\busing\b/i,
|
|
71
|
+
/\busage\b/i,
|
|
72
|
+
/\bworks?\b/i,
|
|
73
|
+
/\bworking\b/i,
|
|
74
|
+
/\bchain/i,
|
|
75
|
+
/base\s?url/i,
|
|
76
|
+
/\bconnect/i,
|
|
77
|
+
/\brpc\b/i,
|
|
78
|
+
/\bexample/i,
|
|
79
|
+
/\bcall\b/i,
|
|
80
|
+
/\binvoke/i,
|
|
81
|
+
/\bmean/i,
|
|
82
|
+
/\bbehav/i,
|
|
83
|
+
/what\s+does/i,
|
|
84
|
+
/how\s+does/i
|
|
85
|
+
];
|
|
86
|
+
/**
|
|
87
|
+
* Markers that the answer says what the API DOES — a usage instruction, a runtime effect, a
|
|
88
|
+
* concrete example, a meaning, a concrete default value or range. Deliberately GENEROUS: every
|
|
89
|
+
* extra marker here can only SUPPRESS a flag, which is the safe direction for precision.
|
|
90
|
+
*/
|
|
91
|
+
const BEHAVIOURAL = [
|
|
92
|
+
/\buse\b/i,
|
|
93
|
+
/\busing\b/i,
|
|
94
|
+
/\bcall/i,
|
|
95
|
+
/\bpass(es|ed|ing)?\b/i,
|
|
96
|
+
/\bimport/i,
|
|
97
|
+
/\binstantiate/i,
|
|
98
|
+
/\bprovide[sd]?\b/i,
|
|
99
|
+
/\binvoke/i,
|
|
100
|
+
/\bread\b/i,
|
|
101
|
+
/\bwrite\b/i,
|
|
102
|
+
/\bexecut/i,
|
|
103
|
+
/\bsends?\b/i,
|
|
104
|
+
/\bprepend/i,
|
|
105
|
+
/\bappend/i,
|
|
106
|
+
/\bconnect/i,
|
|
107
|
+
/\bhandle/i,
|
|
108
|
+
/\bresolves?\s+to\b/i,
|
|
109
|
+
/\bstarts?\b/i,
|
|
110
|
+
/\bspread/i,
|
|
111
|
+
/\bautomatically\b/i,
|
|
112
|
+
/\breplac/i,
|
|
113
|
+
/\benabl/i,
|
|
114
|
+
/\bdisabl/i,
|
|
115
|
+
/\bwrap/i,
|
|
116
|
+
/\bperform/i,
|
|
117
|
+
/\bmanag/i,
|
|
118
|
+
/\bcontrol/i,
|
|
119
|
+
/\baccess/i,
|
|
120
|
+
/\bcreat/i,
|
|
121
|
+
/\ballow/i,
|
|
122
|
+
/\bcontain/i,
|
|
123
|
+
/\bregister/i,
|
|
124
|
+
/\bmount/i,
|
|
125
|
+
/\bnavigat/i,
|
|
126
|
+
/\btrigger/i,
|
|
127
|
+
/\bappl(y|ies)/i,
|
|
128
|
+
/\bmeans\b/i,
|
|
129
|
+
/\brepresents/i,
|
|
130
|
+
/so\s+pass/i,
|
|
131
|
+
/for\s+setup/i,
|
|
132
|
+
/used\s+to/i,
|
|
133
|
+
/used\s+for/i,
|
|
134
|
+
/in\s+order\s+to/i,
|
|
135
|
+
/for\s+\w+ing\b/i,
|
|
136
|
+
/e\.g\./i,
|
|
137
|
+
/for\s+example/i,
|
|
138
|
+
/default\s+of/i,
|
|
139
|
+
/defaults?\s+to/i,
|
|
140
|
+
/with\s+a\s+default/i,
|
|
141
|
+
/from\s+\d+\s+to\s+\d+/i,
|
|
142
|
+
/when\s+using/i,
|
|
143
|
+
/you\s+can/i,
|
|
144
|
+
/you\s+provide/i,
|
|
145
|
+
/you\s+get/i,
|
|
146
|
+
/gives?\s+access/i,
|
|
147
|
+
/to\s+(enable|perform|run|execute|create|manage|handle)\b/i,
|
|
148
|
+
/\bconfigur/i,
|
|
149
|
+
/\bschema/i,
|
|
150
|
+
/\bvalidat/i
|
|
151
|
+
];
|
|
152
|
+
/** Markers that the answer is a declaration/signature restatement. */
|
|
153
|
+
const SIGNATURE = [
|
|
154
|
+
/\bof\s+type\b/i,
|
|
155
|
+
/\btakes?\s+(a|an|two|three|one|four)\b/i,
|
|
156
|
+
/\baccepts?\b/i,
|
|
157
|
+
/\breturns?\s+(a|an|`|the|void|Promise|<|\w+<)/i,
|
|
158
|
+
/\bsignature\s+(is|:)/i,
|
|
159
|
+
/\bextends\b/i,
|
|
160
|
+
/\binterface\b/i,
|
|
161
|
+
/\bgeneric\s+type\b/i,
|
|
162
|
+
/\boverload/i,
|
|
163
|
+
/\bparameters?:/i,
|
|
164
|
+
/\bconstructor\s+accepts/i,
|
|
165
|
+
/declare\s+(const|function)/i
|
|
166
|
+
];
|
|
167
|
+
/** Explicit non-answer the docs child emits when the package cannot answer. */
|
|
168
|
+
const UNCLEAR = /\bunclear\s+from\s+this\s+(package|page)\b/i;
|
|
169
|
+
function firstMatch(text, patterns) {
|
|
170
|
+
for (const p of patterns) {
|
|
171
|
+
const m = p.exec(text);
|
|
172
|
+
if (m)
|
|
173
|
+
return m[0];
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Strip signature/declaration fragments so the behavioural scan sees only PROSE.
|
|
179
|
+
*
|
|
180
|
+
* A behavioural verb is a signal ONLY when it is a verb in prose ("the base URL is
|
|
181
|
+
* prepended", "you use X to Y"), NEVER when it is a method/identifier token inside a
|
|
182
|
+
* declaration. Without this, a bare type-only answer like
|
|
183
|
+
* "It has methods route(url, handler) and use(...handlers)."
|
|
184
|
+
* is wrongly cleared, because `use(...handlers)` matches /\buse\b/ and `handler` matches
|
|
185
|
+
* /handle/ — the F-3(a) router-fixture shape. So before scanning for behaviour we remove:
|
|
186
|
+
* - `name(args)` call fragments, whose args are identifiers, not prose (this is what
|
|
187
|
+
* carries the method-name verbs);
|
|
188
|
+
* - `declare const|function|module|class|namespace …` declaration lines.
|
|
189
|
+
* The SIGNATURE gate still runs against the ORIGINAL answer, so stripping never weakens
|
|
190
|
+
* gate 3 — it only prevents an identifier inside a signature from masquerading as prose.
|
|
191
|
+
* Deliberately conservative: it does NOT strip `: Type` annotations or backtick spans,
|
|
192
|
+
* because a colon or backtick often precedes real behavioural prose ("Usage: call …",
|
|
193
|
+
* "`sql` is prepended …"), and removing those would silently drop behaviour and MANUFACTURE
|
|
194
|
+
* false positives — worse than the recall gap this fixes.
|
|
195
|
+
*/
|
|
196
|
+
export function proseOf(answer) {
|
|
197
|
+
return answer
|
|
198
|
+
.replace(/[\w$][\w$.]*\s*\([^)]*\)/g, ' ')
|
|
199
|
+
.replace(/\bdeclare\s+(const|function|module|class|namespace)\b[^.\n]*/gi, ' ')
|
|
200
|
+
.replace(/\s{2,}/g, ' ')
|
|
201
|
+
.trim();
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Decide whether a pi-worker-docs `answer` to `question` is type-only — a signature/declaration
|
|
205
|
+
* restatement with no behavioural statement, for a question that needed usage semantics.
|
|
206
|
+
*
|
|
207
|
+
* @param answer the child's <answer> prose (NOT the version banner or the source excerpt).
|
|
208
|
+
* @param question the query the answer was produced for.
|
|
209
|
+
*/
|
|
210
|
+
export function isTypeOnlyAnswer(answer, question) {
|
|
211
|
+
const a = answer.trim();
|
|
212
|
+
const q = question.trim();
|
|
213
|
+
if (a.length === 0) {
|
|
214
|
+
return { typeOnly: false, reason: 'empty answer' };
|
|
215
|
+
}
|
|
216
|
+
if (UNCLEAR.test(a)) {
|
|
217
|
+
return {
|
|
218
|
+
typeOnly: false,
|
|
219
|
+
reason: 'explicit "unclear" non-answer — routed through the existing unclear/escalation path, not type-only'
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const usage = firstMatch(q, USAGE_INTENT);
|
|
223
|
+
if (usage === null) {
|
|
224
|
+
return {
|
|
225
|
+
typeOnly: false,
|
|
226
|
+
reason: 'question requests a type/signature/definition, not usage semantics — a signature answer is responsive'
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
// Scan for behavioural verbs over the PROSE only — a verb that is really a method name
|
|
230
|
+
// inside a signature (`use(...handlers)`) must not read as a statement of behaviour.
|
|
231
|
+
const behavioural = firstMatch(proseOf(a), BEHAVIOURAL);
|
|
232
|
+
if (behavioural !== null) {
|
|
233
|
+
return {
|
|
234
|
+
typeOnly: false,
|
|
235
|
+
reason: `answer states behaviour/usage (matched "${behavioural}") — not type-only`
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const signature = firstMatch(a, SIGNATURE);
|
|
239
|
+
if (signature === null) {
|
|
240
|
+
return {
|
|
241
|
+
typeOnly: false,
|
|
242
|
+
reason: 'answer is not a signature/declaration restatement'
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
typeOnly: true,
|
|
247
|
+
reason: `usage question answered only with a signature/declaration restatement (matched "${signature}") and no behavioural statement`
|
|
248
|
+
};
|
|
249
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { fetchAndClean as defaultFetchAndClean } from './html-clean.js';
|
|
2
2
|
import { type SpawnFn } from '../shared/child-process.js';
|
|
3
|
+
import { type ExcerptVerification } from '../shared/child-output.js';
|
|
4
|
+
/** The exact non-answers the child is instructed to emit, matched at the tool layer. */
|
|
5
|
+
export declare const UNCLEAR_ANSWER = "unclear from this page";
|
|
6
|
+
export declare const NOT_COVERED_ANSWER = "not covered by this page";
|
|
3
7
|
export interface FetchRawInput {
|
|
4
8
|
url: string;
|
|
5
9
|
signal?: AbortSignal;
|
|
@@ -11,6 +15,22 @@ export interface FetchRawResult {
|
|
|
11
15
|
title: string;
|
|
12
16
|
}
|
|
13
17
|
export declare function fetchRaw(input: FetchRawInput): Promise<FetchRawResult>;
|
|
18
|
+
/**
|
|
19
|
+
* How the page content is turned into the child's prompt. Injectable so an A/B harness can
|
|
20
|
+
* run the shipped strategy against a frozen legacy one in the SAME process, and assert per
|
|
21
|
+
* rep that the two arms differ by exactly the lever (see scripts/live-fetch-abstention-ab.ts).
|
|
22
|
+
* Production always uses {@link shippedStrategy}.
|
|
23
|
+
*/
|
|
24
|
+
export interface PromptStrategy {
|
|
25
|
+
selectContent(markdown: string, requestedUrl: string): SelectedContent;
|
|
26
|
+
buildPrompt(args: {
|
|
27
|
+
query: string;
|
|
28
|
+
url: string;
|
|
29
|
+
title: string;
|
|
30
|
+
content: string;
|
|
31
|
+
section?: string;
|
|
32
|
+
}): string;
|
|
33
|
+
}
|
|
14
34
|
export interface FetchFocusedInput {
|
|
15
35
|
url: string;
|
|
16
36
|
query: string;
|
|
@@ -18,17 +38,47 @@ export interface FetchFocusedInput {
|
|
|
18
38
|
signal?: AbortSignal;
|
|
19
39
|
fetchAndClean?: typeof defaultFetchAndClean;
|
|
20
40
|
spawn?: SpawnFn;
|
|
41
|
+
/** Defaults to {@link shippedStrategy}; overridden only by the A/B harness. */
|
|
42
|
+
strategy?: PromptStrategy;
|
|
21
43
|
}
|
|
22
44
|
export interface FetchFocusedResult {
|
|
23
45
|
answer: string;
|
|
24
46
|
excerpt?: string;
|
|
25
47
|
excerptVerified?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* The page did not cover the asked-about version/topic — a DISTINCT outcome from
|
|
50
|
+
* `unclear from this page`, so the caller can pick a different URL instead of treating
|
|
51
|
+
* "page has no answer" and "answer is ambiguous" as the same thing (PROMPT-3 item 3).
|
|
52
|
+
*/
|
|
53
|
+
coverageMiss: boolean;
|
|
54
|
+
/** The #fragment slug that was anchored, when the URL carried one and it was located. */
|
|
55
|
+
anchoredSection?: string;
|
|
56
|
+
/** Retained evidence for a false `excerptVerified`, so it is diagnosable without re-fetch. */
|
|
57
|
+
excerptCheck?: ExcerptVerification;
|
|
58
|
+
/** The prompt actually handed to the child — the A/B asserts surgery against this per rep. */
|
|
59
|
+
assembledPrompt: string;
|
|
26
60
|
childExitCode: number;
|
|
27
61
|
aborted: boolean;
|
|
28
62
|
stderr: string;
|
|
29
63
|
stdout: string;
|
|
30
64
|
}
|
|
31
65
|
export declare function fetchFocused(input: FetchFocusedInput): Promise<FetchFocusedResult>;
|
|
66
|
+
export interface SelectedContent {
|
|
67
|
+
content: string;
|
|
68
|
+
/** The #fragment slug that was anchored, if the URL carried one AND it was located. */
|
|
69
|
+
section?: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Slice a markdown page down to the section its heading-anchor #fragment names — the deep
|
|
73
|
+
* link the caller actually asked for — instead of head-truncating the whole page and losing
|
|
74
|
+
* a section that sits past the head window (PROMPT-3 item 2). Falls back to {@link truncate}
|
|
75
|
+
* when the URL has no fragment, or the fragment names no heading in THIS content (e.g. the
|
|
76
|
+
* page changed since the link was captured), so a fragment we cannot honour never degrades a
|
|
77
|
+
* page we could otherwise read.
|
|
78
|
+
*/
|
|
79
|
+
export declare function selectContent(markdown: string, requestedUrl: string): SelectedContent;
|
|
80
|
+
/** The shipped strategy: fragment-aware selection + the recalibrated prompt. */
|
|
81
|
+
export declare const shippedStrategy: PromptStrategy;
|
|
32
82
|
export declare function formatResultText(parsed: {
|
|
33
83
|
answer: string;
|
|
34
84
|
excerpt?: string;
|