@mjasnikovs/pi-task 0.40.31 → 0.40.33
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.
|
@@ -549,9 +549,11 @@ function docsRawCached(cache, pkg, profile, query, ensureIndexed, retrieveChunks
|
|
|
549
549
|
}
|
|
550
550
|
function docsRawUncached(pkg, profile, cacheError, autoInstalled) {
|
|
551
551
|
const parts = [];
|
|
552
|
+
// Already entry-first, and already past the selection rules: assembling that
|
|
553
|
+
// order here needed the entry compared against a list the walker spells with
|
|
554
|
+
// its own resolved paths, and a miss read as "a rule dropped it".
|
|
552
555
|
const surfaceFiles = collectFiles(pkg, profile).surface;
|
|
553
|
-
const
|
|
554
|
-
for (const abs of entryFirst) {
|
|
556
|
+
for (const abs of surfaceFiles) {
|
|
555
557
|
let raw;
|
|
556
558
|
try {
|
|
557
559
|
raw = fs.readFileSync(abs, 'utf8');
|
|
@@ -58,9 +58,14 @@ function computeContentHash(pkg, profile, supplements = []) {
|
|
|
58
58
|
hash.update(Buffer.from(chunkerFingerprint(), 'utf8'));
|
|
59
59
|
hash.update(ZERO_SEP);
|
|
60
60
|
// Source text, the same trick as `declSplitRe.source`: the fingerprint moves
|
|
61
|
-
// whenever the selection rule does, with nothing to remember to bump.
|
|
61
|
+
// whenever the selection rule does, with nothing to remember to bump. The
|
|
62
|
+
// walker is here because it decides which files EXIST to be selected, and it
|
|
63
|
+
// sits one level below `String(ingestBody)` — the level three earlier fixes
|
|
64
|
+
// hid in.
|
|
62
65
|
hash.update(Buffer.from(`${String(profile.isSurfaceFile)}\u0000${String(profile.selectFiles)}`
|
|
63
|
-
+ `\u0000${String(dropDeadMajors)}
|
|
66
|
+
+ `\u0000${String(dropDeadMajors)}\u0000${String(walkSurface)}`
|
|
67
|
+
+ `\u0000${String(withinPackage)}\u0000${String(entryFirst)}`
|
|
68
|
+
+ `\u0000${profile.skipDirs.join(',')}`, 'utf8'));
|
|
64
69
|
hash.update(ZERO_SEP);
|
|
65
70
|
// The extractor and the writer, by source. Surfacing only `pkg.entry` below
|
|
66
71
|
// leaves a package cached whenever a fix moves some OTHER module — the
|
|
@@ -91,8 +96,13 @@ function computeContentHash(pkg, profile, supplements = []) {
|
|
|
91
96
|
return hash.digest('hex');
|
|
92
97
|
}
|
|
93
98
|
function walkSurface(root, profile) {
|
|
94
|
-
|
|
99
|
+
// A set, not a list: two links to one file are one file, and the uncached
|
|
100
|
+
// path prints this list into a single already-truncated blob.
|
|
101
|
+
const out = new Set();
|
|
95
102
|
const stack = [root];
|
|
103
|
+
// A directory symlink is followed by its resolved target, so one pointing at
|
|
104
|
+
// an ancestor inside `root` walks the same subtree forever without this.
|
|
105
|
+
const walked = new Set([root]);
|
|
96
106
|
while (stack.length) {
|
|
97
107
|
const dir = stack.pop();
|
|
98
108
|
let entries;
|
|
@@ -107,30 +117,54 @@ function walkSurface(root, profile) {
|
|
|
107
117
|
continue;
|
|
108
118
|
const full = path.join(dir, entry.name);
|
|
109
119
|
if (entry.isSymbolicLink()) {
|
|
120
|
+
let stat;
|
|
110
121
|
let realPath;
|
|
111
122
|
try {
|
|
112
123
|
realPath = fs.realpathSync(full);
|
|
124
|
+
stat = fs.statSync(realPath);
|
|
113
125
|
}
|
|
114
126
|
catch {
|
|
115
127
|
continue;
|
|
116
128
|
}
|
|
117
|
-
|
|
118
|
-
if (relReal.startsWith('..'))
|
|
129
|
+
if (!withinPackage(root, realPath, profile))
|
|
119
130
|
continue;
|
|
120
|
-
|
|
121
|
-
|
|
131
|
+
if (stat.isDirectory()) {
|
|
132
|
+
if (walked.has(realPath))
|
|
133
|
+
continue;
|
|
134
|
+
walked.add(realPath);
|
|
122
135
|
stack.push(realPath);
|
|
123
|
-
|
|
124
|
-
|
|
136
|
+
// Surface-ness is the visible name's, identity is the target's:
|
|
137
|
+
// `index.d.ts -> src/impl.ts` is what a consumer imports, and
|
|
138
|
+
// `helpers_test.go -> helpers.go` is still a test file.
|
|
139
|
+
}
|
|
140
|
+
else if (stat.isFile() && profile.isSurfaceFile(entry.name))
|
|
141
|
+
out.add(realPath);
|
|
125
142
|
continue;
|
|
126
143
|
}
|
|
127
|
-
if (entry.isDirectory())
|
|
144
|
+
if (entry.isDirectory()) {
|
|
145
|
+
if (walked.has(full))
|
|
146
|
+
continue;
|
|
147
|
+
walked.add(full);
|
|
128
148
|
stack.push(full);
|
|
149
|
+
}
|
|
129
150
|
else if (entry.isFile() && profile.isSurfaceFile(entry.name))
|
|
130
|
-
out.
|
|
151
|
+
out.add(full);
|
|
131
152
|
}
|
|
132
153
|
}
|
|
133
|
-
return out.sort();
|
|
154
|
+
return [...out].sort();
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Is this symlink target part of the package, by the same rules its own tree obeys?
|
|
158
|
+
*
|
|
159
|
+
* The link's NAME cleared `skipDirs`; the path it resolves to has to as well, or
|
|
160
|
+
* `deps -> node_modules` files another package's declarations under this one's
|
|
161
|
+
* name and version banner.
|
|
162
|
+
*/
|
|
163
|
+
function withinPackage(root, realPath, profile) {
|
|
164
|
+
const rel = path.relative(root, realPath);
|
|
165
|
+
if (rel.startsWith('..'))
|
|
166
|
+
return false;
|
|
167
|
+
return !rel.split(path.sep).some(seg => profile.skipDirs.includes(seg));
|
|
134
168
|
}
|
|
135
169
|
/**
|
|
136
170
|
* Drop a `.d.cts` / `.d.mts` that sits beside a `.d.ts` of the same name.
|
|
@@ -188,10 +222,48 @@ function dropDeadMajors(files, root, version) {
|
|
|
188
222
|
export function collectFiles(pkg, profile) {
|
|
189
223
|
const walked = walkSurface(pkg.root, profile);
|
|
190
224
|
const surface = dropDeadMajors(walked, pkg.root, pkg.version);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
225
|
+
const selected = profile.selectFiles ? profile.selectFiles(surface, pkg) : surface;
|
|
226
|
+
return { surface: entryFirst(selected, walked, pkg), readme: pkg.readme };
|
|
227
|
+
}
|
|
228
|
+
/** The list's own spelling of `entry`, resolving links only if the plain compare misses. */
|
|
229
|
+
function sameFile(files, entry) {
|
|
230
|
+
const direct = files.find(f => f === entry);
|
|
231
|
+
if (direct !== undefined)
|
|
232
|
+
return direct;
|
|
233
|
+
const real = realpathOr(entry);
|
|
234
|
+
return files.find(f => f === real || realpathOr(f) === real) ?? null;
|
|
235
|
+
}
|
|
236
|
+
function realpathOr(file) {
|
|
237
|
+
try {
|
|
238
|
+
return fs.realpathSync(file);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return file;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* The manifest's entry at the head, and back in the list when the walk never
|
|
246
|
+
* offered it.
|
|
247
|
+
*
|
|
248
|
+
* Two different questions, and membership alone cannot tell them apart. A rule
|
|
249
|
+
* that DROPPED the entry is doing its job — a CJS-first package names the
|
|
250
|
+
* `.d.cts` twin `dropParallelDeclarations` removes, and putting it back blind
|
|
251
|
+
* put it at the head of the truncated blob. A package naming a `src/index.ts`
|
|
252
|
+
* in `types` was never a surface-file candidate at all, and answering "has no
|
|
253
|
+
* .d.ts files" for it is not the same as answering without one file.
|
|
254
|
+
*
|
|
255
|
+
* The head matters on the uncached path, which prints this list into one
|
|
256
|
+
* truncated blob and nothing else.
|
|
257
|
+
*/
|
|
258
|
+
function entryFirst(selected, walked, pkg) {
|
|
259
|
+
if (pkg.entry === null)
|
|
260
|
+
return [...selected];
|
|
261
|
+
const kept = sameFile(selected, pkg.entry);
|
|
262
|
+
if (kept !== null)
|
|
263
|
+
return [kept, ...selected.filter(f => f !== kept)];
|
|
264
|
+
if (sameFile(walked, pkg.entry) !== null)
|
|
265
|
+
return [...selected];
|
|
266
|
+
return fs.existsSync(pkg.entry) ? [pkg.entry, ...selected] : [...selected];
|
|
195
267
|
}
|
|
196
268
|
function ingestBody(cache, pkg, profile, contentHash, supplements = []) {
|
|
197
269
|
const ecosystem = profile.id;
|
package/dist/workers/eco-go.js
CHANGED
|
@@ -25,7 +25,7 @@ import * as os from 'node:os';
|
|
|
25
25
|
import * as path from 'node:path';
|
|
26
26
|
import { ResolveError } from './docs-resolve.js';
|
|
27
27
|
import { findAtOrAbove } from './eco-cargo.js';
|
|
28
|
-
import { buildConstraint } from './go-surface.js';
|
|
28
|
+
import { buildConstraint, codeOnly, splitGoItems } from './go-surface.js';
|
|
29
29
|
import { readZip, readEntry, isUnsafeEntryName } from '../shared/zip.js';
|
|
30
30
|
import { acquireStdlibPackage, findInGoroot, findSliced } from './go-stdlib.js';
|
|
31
31
|
const PROXY = 'https://proxy.golang.org';
|
|
@@ -653,17 +653,27 @@ function holdsByDefault(src) {
|
|
|
653
653
|
/**
|
|
654
654
|
* The import paths a Go file's import block names.
|
|
655
655
|
*
|
|
656
|
-
* Scoped to the import
|
|
656
|
+
* Scoped to the import DECLARATION, not the file: a sibling's import path also
|
|
657
657
|
* occurs as a plain string constant, in an error message and in a `go:generate`
|
|
658
658
|
* line, and reading those keeps the very subpackage `selectOwnPackage` exists to
|
|
659
|
-
* drop.
|
|
659
|
+
* drop. A regex cannot draw that line — a column-0 `import (` also sits inside a
|
|
660
|
+
* generator template's raw string and inside a doc comment's example — so the
|
|
661
|
+
* split is the surface scanner's. That split strips only the comments BETWEEN
|
|
662
|
+
* declarations, and a commented-out import line inside the block is ordinary Go,
|
|
663
|
+
* so the item's own comments come off too.
|
|
664
|
+
* An `ImportPath` is a `string_lit`, so the backtick form is legal Go and
|
|
660
665
|
* dropping it silently drops everything reachable only through it.
|
|
661
666
|
*/
|
|
662
667
|
function importsOf(src) {
|
|
663
668
|
const out = [];
|
|
664
|
-
for (const
|
|
665
|
-
|
|
666
|
-
|
|
669
|
+
for (const item of splitGoItems(src)) {
|
|
670
|
+
if (/^package\b/.test(item.text))
|
|
671
|
+
continue;
|
|
672
|
+
// Go puts every import declaration before every other one, so the first
|
|
673
|
+
// item that is not an import ends the search.
|
|
674
|
+
if (!/^import\b/.test(item.text))
|
|
675
|
+
break;
|
|
676
|
+
for (const m of codeOnly(item.text).matchAll(/"([^"\n]+)"|`([^`]+)`/g))
|
|
667
677
|
out.push(m[1] ?? m[2]);
|
|
668
678
|
}
|
|
669
679
|
return out;
|
|
@@ -683,9 +693,16 @@ function goMajor(name, version) {
|
|
|
683
693
|
const inVersion = /^(?:v|go)?(\d+)\./.exec(version);
|
|
684
694
|
return inVersion ? Number(inVersion[1]) : 1;
|
|
685
695
|
}
|
|
686
|
-
/**
|
|
696
|
+
/**
|
|
697
|
+
* Does this file declare anything a caller of the package could be handed?
|
|
698
|
+
*
|
|
699
|
+
* Read from the scanned declarations, not the raw text. `cloud.google.com/go`'s
|
|
700
|
+
* doc.go is one block comment, and a wrapped sentence beginning `type ` sits at
|
|
701
|
+
* column 0 — enough for a multiline regex to call the root the API and drop the
|
|
702
|
+
* subdirectories that hold all of it.
|
|
703
|
+
*/
|
|
687
704
|
function declaresApi(src) {
|
|
688
|
-
return /^(?:func|type|var|const)\b
|
|
705
|
+
return splitGoItems(src).some(item => /^(?:func|type|var|const)\b/.test(item.text));
|
|
689
706
|
}
|
|
690
707
|
/**
|
|
691
708
|
* Keep the package the caller asked for, and only the subpackages it can reach.
|
|
@@ -729,7 +746,16 @@ export function selectOwnPackage(files, root, name, version) {
|
|
|
729
746
|
const byDir = new Map();
|
|
730
747
|
for (const f of files)
|
|
731
748
|
byDir.set(dirOf(f), [...(byDir.get(dirOf(f)) ?? []), f]);
|
|
732
|
-
|
|
749
|
+
const sources = new Map();
|
|
750
|
+
const sourceOf = (f) => {
|
|
751
|
+
const seen = sources.get(f);
|
|
752
|
+
if (seen !== undefined)
|
|
753
|
+
return seen;
|
|
754
|
+
const text = safeRead(f) ?? '';
|
|
755
|
+
sources.set(f, text);
|
|
756
|
+
return text;
|
|
757
|
+
};
|
|
758
|
+
if (!(byDir.get('') ?? []).some(f => declaresApi(sourceOf(f))))
|
|
733
759
|
return [...files];
|
|
734
760
|
const major = goMajor(name, version);
|
|
735
761
|
const reachable = new Set(['']);
|
|
@@ -737,7 +763,7 @@ export function selectOwnPackage(files, root, name, version) {
|
|
|
737
763
|
while (queue.length > 0) {
|
|
738
764
|
const dir = queue.shift();
|
|
739
765
|
for (const f of byDir.get(dir) ?? []) {
|
|
740
|
-
for (const imp of importsOf(
|
|
766
|
+
for (const imp of importsOf(sourceOf(f))) {
|
|
741
767
|
if (!imp.startsWith(`${name}/`))
|
|
742
768
|
continue;
|
|
743
769
|
const sub = imp.slice(name.length + 1);
|
|
@@ -765,6 +791,7 @@ export function goContentFingerprintParts() {
|
|
|
765
791
|
String(selectOwnPackage),
|
|
766
792
|
String(declaresApi),
|
|
767
793
|
String(goMajor),
|
|
768
|
-
String(importsOf)
|
|
794
|
+
String(importsOf),
|
|
795
|
+
String(codeOnly)
|
|
769
796
|
];
|
|
770
797
|
}
|
|
@@ -38,6 +38,15 @@ export interface GoItem {
|
|
|
38
38
|
pending: string;
|
|
39
39
|
text: string;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* A declaration's text with its comments removed and its literals intact.
|
|
43
|
+
*
|
|
44
|
+
* `splitGoItems` strips only what sits BETWEEN declarations. A comment inside an
|
|
45
|
+
* import block, or trailing one, stays in the item's own text, so a caller
|
|
46
|
+
* reading string literals out of a declaration reads the commented-out ones too
|
|
47
|
+
* — and a commented-out import is one of the commonest shapes in Go source.
|
|
48
|
+
*/
|
|
49
|
+
export declare function codeOnly(text: string): string;
|
|
41
50
|
/**
|
|
42
51
|
* Split source into declarations. Works unchanged on a struct body or a const
|
|
43
52
|
* group, whose members obey the same semicolon rule with no keyword in front.
|
|
@@ -162,6 +162,39 @@ function endsStatement(word, lastChar) {
|
|
|
162
162
|
return !CONTINUING_KEYWORDS.has(word);
|
|
163
163
|
return STMT_END_CHAR_RE.test(lastChar);
|
|
164
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* A declaration's text with its comments removed and its literals intact.
|
|
167
|
+
*
|
|
168
|
+
* `splitGoItems` strips only what sits BETWEEN declarations. A comment inside an
|
|
169
|
+
* import block, or trailing one, stays in the item's own text, so a caller
|
|
170
|
+
* reading string literals out of a declaration reads the commented-out ones too
|
|
171
|
+
* — and a commented-out import is one of the commonest shapes in Go source.
|
|
172
|
+
*/
|
|
173
|
+
export function codeOnly(text) {
|
|
174
|
+
let out = '';
|
|
175
|
+
let i = 0;
|
|
176
|
+
while (i < text.length) {
|
|
177
|
+
const c = text[i];
|
|
178
|
+
if (c === '/' && text[i + 1] === '/') {
|
|
179
|
+
const nl = text.indexOf('\n', i);
|
|
180
|
+
i = nl < 0 ? text.length : nl;
|
|
181
|
+
}
|
|
182
|
+
else if (c === '/' && text[i + 1] === '*') {
|
|
183
|
+
const close = text.indexOf('*/', i + 2);
|
|
184
|
+
i = close < 0 ? text.length : close + 2;
|
|
185
|
+
}
|
|
186
|
+
else if (c === '"' || c === "'" || c === '`') {
|
|
187
|
+
const end = skipLiteral(text, i);
|
|
188
|
+
out += text.slice(i, end);
|
|
189
|
+
i = end;
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
out += c;
|
|
193
|
+
i++;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
165
198
|
/**
|
|
166
199
|
* Split source into declarations. Works unchanged on a struct body or a const
|
|
167
200
|
* group, whose members obey the same semicolon rule with no keyword in front.
|
|
@@ -530,6 +563,7 @@ export function goContentFingerprint() {
|
|
|
530
563
|
endsStatement,
|
|
531
564
|
skipToCode,
|
|
532
565
|
skipLiteral,
|
|
566
|
+
codeOnly,
|
|
533
567
|
trailingGroup,
|
|
534
568
|
splitBody,
|
|
535
569
|
buildConstraint,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.33",
|
|
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",
|