@mjasnikovs/pi-task 0.40.7 → 0.40.10
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 -5
- package/dist/shared/child-output.js +56 -10
- package/dist/task/deprecated-constraint.d.ts +68 -0
- package/dist/task/deprecated-constraint.js +262 -0
- package/dist/task/phases.d.ts +9 -6
- package/dist/task/phases.js +28 -14
- package/dist/workers/abstention.js +2 -0
- package/dist/workers/docs-ecosystems.d.ts +15 -2
- package/dist/workers/docs-ecosystems.js +37 -2
- package/dist/workers/docs-index.js +11 -10
- package/dist/workers/docs-lookup.js +1 -1
- package/dist/workers/eco-cargo.d.ts +34 -0
- package/dist/workers/eco-cargo.js +220 -0
- package/dist/workers/eco-hackage.d.ts +5 -21
- package/dist/workers/eco-hackage.js +35 -3
- package/dist/workers/export-gap.d.ts +26 -0
- package/dist/workers/export-gap.js +1 -0
- package/dist/workers/pi-worker-fetch.js +1 -1
- package/package.json +1 -1
|
@@ -41,17 +41,33 @@ export interface ExcerptVerification {
|
|
|
41
41
|
contentLength: number;
|
|
42
42
|
/** The whitespace-normalised excerpt that was searched for. */
|
|
43
43
|
normalisedExcerpt: string;
|
|
44
|
+
/**
|
|
45
|
+
* How many verbatim spans of the source the excerpt is made of — 1 when it
|
|
46
|
+
* verified, more when the child stitched it. Over every unverified excerpt in
|
|
47
|
+
* five live runs it ran 2 to 11, with nothing missing.
|
|
48
|
+
*/
|
|
49
|
+
verbatimSpans: number;
|
|
50
|
+
/** Words in no span of the source at all. This, not `verified`, is what a
|
|
51
|
+
* fabrication looks like. */
|
|
52
|
+
absent: string[];
|
|
44
53
|
}
|
|
45
54
|
export declare function verifyExcerpt(excerpt: string, content: string): ExcerptVerification;
|
|
46
55
|
/**
|
|
47
56
|
* Format the child's parsed output with a header and optional excerpt block.
|
|
48
57
|
*
|
|
49
|
-
* The
|
|
50
|
-
*
|
|
51
|
-
* an
|
|
52
|
-
*
|
|
58
|
+
* The note is prepended only on the excerpt path, and only when something was
|
|
59
|
+
* actually checked. With no excerpt the function returns before it is built, and
|
|
60
|
+
* an undefined verification — nothing was checked — prints nothing either. So a
|
|
61
|
+
* note means "checked", never "not checked".
|
|
62
|
+
*
|
|
63
|
+
* Two different findings, because they mean different things to the worker that
|
|
64
|
+
* reads this. An excerpt the source does not contain a word of is a possible
|
|
65
|
+
* fabrication. An excerpt assembled from several real spans is a stitched quote,
|
|
66
|
+
* which is what the extraction prompt produces — and calling that a possible
|
|
67
|
+
* hallucination was wrong on 21 of 21 measured cases, on a fifth of every run's
|
|
68
|
+
* answers. See "Defect 18" in DOC_REGRESSINONS.md.
|
|
53
69
|
*/
|
|
54
70
|
export declare function formatResultText(header: string, parsed: {
|
|
55
71
|
answer: string;
|
|
56
72
|
excerpt?: string;
|
|
57
|
-
},
|
|
73
|
+
}, check: ExcerptVerification | undefined): string;
|
|
@@ -44,6 +44,40 @@ export function isExcerptInContent(excerpt, content) {
|
|
|
44
44
|
return false;
|
|
45
45
|
return normaliseWhitespace(content).includes(ne);
|
|
46
46
|
}
|
|
47
|
+
/** The child writes these to mark its own join, so they are not source text and
|
|
48
|
+
* not evidence of invention either. */
|
|
49
|
+
const ELISION = /^(?:\.{3}|\u2026|\/\/\s*\.{3})$/;
|
|
50
|
+
/**
|
|
51
|
+
* Cover the excerpt with the longest runs the source actually contains, greedily.
|
|
52
|
+
*
|
|
53
|
+
* `verified` asks whether the excerpt is ONE such run. This asks what it is made
|
|
54
|
+
* of, which is the difference between a quote assembled from four real places and
|
|
55
|
+
* a quote with a word nobody wrote. The warning needs the second question; it had
|
|
56
|
+
* only ever asked the first.
|
|
57
|
+
*/
|
|
58
|
+
function coverBySource(normalisedExcerpt, normalisedContent) {
|
|
59
|
+
const words = normalisedExcerpt.length === 0 ? [] : normalisedExcerpt.split(' ');
|
|
60
|
+
const absent = [];
|
|
61
|
+
let verbatimSpans = 0;
|
|
62
|
+
let i = 0;
|
|
63
|
+
while (i < words.length) {
|
|
64
|
+
let run = 0;
|
|
65
|
+
while (i + run < words.length
|
|
66
|
+
&& normalisedContent.includes(words.slice(i, i + run + 1).join(' '))) {
|
|
67
|
+
run++;
|
|
68
|
+
}
|
|
69
|
+
if (run === 0) {
|
|
70
|
+
if (!ELISION.test(words[i]))
|
|
71
|
+
absent.push(words[i]);
|
|
72
|
+
i++;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
verbatimSpans++;
|
|
76
|
+
i += run;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { verbatimSpans, absent };
|
|
80
|
+
}
|
|
47
81
|
export function verifyExcerpt(excerpt, content) {
|
|
48
82
|
const nc = normaliseWhitespace(content);
|
|
49
83
|
const ne = normaliseWhitespace(excerpt);
|
|
@@ -52,24 +86,36 @@ export function verifyExcerpt(excerpt, content) {
|
|
|
52
86
|
verified: isExcerptInContent(excerpt, content),
|
|
53
87
|
contentSha256: createHash('sha256').update(nc).digest('hex'),
|
|
54
88
|
contentLength: nc.length,
|
|
55
|
-
normalisedExcerpt: ne
|
|
89
|
+
normalisedExcerpt: ne,
|
|
90
|
+
...coverBySource(ne, nc)
|
|
56
91
|
};
|
|
57
92
|
}
|
|
58
93
|
/**
|
|
59
94
|
* Format the child's parsed output with a header and optional excerpt block.
|
|
60
95
|
*
|
|
61
|
-
* The
|
|
62
|
-
*
|
|
63
|
-
* an
|
|
64
|
-
*
|
|
96
|
+
* The note is prepended only on the excerpt path, and only when something was
|
|
97
|
+
* actually checked. With no excerpt the function returns before it is built, and
|
|
98
|
+
* an undefined verification — nothing was checked — prints nothing either. So a
|
|
99
|
+
* note means "checked", never "not checked".
|
|
100
|
+
*
|
|
101
|
+
* Two different findings, because they mean different things to the worker that
|
|
102
|
+
* reads this. An excerpt the source does not contain a word of is a possible
|
|
103
|
+
* fabrication. An excerpt assembled from several real spans is a stitched quote,
|
|
104
|
+
* which is what the extraction prompt produces — and calling that a possible
|
|
105
|
+
* hallucination was wrong on 21 of 21 measured cases, on a fifth of every run's
|
|
106
|
+
* answers. See "Defect 18" in DOC_REGRESSINONS.md.
|
|
65
107
|
*/
|
|
66
|
-
export function formatResultText(header, parsed,
|
|
108
|
+
export function formatResultText(header, parsed, check) {
|
|
67
109
|
if (!parsed.excerpt) {
|
|
68
110
|
return header ? `${header}\n\n${parsed.answer}` : parsed.answer;
|
|
69
111
|
}
|
|
70
112
|
const quote = parsed.excerpt.replace(/\n/g, '\n> ');
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
113
|
+
let note = '';
|
|
114
|
+
if (check && !check.verified) {
|
|
115
|
+
note =
|
|
116
|
+
check.absent.length > 0 ?
|
|
117
|
+
'WARNING: cited excerpt not found verbatim in source content — the child pi may have paraphrased or hallucinated.\n\n'
|
|
118
|
+
: `NOTE: cited excerpt is stitched from ${check.verbatimSpans} separate spans of the source; every span is verbatim.\n\n`;
|
|
119
|
+
}
|
|
120
|
+
return `${note}${header}\n\n${parsed.answer}\n\nSource excerpt:\n> ${quote}`;
|
|
75
121
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sibling of refuted-constraint.ts, for the other half of the same failure.
|
|
3
|
+
*
|
|
4
|
+
* That pass deletes a refine-invented DEPENDENCY the research says is not needed.
|
|
5
|
+
* This one deletes a refine-invented API or package the research says is
|
|
6
|
+
* DEPRECATED. Defect 14: refine writes an API example into CONSTRAINTS from
|
|
7
|
+
* memory before research runs, research then names that exact form as superseded,
|
|
8
|
+
* and compose prefers the constraint — so the run ships the deprecated call with
|
|
9
|
+
* the correct answer sitting one section above it in the same file.
|
|
10
|
+
*
|
|
11
|
+
* Everything about the sibling's discipline carries over unchanged: the
|
|
12
|
+
* correction is SUBTRACTIVE (an appended one loses to the text it contradicts),
|
|
13
|
+
* the match is lexical, and an owned-requirement line is never touched.
|
|
14
|
+
*
|
|
15
|
+
* Two things are deliberately NOT shared with the sibling, which is why this is a
|
|
16
|
+
* second pass and not a widening of the first:
|
|
17
|
+
*
|
|
18
|
+
* - it reads research APIS as well as CONTEXT, and two of the three recorded
|
|
19
|
+
* fires are in APIS;
|
|
20
|
+
* - its token class admits an UN-BACKTICKED API expression (`z.string().email()`),
|
|
21
|
+
* which `isPackageToken` refuses on purpose to keep the dependency channel
|
|
22
|
+
* narrow. Widening that channel to reach this one would loosen it for every
|
|
23
|
+
* dependency drop.
|
|
24
|
+
*/
|
|
25
|
+
export type Deprecation = {
|
|
26
|
+
/** The deprecated token, as it must be matched inside the constraint. */
|
|
27
|
+
token: string;
|
|
28
|
+
/** Index into the refined prompt's lines. */
|
|
29
|
+
line: number;
|
|
30
|
+
/** The refine CONSTRAINTS line, verbatim, before the drop. */
|
|
31
|
+
constraint: string;
|
|
32
|
+
/** The research line that deprecates it, verbatim. */
|
|
33
|
+
research: string;
|
|
34
|
+
/** `apis-symbol` or `marker-adjacent` — which rule fired. */
|
|
35
|
+
rule: string;
|
|
36
|
+
};
|
|
37
|
+
/** The tokens one research line deprecates, or an empty list. */
|
|
38
|
+
export declare function deprecatedTokens(line: string): string[];
|
|
39
|
+
/**
|
|
40
|
+
* Find every (constraint line, research line, deprecated token) triple. `refined`
|
|
41
|
+
* is the refined prompt and `research` the research output — the exact strings
|
|
42
|
+
* compose is handed.
|
|
43
|
+
*/
|
|
44
|
+
export declare function detectDeprecations(refined: string, research: string): Deprecation[];
|
|
45
|
+
/**
|
|
46
|
+
* Delete one token from a constraint line, backticked or bare, plus one adjacent
|
|
47
|
+
* list separator. Then collapse the wreckage the deletion left: an emptied
|
|
48
|
+
* `(e.g. )`, doubled spaces, a space stranded before punctuation.
|
|
49
|
+
*
|
|
50
|
+
* Every step removes characters and adds none, so the result stays a character
|
|
51
|
+
* subsequence of the input — the invariant the whole approach rests on.
|
|
52
|
+
*
|
|
53
|
+
* The example-parenthetical collapse requires a literal `e.g.`/`i.e.` rather than
|
|
54
|
+
* matching any emptied `()`. A bare `\(\s*\)` also matches the call parens inside
|
|
55
|
+
* `z.string()` elsewhere on the same line, which would silently corrupt a
|
|
56
|
+
* constraint this pass is not even firing on.
|
|
57
|
+
*
|
|
58
|
+
* Returns null when nothing but boilerplate is left, which the caller turns into
|
|
59
|
+
* a whole-line drop.
|
|
60
|
+
*/
|
|
61
|
+
export declare function dropExpression(line: string, token: string): string | null;
|
|
62
|
+
export type DeprecationResult = {
|
|
63
|
+
refined: string;
|
|
64
|
+
trail: string[];
|
|
65
|
+
deprecations: Deprecation[];
|
|
66
|
+
};
|
|
67
|
+
/** Apply every detected deprecation to the refined prompt. Purely subtractive. */
|
|
68
|
+
export declare function applyDeprecations(refined: string, research: string): DeprecationResult;
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sibling of refuted-constraint.ts, for the other half of the same failure.
|
|
3
|
+
*
|
|
4
|
+
* That pass deletes a refine-invented DEPENDENCY the research says is not needed.
|
|
5
|
+
* This one deletes a refine-invented API or package the research says is
|
|
6
|
+
* DEPRECATED. Defect 14: refine writes an API example into CONSTRAINTS from
|
|
7
|
+
* memory before research runs, research then names that exact form as superseded,
|
|
8
|
+
* and compose prefers the constraint — so the run ships the deprecated call with
|
|
9
|
+
* the correct answer sitting one section above it in the same file.
|
|
10
|
+
*
|
|
11
|
+
* Everything about the sibling's discipline carries over unchanged: the
|
|
12
|
+
* correction is SUBTRACTIVE (an appended one loses to the text it contradicts),
|
|
13
|
+
* the match is lexical, and an owned-requirement line is never touched.
|
|
14
|
+
*
|
|
15
|
+
* Two things are deliberately NOT shared with the sibling, which is why this is a
|
|
16
|
+
* second pass and not a widening of the first:
|
|
17
|
+
*
|
|
18
|
+
* - it reads research APIS as well as CONTEXT, and two of the three recorded
|
|
19
|
+
* fires are in APIS;
|
|
20
|
+
* - its token class admits an UN-BACKTICKED API expression (`z.string().email()`),
|
|
21
|
+
* which `isPackageToken` refuses on purpose to keep the dependency channel
|
|
22
|
+
* narrow. Widening that channel to reach this one would loosen it for every
|
|
23
|
+
* dependency drop.
|
|
24
|
+
*/
|
|
25
|
+
/** Bare ALL-CAPS section header — same boundary convention as the sibling. */
|
|
26
|
+
const HEADER = /^[A-Z][A-Z -]*$/;
|
|
27
|
+
/**
|
|
28
|
+
* The closed deprecation set, exactly as the base-rate measurement left it.
|
|
29
|
+
*
|
|
30
|
+
* `\breplaced by\b` was in the first pattern set and the measurement REMOVED it:
|
|
31
|
+
* it fired on "`src/shared/index.ts` Empty file to be replaced by schema.ts
|
|
32
|
+
* exports", a claim about a FILE, and it was the only false positive in 14,171
|
|
33
|
+
* task files. Dropping it loses none of the three true fires.
|
|
34
|
+
*/
|
|
35
|
+
const DEPRECATION = /@?\bdeprecated\b|\bsupersed(?:e|es|ed)\b|\bmerged\s+into\b|\bno\s+longer\s+(?:recommended|supported|maintained)\b/i;
|
|
36
|
+
/**
|
|
37
|
+
* Where the token's own clause ends. A deprecation marker past one of these is a
|
|
38
|
+
* claim about something else — on "z.email() … for adminEmail (z.string().email()
|
|
39
|
+
* … is @deprecated)" the opening paren is what stops the leading symbol from
|
|
40
|
+
* inheriting the verdict on the expression inside it.
|
|
41
|
+
*
|
|
42
|
+
* A character window was measured first and rejected: it is flat from 41 to 4,000
|
|
43
|
+
* over 12,568 task files, so the corpus cannot choose one and any value would be
|
|
44
|
+
* a guess. The clause boundary reproduces the same 3 fires with no constant.
|
|
45
|
+
*/
|
|
46
|
+
const CLAUSE_END = /[;()\u2014\u2013]|\.\s/;
|
|
47
|
+
/** The owned-requirement stamp requirements.ts writes. Never refutable. */
|
|
48
|
+
const OWNED_MARKER = 'owned requirement from the source design';
|
|
49
|
+
const IDENT = '[A-Za-z_$][A-Za-z0-9_$]*';
|
|
50
|
+
const CALL = '\\([^()]*\\)';
|
|
51
|
+
/**
|
|
52
|
+
* A call expression: identifiers and calls chained with `.`, requiring at least
|
|
53
|
+
* one of each. `Network.Wai.Test` is a module path and does not qualify;
|
|
54
|
+
* `z.string().email()` does. This is the class the sibling refuses.
|
|
55
|
+
*/
|
|
56
|
+
const EXPRESSION = new RegExp(`${IDENT}(?:\\.${IDENT}|${CALL})+`, 'g');
|
|
57
|
+
/**
|
|
58
|
+
* A package name, narrowed to the compound forms. A bare word (`aeson`, `text`)
|
|
59
|
+
* is excluded: on the leading-symbol rule below it would let any one-word noun in
|
|
60
|
+
* a deprecation sentence reach CONSTRAINTS, and no recorded fire needs it.
|
|
61
|
+
*/
|
|
62
|
+
const PACKAGE = /^@?[a-z][a-z0-9.]*(?:[-/][a-z0-9.]+)+$/;
|
|
63
|
+
/** Section body between a bare ALL-CAPS header and the next one (or EOF). */
|
|
64
|
+
function capsSection(text, heading) {
|
|
65
|
+
const lines = text.split('\n');
|
|
66
|
+
const start = lines.findIndex(l => l.trim() === heading);
|
|
67
|
+
if (start === -1)
|
|
68
|
+
return null;
|
|
69
|
+
const rest = lines.slice(start + 1);
|
|
70
|
+
const end = rest.findIndex(l => HEADER.test(l.trim()) && l.trim().length > 1);
|
|
71
|
+
return (end === -1 ? rest : rest.slice(0, end)).join('\n');
|
|
72
|
+
}
|
|
73
|
+
/** One entry per bullet or per `symbol description` row; continuations joined. */
|
|
74
|
+
function entries(section) {
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const raw of section.split('\n')) {
|
|
77
|
+
const line = raw.trimEnd();
|
|
78
|
+
if (!line.trim())
|
|
79
|
+
continue;
|
|
80
|
+
if (/^\s*[-*]\s+/.test(line) || !/^\s/.test(line))
|
|
81
|
+
out.push(line.trim());
|
|
82
|
+
else if (out.length > 0)
|
|
83
|
+
out[out.length - 1] += ` ${line.trim()}`;
|
|
84
|
+
else
|
|
85
|
+
out.push(line.trim());
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
function stripTicks(s) {
|
|
90
|
+
return s.replace(/`/g, '');
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Every expression on the line whose deprecation marker sits in the same clause,
|
|
94
|
+
* ahead of it. Forward-only: "X is deprecated, use Y" must not indict Y, and Y is
|
|
95
|
+
* always on the far side of the marker.
|
|
96
|
+
*/
|
|
97
|
+
function markerAdjacent(line) {
|
|
98
|
+
const flat = stripTicks(line);
|
|
99
|
+
const out = [];
|
|
100
|
+
for (const m of flat.matchAll(EXPRESSION)) {
|
|
101
|
+
const tok = m[0];
|
|
102
|
+
if (!tok.includes('.') || !tok.includes('('))
|
|
103
|
+
continue;
|
|
104
|
+
const rest = flat.slice(m.index + tok.length);
|
|
105
|
+
const stop = CLAUSE_END.exec(rest);
|
|
106
|
+
const tail = stop ? rest.slice(0, stop.index) : rest;
|
|
107
|
+
if (DEPRECATION.test(tail) && !out.includes(tok))
|
|
108
|
+
out.push(tok);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* An APIS row is `symbol description`, so the row's leading symbol is what the
|
|
114
|
+
* row is ABOUT. Used only when no expression sits next to the marker: on
|
|
115
|
+
* "z.email() … (z.string().email() … is @deprecated)" the leading symbol is the
|
|
116
|
+
* replacement, not the casualty, and the adjacent expression is the truth.
|
|
117
|
+
*/
|
|
118
|
+
function apisSymbol(line) {
|
|
119
|
+
const m = /^(\S+)\s\s+\S/.exec(stripTicks(line));
|
|
120
|
+
if (!m)
|
|
121
|
+
return null;
|
|
122
|
+
const tok = m[1];
|
|
123
|
+
if (PACKAGE.test(tok))
|
|
124
|
+
return tok;
|
|
125
|
+
if (tok.includes('.') && tok.includes('('))
|
|
126
|
+
return tok;
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
/** The tokens one research line deprecates, or an empty list. */
|
|
130
|
+
export function deprecatedTokens(line) {
|
|
131
|
+
if (!DEPRECATION.test(stripTicks(line)))
|
|
132
|
+
return [];
|
|
133
|
+
const adjacent = markerAdjacent(line);
|
|
134
|
+
if (adjacent.length > 0)
|
|
135
|
+
return adjacent;
|
|
136
|
+
const symbol = apisSymbol(line);
|
|
137
|
+
return symbol === null ? [] : [symbol];
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Find every (constraint line, research line, deprecated token) triple. `refined`
|
|
141
|
+
* is the refined prompt and `research` the research output — the exact strings
|
|
142
|
+
* compose is handed.
|
|
143
|
+
*/
|
|
144
|
+
export function detectDeprecations(refined, research) {
|
|
145
|
+
const constraintsBody = capsSection(refined, 'CONSTRAINTS');
|
|
146
|
+
if (constraintsBody === null)
|
|
147
|
+
return [];
|
|
148
|
+
const deprecated = new Map();
|
|
149
|
+
for (const heading of ['APIS', 'CONTEXT']) {
|
|
150
|
+
const body = capsSection(research, heading);
|
|
151
|
+
if (body === null)
|
|
152
|
+
continue;
|
|
153
|
+
for (const line of entries(body)) {
|
|
154
|
+
const adjacent = markerAdjacent(line);
|
|
155
|
+
const rule = adjacent.length > 0 ? 'marker-adjacent' : 'apis-symbol';
|
|
156
|
+
for (const tok of deprecatedTokens(line)) {
|
|
157
|
+
if (!deprecated.has(tok))
|
|
158
|
+
deprecated.set(tok, { research: line, rule });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (deprecated.size === 0)
|
|
163
|
+
return [];
|
|
164
|
+
const lines = refined.split('\n');
|
|
165
|
+
const start = lines.findIndex(l => l.trim() === 'CONSTRAINTS');
|
|
166
|
+
const out = [];
|
|
167
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
168
|
+
const line = lines[i];
|
|
169
|
+
if (HEADER.test(line.trim()) && line.trim().length > 1)
|
|
170
|
+
break;
|
|
171
|
+
if (line.includes(OWNED_MARKER))
|
|
172
|
+
continue;
|
|
173
|
+
for (const [tok, src] of deprecated) {
|
|
174
|
+
if (!stripTicks(line).includes(tok))
|
|
175
|
+
continue;
|
|
176
|
+
out.push({
|
|
177
|
+
token: tok,
|
|
178
|
+
line: i,
|
|
179
|
+
constraint: line,
|
|
180
|
+
research: src.research,
|
|
181
|
+
rule: src.rule
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
function escapeRe(s) {
|
|
188
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Delete one token from a constraint line, backticked or bare, plus one adjacent
|
|
192
|
+
* list separator. Then collapse the wreckage the deletion left: an emptied
|
|
193
|
+
* `(e.g. )`, doubled spaces, a space stranded before punctuation.
|
|
194
|
+
*
|
|
195
|
+
* Every step removes characters and adds none, so the result stays a character
|
|
196
|
+
* subsequence of the input — the invariant the whole approach rests on.
|
|
197
|
+
*
|
|
198
|
+
* The example-parenthetical collapse requires a literal `e.g.`/`i.e.` rather than
|
|
199
|
+
* matching any emptied `()`. A bare `\(\s*\)` also matches the call parens inside
|
|
200
|
+
* `z.string()` elsewhere on the same line, which would silently corrupt a
|
|
201
|
+
* constraint this pass is not even firing on.
|
|
202
|
+
*
|
|
203
|
+
* Returns null when nothing but boilerplate is left, which the caller turns into
|
|
204
|
+
* a whole-line drop.
|
|
205
|
+
*/
|
|
206
|
+
export function dropExpression(line, token) {
|
|
207
|
+
const tokRe = new RegExp('`?' + escapeRe(token) + '`?', 'g');
|
|
208
|
+
let next = line;
|
|
209
|
+
for (;;) {
|
|
210
|
+
const m = tokRe.exec(next);
|
|
211
|
+
if (!m)
|
|
212
|
+
break;
|
|
213
|
+
const start = m.index;
|
|
214
|
+
const end = start + m[0].length;
|
|
215
|
+
const before = next.slice(0, start);
|
|
216
|
+
const sepBefore = /(?:,\s*|\s+or\s+|\s+and\s+)$/.exec(before);
|
|
217
|
+
if (sepBefore) {
|
|
218
|
+
next = next.slice(0, start - sepBefore[0].length) + next.slice(end);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
const sepAfter = /^(?:\s*,\s*|\s+or\s+|\s+and\s+)/.exec(next.slice(end));
|
|
222
|
+
next = before + next.slice(end + (sepAfter ? sepAfter[0].length : 0));
|
|
223
|
+
}
|
|
224
|
+
tokRe.lastIndex = 0;
|
|
225
|
+
}
|
|
226
|
+
if (next === line)
|
|
227
|
+
return line;
|
|
228
|
+
// Leading whitespace is held out of the collapse: it is a nested bullet's
|
|
229
|
+
// markdown level, not a gap this deletion opened.
|
|
230
|
+
const indent = /^\s*/.exec(next)?.[0] ?? '';
|
|
231
|
+
next =
|
|
232
|
+
indent
|
|
233
|
+
+ next
|
|
234
|
+
.slice(indent.length)
|
|
235
|
+
.replace(/\s*\((?:\s*e\.g\.,?|\s*i\.e\.,?)\s*\)/gi, '')
|
|
236
|
+
.replace(/ {2,}/g, ' ')
|
|
237
|
+
.replace(/ +([.,;:)])/g, '$1');
|
|
238
|
+
const carcass = next.replace(/^\s*[-*]\s*/, '').replace(/[\s,.;:()]/g, '');
|
|
239
|
+
return carcass.length === 0 ? null : next;
|
|
240
|
+
}
|
|
241
|
+
/** Apply every detected deprecation to the refined prompt. Purely subtractive. */
|
|
242
|
+
export function applyDeprecations(refined, research) {
|
|
243
|
+
const deprecations = detectDeprecations(refined, research);
|
|
244
|
+
if (deprecations.length === 0)
|
|
245
|
+
return { refined, trail: [], deprecations };
|
|
246
|
+
const lines = refined.split('\n');
|
|
247
|
+
const dropped = new Set();
|
|
248
|
+
const trail = [];
|
|
249
|
+
for (const d of deprecations) {
|
|
250
|
+
const next = dropExpression(lines[d.line], d.token);
|
|
251
|
+
const what = next === null ?
|
|
252
|
+
`dropped the whole CONSTRAINTS line for '${d.token}'`
|
|
253
|
+
: `dropped '${d.token}' from CONSTRAINTS`;
|
|
254
|
+
if (next === null)
|
|
255
|
+
dropped.add(d.line);
|
|
256
|
+
else
|
|
257
|
+
lines[d.line] = next;
|
|
258
|
+
trail.push(`constraint deprecated by research — ${what}`
|
|
259
|
+
+ ` | constraint: "${d.constraint.trim()}" | research: "${d.research.trim()}"`);
|
|
260
|
+
}
|
|
261
|
+
return { refined: lines.filter((_l, i) => !dropped.has(i)).join('\n'), trail, deprecations };
|
|
262
|
+
}
|
package/dist/task/phases.d.ts
CHANGED
|
@@ -174,10 +174,12 @@ export declare function phaseResearch(deps: PhaseDeps, refined: string): Promise
|
|
|
174
174
|
export declare function phaseAutoAnswer(deps: PhaseDeps, refined: string, research: string, question: string): Promise<AutoAnswer>;
|
|
175
175
|
export declare function phaseGrill(deps: PhaseDeps, ctx: ExtensionCommandContext, widgetState: WidgetState, refined: string, research: string): Promise<string>;
|
|
176
176
|
/**
|
|
177
|
-
* A
|
|
178
|
-
* dependency refine invented is not needed,
|
|
179
|
-
*
|
|
180
|
-
*
|
|
177
|
+
* A correction is a DELETION. Where the run's own research explicitly says a
|
|
178
|
+
* dependency refine invented is not needed, or names the API refine wrote as
|
|
179
|
+
* deprecated, drop that token from CONSTRAINTS — compose cannot forbid the
|
|
180
|
+
* design's own API "because the refined task explicitly requires `argon2`" if the
|
|
181
|
+
* refined task no longer requires it, and cannot ship `z.string().email()` under
|
|
182
|
+
* a constraint that no longer names it.
|
|
181
183
|
*
|
|
182
184
|
* Applied to the REFINED TASK ITSELF, not to compose's copy of it, and that is
|
|
183
185
|
* load-bearing: critique receives the refined task as GROUND TRUTH and is told
|
|
@@ -185,7 +187,8 @@ export declare function phaseGrill(deps: PhaseDeps, ctx: ExtensionCommandContext
|
|
|
185
187
|
* them", so a deletion visible only to compose is restored one phase later. Both
|
|
186
188
|
* spec-producing phases have to see the same text.
|
|
187
189
|
*
|
|
188
|
-
* Purely subtractive and never touches an owned line (task/refuted-constraint.ts
|
|
190
|
+
* Purely subtractive and never touches an owned line (task/refuted-constraint.ts,
|
|
191
|
+
* task/deprecated-constraint.ts).
|
|
189
192
|
* Idempotent, so a resumed run re-deriving `refined` from the task file lands in
|
|
190
193
|
* the same place. The task file's `## refined prompt` is deliberately left as
|
|
191
194
|
* refine wrote it; the drop is recorded on the `## gates` trail with both source
|
|
@@ -193,7 +196,7 @@ export declare function phaseGrill(deps: PhaseDeps, ctx: ExtensionCommandContext
|
|
|
193
196
|
*/
|
|
194
197
|
export declare function dropRefutedConstraints(deps: PhaseDeps, refined: string, research: string): Promise<string>;
|
|
195
198
|
/**
|
|
196
|
-
* COMPOSE's carry:
|
|
199
|
+
* COMPOSE's carry: both subtractive drops, as a `PhaseConfig.carry`.
|
|
197
200
|
*
|
|
198
201
|
* Same transform as `dropRefutedConstraints` over the same pure core, minus the
|
|
199
202
|
* recording — the caller decides whether this application is the live one or a
|
package/dist/task/phases.js
CHANGED
|
@@ -22,6 +22,7 @@ import { resolve } from 'node:path';
|
|
|
22
22
|
import { buildExternalContext, gatherExternalContext } from './external-context.js';
|
|
23
23
|
import { REFINE_PROMPT, RESEARCH_FILES_PROMPT, RESEARCH_APIS_PROMPT, RESEARCH_CONTEXT_PROMPT, RESEARCH_TOOLING_PROMPT, GRILL_GEN_PROMPT, GRILL_AUTO_ANSWER_PROMPT, GRILL_AUTO_FORMAT_HINT, COMPOSE_PROMPT, CRITIQUE_PROMPT, CRITIQUE_TRIAGE_PROMPT, VERIFY_TOOLING_PROMPT, MAX_GRILL_QUESTIONS } from './prompts.js';
|
|
24
24
|
import { appendGateRecord, readSection, removeTaskSection, setTaskSection, updateTaskFrontMatter } from './task-io.js';
|
|
25
|
+
import { applyDeprecations } from './deprecated-constraint.js';
|
|
25
26
|
import { applyRefutations } from './refuted-constraint.js';
|
|
26
27
|
import { spawnSync } from 'node:child_process';
|
|
27
28
|
import {} from './task-types.js';
|
|
@@ -906,10 +907,12 @@ export async function phaseGrill(deps, ctx, widgetState, refined, research) {
|
|
|
906
907
|
return transcript.forRecord();
|
|
907
908
|
}
|
|
908
909
|
/**
|
|
909
|
-
* A
|
|
910
|
-
* dependency refine invented is not needed,
|
|
911
|
-
*
|
|
912
|
-
*
|
|
910
|
+
* A correction is a DELETION. Where the run's own research explicitly says a
|
|
911
|
+
* dependency refine invented is not needed, or names the API refine wrote as
|
|
912
|
+
* deprecated, drop that token from CONSTRAINTS — compose cannot forbid the
|
|
913
|
+
* design's own API "because the refined task explicitly requires `argon2`" if the
|
|
914
|
+
* refined task no longer requires it, and cannot ship `z.string().email()` under
|
|
915
|
+
* a constraint that no longer names it.
|
|
913
916
|
*
|
|
914
917
|
* Applied to the REFINED TASK ITSELF, not to compose's copy of it, and that is
|
|
915
918
|
* load-bearing: critique receives the refined task as GROUND TRUTH and is told
|
|
@@ -917,21 +920,22 @@ export async function phaseGrill(deps, ctx, widgetState, refined, research) {
|
|
|
917
920
|
* them", so a deletion visible only to compose is restored one phase later. Both
|
|
918
921
|
* spec-producing phases have to see the same text.
|
|
919
922
|
*
|
|
920
|
-
* Purely subtractive and never touches an owned line (task/refuted-constraint.ts
|
|
923
|
+
* Purely subtractive and never touches an owned line (task/refuted-constraint.ts,
|
|
924
|
+
* task/deprecated-constraint.ts).
|
|
921
925
|
* Idempotent, so a resumed run re-deriving `refined` from the task file lands in
|
|
922
926
|
* the same place. The task file's `## refined prompt` is deliberately left as
|
|
923
927
|
* refine wrote it; the drop is recorded on the `## gates` trail with both source
|
|
924
928
|
* lines quoted, so the decision stays auditable after the fact.
|
|
925
929
|
*/
|
|
926
930
|
export async function dropRefutedConstraints(deps, refined, research) {
|
|
927
|
-
const
|
|
928
|
-
if (
|
|
931
|
+
const dropped = dropRefutedAndDeprecated(refined, research);
|
|
932
|
+
if (dropped.trail.length === 0)
|
|
929
933
|
return refined;
|
|
930
|
-
await recordPhaseTrail(deps, 'compose',
|
|
931
|
-
return
|
|
934
|
+
await recordPhaseTrail(deps, 'compose', dropped.trail);
|
|
935
|
+
return dropped.refined;
|
|
932
936
|
}
|
|
933
937
|
/**
|
|
934
|
-
* COMPOSE's carry:
|
|
938
|
+
* COMPOSE's carry: both subtractive drops, as a `PhaseConfig.carry`.
|
|
935
939
|
*
|
|
936
940
|
* Same transform as `dropRefutedConstraints` over the same pure core, minus the
|
|
937
941
|
* recording — the caller decides whether this application is the live one or a
|
|
@@ -941,11 +945,21 @@ export async function dropRefutedConstraints(deps, refined, research) {
|
|
|
941
945
|
export function composeCarry(_deps, pc) {
|
|
942
946
|
// Not `async`, and that is the shape rather than an oversight: this carry
|
|
943
947
|
// performs no I/O at all, which is what lets the resume path replay it.
|
|
944
|
-
const
|
|
945
|
-
if (
|
|
948
|
+
const dropped = dropRefutedAndDeprecated(pc.refined, pc.research);
|
|
949
|
+
if (dropped.trail.length === 0)
|
|
946
950
|
return Promise.resolve([]);
|
|
947
|
-
pc.refined =
|
|
948
|
-
return Promise.resolve(
|
|
951
|
+
pc.refined = dropped.refined;
|
|
952
|
+
return Promise.resolve(dropped.trail);
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* The two subtractive passes, in one place so the live path and the resume replay
|
|
956
|
+
* cannot drift apart. Chained rather than run side by side: the second reads the
|
|
957
|
+
* first's output, so a line the first deleted outright is never scanned again.
|
|
958
|
+
*/
|
|
959
|
+
function dropRefutedAndDeprecated(refined, research) {
|
|
960
|
+
const refuted = applyRefutations(refined, research);
|
|
961
|
+
const deprecated = applyDeprecations(refuted.refined, research);
|
|
962
|
+
return { refined: deprecated.refined, trail: [...refuted.trail, ...deprecated.trail] };
|
|
949
963
|
}
|
|
950
964
|
/** Write a carry's trail to the debug log and the task file's `## gates` section. */
|
|
951
965
|
export async function recordPhaseTrail(deps, phaseName, trail) {
|
|
@@ -79,6 +79,8 @@ export function buildExtractionPrompt(opts) {
|
|
|
79
79
|
+ `4. If the answer is unclear, ambiguous, or absent from <${tag}-content>, write exactly:\n`
|
|
80
80
|
+ ` <answer>${abstentionSentence(opts.kind)}</answer> and put the closest related text in <excerpt>.\n`
|
|
81
81
|
+ ` Do not guess.\n`
|
|
82
|
+
+ ` A question with several parts: answer the parts <${tag}-content> covers, and name\n`
|
|
83
|
+
+ ` the parts it does not. Use rule 4's sentence alone only when it covers no part.\n`
|
|
82
84
|
+ `5. Be terse. One short paragraph in <answer> max.\n`
|
|
83
85
|
+ `\n`
|
|
84
86
|
+ `<${tag}>${opts.identity}</${tag}>\n`
|
|
@@ -17,6 +17,7 @@ import { type AutoInstallPin } from './docs-core.js';
|
|
|
17
17
|
import { resolvePackage, type ResolvedPackage } from './docs-resolve.js';
|
|
18
18
|
import { npmVersionLookup, type NpmVersionInfo } from './npm-version.js';
|
|
19
19
|
import { type SpawnFn } from '../shared/child-process.js';
|
|
20
|
+
import type { ExportGap } from './export-gap.js';
|
|
20
21
|
export type EcosystemId = 'npm' | 'cargo' | 'hackage';
|
|
21
22
|
/**
|
|
22
23
|
* Every filesystem, process and network reach a row is allowed. Rows read no
|
|
@@ -75,10 +76,22 @@ export interface EcosystemProfile {
|
|
|
75
76
|
}>;
|
|
76
77
|
/**
|
|
77
78
|
* Packages whose declarations belong in THIS package's index, because this
|
|
78
|
-
* package exports names it does not declare
|
|
79
|
-
* `
|
|
79
|
+
* package exports names it does not declare — `hspec`/`hspec-core`,
|
|
80
|
+
* `axum`/`axum-core`; see DEFECT-12-STOPPING-RULE.md.
|
|
80
81
|
*/
|
|
81
82
|
supplements?: (pkg: ResolvedPackage, cwd: string, io: EcosystemIo) => Promise<ResolvedPackage[]>;
|
|
83
|
+
/**
|
|
84
|
+
* What this package publishes and declares nowhere, in this ecosystem's own
|
|
85
|
+
* syntax. Read only when `supplements` returned something, and it decides
|
|
86
|
+
* which of a supplement's chunks are kept.
|
|
87
|
+
*/
|
|
88
|
+
exportGap?: (root: string) => ExportGap;
|
|
89
|
+
/**
|
|
90
|
+
* Source of `exportGap` AND everything it delegates to, for the index
|
|
91
|
+
* fingerprint. `String(exportGap)` alone leaves a fix in a helper invisible,
|
|
92
|
+
* which is the failure `chunkerFingerprint` exists to close one level up.
|
|
93
|
+
*/
|
|
94
|
+
exportGapFingerprint?: () => string;
|
|
82
95
|
/** The registry's own newest version, for grounding an answer in the present. */
|
|
83
96
|
latest: (name: string, io: EcosystemIo) => Promise<NpmVersionInfo | null>;
|
|
84
97
|
/** True for a file that carries the package's public API surface. */
|
|
@@ -21,8 +21,8 @@ import { runAutoInstall, findDeclaredRange, extractParentPackage, resolveTypeSou
|
|
|
21
21
|
import { resolvePackage, isDtsFile, isValidModuleName } from './docs-resolve.js';
|
|
22
22
|
import { DECL_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, CARGO_DECL_SPLIT_RE } from './eco-cargo.js';
|
|
25
|
-
import { resolveHackage, hackageLatest, hackageVersion, hackageTarballUrl, hackageExtractDir, hackageProjectName, supplementCandidates, 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, cargoGapFingerprint, cargoSupplementCandidates, CARGO_DECL_SPLIT_RE } from './eco-cargo.js';
|
|
25
|
+
import { resolveHackage, hackageLatest, hackageVersion, hackageTarballUrl, hackageExtractDir, hackageProjectName, supplementCandidates, hackageExportGap, hackageGapFingerprint, findCabalTarball, cachedVersions, resolvedVersions, manifestPackages, isValidHackageName, isHaskellFile, haskellSurface, HACKAGE_DECL_SPLIT_RE, HACKAGE_SKIP_DIRS } 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?
|
|
@@ -267,6 +267,39 @@ const cargoProfile = {
|
|
|
267
267
|
}
|
|
268
268
|
return acquireCrate(info?.pkg ?? name, version, io);
|
|
269
269
|
},
|
|
270
|
+
supplements: async (pkg, cwd, io) => {
|
|
271
|
+
const deps = manifestCrates(pkg.root);
|
|
272
|
+
if (!deps)
|
|
273
|
+
return [];
|
|
274
|
+
// The PROJECT's lock, never the crate's own root. `findLock` walks upward,
|
|
275
|
+
// and a crate unpacked under `~/.cargo/registry` sits below whatever lock
|
|
276
|
+
// happens to be above it — which resolved a version this project never
|
|
277
|
+
// pinned, making the index a function of the machine.
|
|
278
|
+
const candidates = cargoSupplementCandidates(pkg.name, deps, lockedDeps(cwd) ?? {});
|
|
279
|
+
const out = [];
|
|
280
|
+
for (const c of candidates) {
|
|
281
|
+
try {
|
|
282
|
+
out.push(resolveCrate(c.name, cwd, { cargoHome: io.cargoHome, modulesDir: io.modulesDir }));
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
// Not unpacked here. `acquire` reads crates.io for the published
|
|
287
|
+
// spelling, so `tokio-util` and `tokio_util` both land.
|
|
288
|
+
}
|
|
289
|
+
const got = await cargoProfile.acquire(c.name, c.version, io);
|
|
290
|
+
if (!got.success)
|
|
291
|
+
continue;
|
|
292
|
+
try {
|
|
293
|
+
out.push(resolveCrate(c.name, cwd, { cargoHome: io.cargoHome, modulesDir: io.modulesDir }));
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
// A supplement that will not resolve leaves the facade as it was.
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return out;
|
|
300
|
+
},
|
|
301
|
+
exportGap: cargoExportGap,
|
|
302
|
+
exportGapFingerprint: cargoGapFingerprint,
|
|
270
303
|
latest: (name, io) => cratesLatest(name, io.fetch, io.signal),
|
|
271
304
|
isSurfaceFile: isRustFile,
|
|
272
305
|
surface: content => rustSurface(content),
|
|
@@ -381,6 +414,8 @@ const hackageProfile = {
|
|
|
381
414
|
}
|
|
382
415
|
return out;
|
|
383
416
|
},
|
|
417
|
+
exportGap: hackageExportGap,
|
|
418
|
+
exportGapFingerprint: hackageGapFingerprint,
|
|
384
419
|
latest: (name, io) => hackageLatest(name, io.fetch, io.signal),
|
|
385
420
|
isSurfaceFile: isHaskellFile,
|
|
386
421
|
surface: haskellSurface,
|
|
@@ -4,7 +4,6 @@ import * as path from 'node:path';
|
|
|
4
4
|
import {} from './docs-resolve.js';
|
|
5
5
|
import { chunkDeclarations, chunkReadme, splitAtMatches } from './docs-chunk.js';
|
|
6
6
|
import { ECOSYSTEMS } from './docs-ecosystems.js';
|
|
7
|
-
import { hackageExportGap, declaredInSurface } from './eco-hackage.js';
|
|
8
7
|
const ZERO_SEP = Buffer.from([0]);
|
|
9
8
|
/**
|
|
10
9
|
* The gate that decides whether a package needs re-indexing.
|
|
@@ -55,9 +54,14 @@ function computeContentHash(pkg, profile, supplements = []) {
|
|
|
55
54
|
// — and nothing surfaced the duplicate drop in `ingestBody` at all.
|
|
56
55
|
hash.update(Buffer.from(`${String(profile.surface)}\u0000${String(ingestBody)}`, 'utf8'));
|
|
57
56
|
hash.update(ZERO_SEP);
|
|
58
|
-
// Which packages were folded in, so gaining or losing one re-indexes
|
|
57
|
+
// Which packages were folded in, so gaining or losing one re-indexes — and the
|
|
58
|
+
// rule that decides WHICH of their chunks are kept, by source. Hashing the set
|
|
59
|
+
// alone left a fix to `cargoExportGap` invisible, so every cached facade held
|
|
60
|
+
// the chunks the old rule chose.
|
|
59
61
|
hash.update(Buffer.from(supplements.map(s => `${s.name}@${s.version}`).join('\u0000'), 'utf8'));
|
|
60
62
|
hash.update(ZERO_SEP);
|
|
63
|
+
hash.update(Buffer.from(profile.exportGapFingerprint?.() ?? String(profile.exportGap ?? ''), 'utf8'));
|
|
64
|
+
hash.update(ZERO_SEP);
|
|
61
65
|
if (pkg.entry && fs.existsSync(pkg.entry)) {
|
|
62
66
|
try {
|
|
63
67
|
hash.update(Buffer.from(profile.surface(fs.readFileSync(pkg.entry, 'utf8')), 'utf8'));
|
|
@@ -216,10 +220,9 @@ function ingestBody(cache, pkg, profile, contentHash, supplements = []) {
|
|
|
216
220
|
// A facade package indexes to a table of contents: `hspec` is 14 chunks of
|
|
217
221
|
// export lists and every signature is in `hspec-core`. Fill only the holes —
|
|
218
222
|
// see DEFECT-12-STOPPING-RULE.md for the boundary and why it stops here.
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
: []) {
|
|
223
|
+
const found = supplements.length > 0 ? (profile.exportGap?.(pkg.root) ?? null) : null;
|
|
224
|
+
const gap = found !== null && !found.empty ? found : null;
|
|
225
|
+
for (const sup of gap === null ? [] : supplements) {
|
|
223
226
|
for (const abs of collectFiles(sup, profile).surface) {
|
|
224
227
|
let raw;
|
|
225
228
|
try {
|
|
@@ -228,15 +231,13 @@ function ingestBody(cache, pkg, profile, contentHash, supplements = []) {
|
|
|
228
231
|
catch {
|
|
229
232
|
continue;
|
|
230
233
|
}
|
|
231
|
-
const module = /^module\s+([\w.']+)/m.exec(raw)?.[1];
|
|
232
|
-
const whole = module !== undefined && gap.reexportedModules.has(module);
|
|
233
234
|
// The path names the package the declaration really came from: the
|
|
234
235
|
// chunk header is model-facing, and a signature attributed to the
|
|
235
236
|
// wrong package is the bug this whole table exists for.
|
|
236
237
|
const rel = `${sup.name}-${sup.version}/` + path.relative(sup.root, abs).replace(/\\/g, '/');
|
|
238
|
+
const whole = gap.wholesale(rel, raw);
|
|
237
239
|
for (const c of chunkDeclarations(profile.surface(raw), rel, profile.declSplitRe, profile.commentPrefix)) {
|
|
238
|
-
|
|
239
|
-
if (!whole && ![...declares].some(n => gap.unresolved.has(n)))
|
|
240
|
+
if (!whole && !gap.fillsHole(c.replace(/^\S.*\n/, '')))
|
|
240
241
|
continue;
|
|
241
242
|
if (seen.has(c))
|
|
242
243
|
continue;
|
|
@@ -36,7 +36,7 @@ export async function docsLookup(input) {
|
|
|
36
36
|
const excerptVerified = extraction.excerptVerified;
|
|
37
37
|
return {
|
|
38
38
|
kind: 'answer',
|
|
39
|
-
body: formatResultText(input.corpus.header, extraction,
|
|
39
|
+
body: formatResultText(input.corpus.header, extraction, extraction.excerptCheck),
|
|
40
40
|
content,
|
|
41
41
|
extraction,
|
|
42
42
|
excerptVerified
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { type ResolvedPackage } from './docs-resolve.js';
|
|
14
14
|
import type { NpmVersionInfo } from './npm-version.js';
|
|
15
|
+
import type { ExportGap } from './export-gap.js';
|
|
15
16
|
export declare function isValidCrateName(name: string): boolean;
|
|
16
17
|
/** `serde_json::Value` is a path into `serde_json`; the crate is what installs. */
|
|
17
18
|
export declare function crateOf(name: string): string;
|
|
@@ -123,4 +124,37 @@ export declare function cargoProjectName(cwd: string): string | null;
|
|
|
123
124
|
* Undefined when there is no readable manifest, which is not "declares nothing".
|
|
124
125
|
*/
|
|
125
126
|
export declare function manifestCrates(cwd: string): Set<string> | undefined;
|
|
127
|
+
/**
|
|
128
|
+
* The names this crate publishes through a dependency and declares nowhere.
|
|
129
|
+
*
|
|
130
|
+
* The trigger is the hole, with no threshold — measured, and for the same reason
|
|
131
|
+
* as hackage: across twenty-two crates the unresolved fraction reads 100% on a
|
|
132
|
+
* crate with one re-export and 0% on a crate with none, so a ratio separates
|
|
133
|
+
* nothing. See "Defect 16" in DOC_REGRESSINONS.md for the sweep.
|
|
134
|
+
*/
|
|
135
|
+
export declare function cargoExportGap(root: string): ExportGap;
|
|
136
|
+
/**
|
|
137
|
+
* Source of every function `cargoExportGap` delegates to.
|
|
138
|
+
*
|
|
139
|
+
* `String(cargoExportGap)` covers only the top level, and the two bugs already
|
|
140
|
+
* found in this pass — a rename split that truncated `Hasher`, a lock read from
|
|
141
|
+
* the wrong root — both lived in helpers. A fix to one of them has to move the
|
|
142
|
+
* index fingerprint or every cached facade keeps the chunks the old rule chose.
|
|
143
|
+
*/
|
|
144
|
+
export declare function cargoGapFingerprint(): string;
|
|
145
|
+
/**
|
|
146
|
+
* Which declared dependencies may be opened to fill the gap.
|
|
147
|
+
*
|
|
148
|
+
* Cargo splits a facade from its implementation by name the way hackage does —
|
|
149
|
+
* `axum`/`axum-core`, `futures`/`futures-util`, `tracing`/`tracing-core` — and
|
|
150
|
+
* writes that name with either separator, so both spellings are one candidate.
|
|
151
|
+
*
|
|
152
|
+
* The bound's cost is stated rather than hidden: `hyper` re-exports twelve names
|
|
153
|
+
* from `http`, `bytes` and `http-body`, and axum's own `Bytes` comes from `bytes`.
|
|
154
|
+
* No prefix rule can see any of them.
|
|
155
|
+
*/
|
|
156
|
+
export declare function cargoSupplementCandidates(pkgName: string, declaredDeps: ReadonlySet<string>, resolved: Readonly<Record<string, string>>): Array<{
|
|
157
|
+
name: string;
|
|
158
|
+
version: string;
|
|
159
|
+
}>;
|
|
126
160
|
export {};
|
|
@@ -832,3 +832,223 @@ export function manifestCrates(cwd) {
|
|
|
832
832
|
}
|
|
833
833
|
return out;
|
|
834
834
|
}
|
|
835
|
+
// ── the facade gap (DEFECT-12-STOPPING-RULE.md, cargo half) ─────────────────
|
|
836
|
+
/** A `pub use …;` statement, attributes and line breaks included. */
|
|
837
|
+
const PUB_USE_RE = /\bpub\s+use\s+([^;]+);/g;
|
|
838
|
+
/** Every item head that introduces a name, visibility ignored — a facade may
|
|
839
|
+
* re-export something its own private module declares. */
|
|
840
|
+
const RUST_DECL_RE = /\b(?:fn|struct|enum|union|trait|type|const|static|mod)\s+([A-Za-z_][A-Za-z0-9_]*)|macro_rules!\s*([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
841
|
+
/** Path roots that name this crate, never a dependency. */
|
|
842
|
+
const OWN_PATH_ROOTS = new Set(['crate', 'self', 'super']);
|
|
843
|
+
/** Read `[dependencies]` only. Dev- and build-dependencies were measured and
|
|
844
|
+
* fetch `tokio-test`, `regex-test` and `tower-test` for zero extra names. */
|
|
845
|
+
function runtimeDeps(root) {
|
|
846
|
+
const text = safeRead(path.join(root, 'Cargo.toml'));
|
|
847
|
+
const out = new Set();
|
|
848
|
+
if (text === null)
|
|
849
|
+
return out;
|
|
850
|
+
let inDeps = false;
|
|
851
|
+
for (const raw of text.split('\n')) {
|
|
852
|
+
const line = raw.trim();
|
|
853
|
+
const header = /^\[([^\]]+)\]$/.exec(line);
|
|
854
|
+
if (header) {
|
|
855
|
+
const table = /^(?:target\.[^.]*\.)?dependencies(?:\.(.+))?$/.exec(header[1]);
|
|
856
|
+
inDeps = table !== null && table[1] === undefined;
|
|
857
|
+
if (table?.[1])
|
|
858
|
+
out.add(canonical(table[1]));
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
if (!inDeps)
|
|
862
|
+
continue;
|
|
863
|
+
const key = /^([A-Za-z0-9_-]+)\s*=/.exec(line);
|
|
864
|
+
if (key)
|
|
865
|
+
out.add(canonical(key[1]));
|
|
866
|
+
}
|
|
867
|
+
return out;
|
|
868
|
+
}
|
|
869
|
+
/** Every leaf name a use-path brings in, and every module it globs. */
|
|
870
|
+
function useTargets(body) {
|
|
871
|
+
// Whitespace is normalised, never removed: `Inner as Outer` collapsed to
|
|
872
|
+
// `InnerasOuter` is unrecoverable, and splitting a leaf on a bare "as" turns
|
|
873
|
+
// `Hasher` into `H`.
|
|
874
|
+
const flat = body
|
|
875
|
+
.replace(/#\[[^\]]*\]/g, '')
|
|
876
|
+
.replace(/\s+/g, ' ')
|
|
877
|
+
.trim();
|
|
878
|
+
const names = [];
|
|
879
|
+
const globs = [];
|
|
880
|
+
const expand = (prefix, rest) => {
|
|
881
|
+
const brace = rest.indexOf('{');
|
|
882
|
+
if (brace === -1) {
|
|
883
|
+
const full = prefix + rest;
|
|
884
|
+
if (full.endsWith('*'))
|
|
885
|
+
globs.push(full.replace(/::\*$/, ''));
|
|
886
|
+
else {
|
|
887
|
+
// The SOURCE name of a rename is the hole: the supplier declares
|
|
888
|
+
// `Inner`, whatever the facade calls it.
|
|
889
|
+
const leaf = full
|
|
890
|
+
.split('::')
|
|
891
|
+
.pop()
|
|
892
|
+
?.split(/\s+as\s+/)[0]
|
|
893
|
+
.trim();
|
|
894
|
+
if (leaf)
|
|
895
|
+
names.push(leaf);
|
|
896
|
+
}
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
const head = prefix + rest.slice(0, brace);
|
|
900
|
+
let depth = 0;
|
|
901
|
+
let start = brace + 1;
|
|
902
|
+
for (let i = brace; i < rest.length; i++) {
|
|
903
|
+
const c = rest[i];
|
|
904
|
+
if (c === '{')
|
|
905
|
+
depth++;
|
|
906
|
+
else if (c === '}') {
|
|
907
|
+
depth--;
|
|
908
|
+
if (depth === 0)
|
|
909
|
+
return expand(head, rest.slice(start, i));
|
|
910
|
+
}
|
|
911
|
+
else if (c === ',' && depth === 1) {
|
|
912
|
+
expand(head, rest.slice(start, i));
|
|
913
|
+
start = i + 1;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
expand('', flat);
|
|
918
|
+
return { names, globs };
|
|
919
|
+
}
|
|
920
|
+
function rustSources(root) {
|
|
921
|
+
const out = [];
|
|
922
|
+
const walk = (dir) => {
|
|
923
|
+
let entries;
|
|
924
|
+
try {
|
|
925
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
926
|
+
}
|
|
927
|
+
catch {
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
for (const e of entries) {
|
|
931
|
+
if (e.isDirectory()) {
|
|
932
|
+
if (!CARGO_SKIP_DIRS.includes(e.name))
|
|
933
|
+
walk(path.join(dir, e.name));
|
|
934
|
+
}
|
|
935
|
+
else if (isRustFile(e.name))
|
|
936
|
+
out.push(path.join(dir, e.name));
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
walk(root);
|
|
940
|
+
return out;
|
|
941
|
+
}
|
|
942
|
+
const CARGO_SKIP_DIRS = ['tests', 'benches', 'examples', 'target', '.git'];
|
|
943
|
+
/** `axum-core-0.5.6/src/response/mod.rs` -> `response`, the module path a
|
|
944
|
+
* `pub use axum_core::response::*` names. `lib.rs` and `mod.rs` are the module
|
|
945
|
+
* they sit in, not a module of their own. */
|
|
946
|
+
function moduleOfPath(relPath) {
|
|
947
|
+
const parts = relPath.replace(/\\/g, '/').split('/');
|
|
948
|
+
const src = parts.indexOf('src');
|
|
949
|
+
const tail = (src === -1 ? parts : parts.slice(src + 1)).join('/').replace(/\.rs$/, '');
|
|
950
|
+
return tail
|
|
951
|
+
.split('/')
|
|
952
|
+
.filter(seg => seg !== 'mod' && seg !== 'lib')
|
|
953
|
+
.join('::');
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* The names this crate publishes through a dependency and declares nowhere.
|
|
957
|
+
*
|
|
958
|
+
* The trigger is the hole, with no threshold — measured, and for the same reason
|
|
959
|
+
* as hackage: across twenty-two crates the unresolved fraction reads 100% on a
|
|
960
|
+
* crate with one re-export and 0% on a crate with none, so a ratio separates
|
|
961
|
+
* nothing. See "Defect 16" in DOC_REGRESSINONS.md for the sweep.
|
|
962
|
+
*/
|
|
963
|
+
export function cargoExportGap(root) {
|
|
964
|
+
const deps = runtimeDeps(root);
|
|
965
|
+
const declared = new Set();
|
|
966
|
+
const reexported = new Set();
|
|
967
|
+
const globModules = new Set();
|
|
968
|
+
for (const file of rustSources(root)) {
|
|
969
|
+
const src = safeRead(file);
|
|
970
|
+
if (src === null)
|
|
971
|
+
continue;
|
|
972
|
+
for (const m of src.matchAll(RUST_DECL_RE))
|
|
973
|
+
declared.add((m[1] ?? m[2]));
|
|
974
|
+
for (const m of src.matchAll(PUB_USE_RE)) {
|
|
975
|
+
const rootSeg = m[1]
|
|
976
|
+
.replace(/#\[[^\]]*\]/g, '')
|
|
977
|
+
.trim()
|
|
978
|
+
.split(/::|\{/)[0]
|
|
979
|
+
.trim();
|
|
980
|
+
if (rootSeg === '' || OWN_PATH_ROOTS.has(rootSeg) || !deps.has(canonical(rootSeg)))
|
|
981
|
+
continue;
|
|
982
|
+
const { names, globs } = useTargets(m[1]);
|
|
983
|
+
for (const n of names)
|
|
984
|
+
if (/^[A-Za-z_]/.test(n))
|
|
985
|
+
reexported.add(n);
|
|
986
|
+
// Drop the leading crate segment: the supplier's own paths start below it.
|
|
987
|
+
for (const g of globs)
|
|
988
|
+
globModules.add(g.split('::').slice(1).join('::'));
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
const unresolved = new Set([...reexported].filter(n => !declared.has(n)));
|
|
992
|
+
return {
|
|
993
|
+
empty: unresolved.size === 0 && globModules.size === 0,
|
|
994
|
+
wholesale: relPath => globModules.has(moduleOfPath(relPath)),
|
|
995
|
+
fillsHole: chunk => {
|
|
996
|
+
for (const m of chunk.matchAll(RUST_DECL_RE)) {
|
|
997
|
+
if (unresolved.has((m[1] ?? m[2])))
|
|
998
|
+
return true;
|
|
999
|
+
}
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Source of every function `cargoExportGap` delegates to.
|
|
1006
|
+
*
|
|
1007
|
+
* `String(cargoExportGap)` covers only the top level, and the two bugs already
|
|
1008
|
+
* found in this pass — a rename split that truncated `Hasher`, a lock read from
|
|
1009
|
+
* the wrong root — both lived in helpers. A fix to one of them has to move the
|
|
1010
|
+
* index fingerprint or every cached facade keeps the chunks the old rule chose.
|
|
1011
|
+
*/
|
|
1012
|
+
export function cargoGapFingerprint() {
|
|
1013
|
+
return [cargoExportGap, runtimeDeps, useTargets, rustSources, moduleOfPath]
|
|
1014
|
+
.map(String)
|
|
1015
|
+
.concat([
|
|
1016
|
+
PUB_USE_RE.source,
|
|
1017
|
+
RUST_DECL_RE.source,
|
|
1018
|
+
CARGO_SKIP_DIRS.join(','),
|
|
1019
|
+
[...OWN_PATH_ROOTS].join(',')
|
|
1020
|
+
])
|
|
1021
|
+
.join('\u0000');
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Which declared dependencies may be opened to fill the gap.
|
|
1025
|
+
*
|
|
1026
|
+
* Cargo splits a facade from its implementation by name the way hackage does —
|
|
1027
|
+
* `axum`/`axum-core`, `futures`/`futures-util`, `tracing`/`tracing-core` — and
|
|
1028
|
+
* writes that name with either separator, so both spellings are one candidate.
|
|
1029
|
+
*
|
|
1030
|
+
* The bound's cost is stated rather than hidden: `hyper` re-exports twelve names
|
|
1031
|
+
* from `http`, `bytes` and `http-body`, and axum's own `Bytes` comes from `bytes`.
|
|
1032
|
+
* No prefix rule can see any of them.
|
|
1033
|
+
*/
|
|
1034
|
+
export function cargoSupplementCandidates(pkgName, declaredDeps, resolved) {
|
|
1035
|
+
const out = [];
|
|
1036
|
+
const seen = new Set();
|
|
1037
|
+
for (const dep of declaredDeps) {
|
|
1038
|
+
if (canonical(dep) === canonical(pkgName))
|
|
1039
|
+
continue;
|
|
1040
|
+
if (!canonical(dep).startsWith(`${canonical(pkgName)}_`))
|
|
1041
|
+
continue;
|
|
1042
|
+
const version = resolved[dep];
|
|
1043
|
+
if (!version || seen.has(canonical(dep)))
|
|
1044
|
+
continue;
|
|
1045
|
+
seen.add(canonical(dep));
|
|
1046
|
+
out.push({ name: dep, version });
|
|
1047
|
+
}
|
|
1048
|
+
// Code-unit order, not `localeCompare`: the sort decides the order supplement
|
|
1049
|
+
// chunks enter the index, and a locale-aware compare puts `-` and `_` in
|
|
1050
|
+
// different places under a different LANG.
|
|
1051
|
+
return out.sort((a, b) => a.name < b.name ? -1
|
|
1052
|
+
: a.name > b.name ? 1
|
|
1053
|
+
: 0);
|
|
1054
|
+
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { type ResolvedPackage } from './docs-resolve.js';
|
|
19
19
|
import type { NpmVersionInfo } from './npm-version.js';
|
|
20
|
+
import type { ExportGap } from './export-gap.js';
|
|
20
21
|
export declare function isValidHackageName(name: string): boolean;
|
|
21
22
|
/** True for a dotted Haskell MODULE name, which is never a package name. */
|
|
22
23
|
export declare function looksLikeModuleName(name: string): boolean;
|
|
@@ -100,29 +101,12 @@ export declare function hackageProjectName(cwd: string): string | null;
|
|
|
100
101
|
* Undefined when there is no readable `.cabal` file.
|
|
101
102
|
*/
|
|
102
103
|
export declare function manifestPackages(cwd: string): Set<string> | undefined;
|
|
103
|
-
/**
|
|
104
|
-
* What a package exports but does not declare.
|
|
105
|
-
*
|
|
106
|
-
* `hspec` indexes to 14 chunks of export lists: `it`, `describe` and `shouldBe`
|
|
107
|
-
* are in the corpus as bare names with no signature attached, because every
|
|
108
|
-
* signature is in `hspec-core`. The whole index is a table of contents.
|
|
109
|
-
*
|
|
110
|
-
* Both shapes are here because either alone misses half of it. A name-level
|
|
111
|
-
* re-export puts the name in the export list; a `module X` re-export puts
|
|
112
|
-
* nothing there at all, which is why `shouldBe` is invisible to the first.
|
|
113
|
-
*
|
|
114
|
-
* See DEFECT-12-STOPPING-RULE.md for why this triggers on the hole itself
|
|
115
|
-
* rather than on a fraction of the export list.
|
|
116
|
-
*/
|
|
117
|
-
export interface HackageExportGap {
|
|
118
|
-
/** Exported names with no declaration anywhere in the package. */
|
|
119
|
-
unresolved: Set<string>;
|
|
120
|
-
/** `module X` re-exports of modules this package does not own. */
|
|
121
|
-
reexportedModules: Set<string>;
|
|
122
|
-
}
|
|
123
104
|
/** Every name the extracted surface declares: signatures, heads, constructors, fields. */
|
|
124
105
|
export declare function declaredInSurface(surface: string): Set<string>;
|
|
125
|
-
|
|
106
|
+
/** Source of every function {@link hackageExportGap} delegates to — see
|
|
107
|
+
* `cargoGapFingerprint` for why the top level alone is not enough. */
|
|
108
|
+
export declare function hackageGapFingerprint(): string;
|
|
109
|
+
export declare function hackageExportGap(root: string): ExportGap;
|
|
126
110
|
/**
|
|
127
111
|
* Which declared dependencies may be opened to fill the gap.
|
|
128
112
|
*
|
|
@@ -586,6 +586,21 @@ export function manifestPackages(cwd) {
|
|
|
586
586
|
}
|
|
587
587
|
return out;
|
|
588
588
|
}
|
|
589
|
+
// ── re-export resolution ────────────────────────────────────────────────────
|
|
590
|
+
/**
|
|
591
|
+
* What a package exports but does not declare.
|
|
592
|
+
*
|
|
593
|
+
* `hspec` indexes to 14 chunks of export lists: `it`, `describe` and `shouldBe`
|
|
594
|
+
* are in the corpus as bare names with no signature attached, because every
|
|
595
|
+
* signature is in `hspec-core`. The whole index is a table of contents.
|
|
596
|
+
*
|
|
597
|
+
* Both shapes are here because either alone misses half of it. A name-level
|
|
598
|
+
* re-export puts the name in the export list; a `module X` re-export puts
|
|
599
|
+
* nothing there at all, which is why `shouldBe` is invisible to the first.
|
|
600
|
+
*
|
|
601
|
+
* See DEFECT-12-STOPPING-RULE.md for why this triggers on the hole itself
|
|
602
|
+
* rather than on a fraction of the export list.
|
|
603
|
+
*/
|
|
589
604
|
const EXPORT_NAME_RE = /^[A-Za-z_][\w']*$/;
|
|
590
605
|
/** `module X` inside an export list is a re-export; `Prelude` is base, and base is not fetched. */
|
|
591
606
|
const REEXPORT_RE = /\bmodule\s+([\w.']+)/g;
|
|
@@ -655,6 +670,14 @@ function haskellSources(root) {
|
|
|
655
670
|
walk(root);
|
|
656
671
|
return out;
|
|
657
672
|
}
|
|
673
|
+
/** Source of every function {@link hackageExportGap} delegates to — see
|
|
674
|
+
* `cargoGapFingerprint` for why the top level alone is not enough. */
|
|
675
|
+
export function hackageGapFingerprint() {
|
|
676
|
+
return [hackageExportGap, haskellSources, exportListText, declaredInSurface, haskellSurface]
|
|
677
|
+
.map(String)
|
|
678
|
+
.concat([REEXPORT_RE.source, EXPORT_NAME_RE.source])
|
|
679
|
+
.join('\u0000');
|
|
680
|
+
}
|
|
658
681
|
export function hackageExportGap(root) {
|
|
659
682
|
const declared = new Set();
|
|
660
683
|
const exported = new Set();
|
|
@@ -687,9 +710,14 @@ export function hackageExportGap(root) {
|
|
|
687
710
|
for (const m of ownModules)
|
|
688
711
|
reexportedModules.delete(m);
|
|
689
712
|
reexportedModules.delete('Prelude');
|
|
713
|
+
const unresolved = new Set([...exported].filter(n => !declared.has(n)));
|
|
690
714
|
return {
|
|
691
|
-
|
|
692
|
-
|
|
715
|
+
empty: unresolved.size === 0 && reexportedModules.size === 0,
|
|
716
|
+
wholesale: (_relPath, raw) => {
|
|
717
|
+
const module = /^module\s+([\w.']+)/m.exec(raw)?.[1];
|
|
718
|
+
return module !== undefined && reexportedModules.has(module);
|
|
719
|
+
},
|
|
720
|
+
fillsHole: chunk => [...declaredInSurface(chunk)].some(n => unresolved.has(n))
|
|
693
721
|
};
|
|
694
722
|
}
|
|
695
723
|
/**
|
|
@@ -713,5 +741,9 @@ export function supplementCandidates(pkgName, declaredDeps, resolved) {
|
|
|
713
741
|
if (version)
|
|
714
742
|
out.push({ name: dep, version });
|
|
715
743
|
}
|
|
716
|
-
|
|
744
|
+
// Code-unit order, not `localeCompare`: this sort decides index order and a
|
|
745
|
+
// locale-aware compare is a machine-dependent index.
|
|
746
|
+
return out.sort((a, b) => a.name < b.name ? -1
|
|
747
|
+
: a.name > b.name ? 1
|
|
748
|
+
: 0);
|
|
717
749
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a facade package publishes and does not declare.
|
|
3
|
+
*
|
|
4
|
+
* `hspec` indexes to a table of contents and every signature is in `hspec-core`;
|
|
5
|
+
* `axum` re-exports `IntoResponse` and the trait lives in `axum-core`. Both are
|
|
6
|
+
* the same failure — a query retrieves the package's own chunks and not one of
|
|
7
|
+
* them defines the thing asked about — and DEFECT-12-STOPPING-RULE.md fixes the
|
|
8
|
+
* boundary for following the re-export.
|
|
9
|
+
*
|
|
10
|
+
* The boundary is shared; the parsing is not. Haskell states the gap in an export
|
|
11
|
+
* list, Rust in `pub use`, so each ecosystem answers the same three questions in
|
|
12
|
+
* its own syntax and the indexer never learns either language.
|
|
13
|
+
*/
|
|
14
|
+
export type ExportGap = {
|
|
15
|
+
/** No hole at all, so no supplement is opened. */
|
|
16
|
+
empty: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Does this whole supplement file belong in the index? True for the target of
|
|
19
|
+
* a wholesale re-export — Haskell's `module X`, Rust's `pub use x::y::*`.
|
|
20
|
+
* Given both the path and the source because Haskell names its module in the
|
|
21
|
+
* source and Rust names it by file location.
|
|
22
|
+
*/
|
|
23
|
+
wholesale: (relPath: string, rawSource: string) => boolean;
|
|
24
|
+
/** Does this chunk declare a name the facade publishes and lacks? */
|
|
25
|
+
fillsHole: (chunkBody: string) => boolean;
|
|
26
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -56,7 +56,7 @@ export function registerPiWorkerFetch(pi, internals = {}) {
|
|
|
56
56
|
}));
|
|
57
57
|
}
|
|
58
58
|
const body = formatResultText('', // a fetched page answer carries no package header
|
|
59
|
-
{ answer: result.answer, excerpt: result.excerpt }, result.
|
|
59
|
+
{ answer: result.answer, excerpt: result.excerpt }, result.excerptCheck) || '(no output)';
|
|
60
60
|
// The coverage miss is the one outcome that carries an instruction. It goes
|
|
61
61
|
// in the TEXT, not only in details: details are for the harness, and the
|
|
62
62
|
// worker acts on what it reads.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.10",
|
|
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",
|