@mjasnikovs/pi-task 0.40.32 → 0.40.34
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/README.md +1 -1
- package/dist/shared/child-output.d.ts +1 -1
- package/dist/shared/child-output.js +1 -1
- package/dist/task/deep-render-check.d.ts +8 -6
- package/dist/task/deep-render-check.js +47 -41
- package/dist/workers/docs-core.js +4 -6
- package/dist/workers/docs-ecosystems.d.ts +1 -1
- package/dist/workers/docs-index.js +76 -15
- package/dist/workers/eco-cargo.d.ts +1 -1
- package/dist/workers/eco-cargo.js +2 -2
- package/dist/workers/eco-go.js +12 -5
- package/dist/workers/eco-hackage.js +3 -2
- package/dist/workers/export-gap.d.ts +2 -2
- package/dist/workers/go-surface.d.ts +9 -0
- package/dist/workers/go-surface.js +34 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -268,7 +268,7 @@ bun run lint # prettier + eslint + tsc --noEmit
|
|
|
268
268
|
bun run build # tsc → dist/
|
|
269
269
|
```
|
|
270
270
|
|
|
271
|
-
Built with [Bun](https://bun.sh), TypeScript (strict), and [TypeBox](https://github.com/sinclairzx81/typebox) for tool schemas.
|
|
271
|
+
Built with [Bun](https://bun.sh), TypeScript (strict), and [TypeBox](https://github.com/sinclairzx81/typebox) for tool schemas.
|
|
272
272
|
|
|
273
273
|
## License
|
|
274
274
|
|
|
@@ -65,7 +65,7 @@ export declare function verifyExcerpt(excerpt: string, content: string): Excerpt
|
|
|
65
65
|
* fabrication. An excerpt assembled from several real spans is a stitched quote,
|
|
66
66
|
* which is what the extraction prompt produces — and calling that a possible
|
|
67
67
|
* hallucination was wrong on 21 of 21 measured cases, on a fifth of every run's
|
|
68
|
-
* answers.
|
|
68
|
+
* answers.
|
|
69
69
|
*/
|
|
70
70
|
export declare function formatResultText(header: string, parsed: {
|
|
71
71
|
answer: string;
|
|
@@ -112,7 +112,7 @@ export function verifyExcerpt(excerpt, content) {
|
|
|
112
112
|
* fabrication. An excerpt assembled from several real spans is a stitched quote,
|
|
113
113
|
* which is what the extraction prompt produces — and calling that a possible
|
|
114
114
|
* hallucination was wrong on 21 of 21 measured cases, on a fifth of every run's
|
|
115
|
-
* answers.
|
|
115
|
+
* answers.
|
|
116
116
|
*/
|
|
117
117
|
export function formatResultText(header, parsed, check) {
|
|
118
118
|
if (!parsed.excerpt) {
|
|
@@ -70,7 +70,8 @@ export interface SessionRequest {
|
|
|
70
70
|
failed: boolean;
|
|
71
71
|
/** CDP resource type, collapsed: what ISSUED this request. */
|
|
72
72
|
initiator: 'xhr' | 'document' | 'other';
|
|
73
|
-
/**
|
|
73
|
+
/** Before the submit, the sign-in request itself, or issued at or after the
|
|
74
|
+
* submit. */
|
|
74
75
|
phase: 'pre' | 'auth' | 'post';
|
|
75
76
|
}
|
|
76
77
|
export interface DeepSessionFacts {
|
|
@@ -93,8 +94,8 @@ export interface DeepSessionFacts {
|
|
|
93
94
|
status: number | null;
|
|
94
95
|
failed: boolean;
|
|
95
96
|
} | null;
|
|
96
|
-
/** Same-origin XHR/fetch requests issued
|
|
97
|
-
*
|
|
97
|
+
/** Same-origin XHR/fetch requests issued at or after the submit, excluding the
|
|
98
|
+
* sign-in request itself. Derived: `sessionRequests` in phase 'post'. */
|
|
98
99
|
postAuthDataAttempted: number;
|
|
99
100
|
postAuthData2xx: number;
|
|
100
101
|
/** Origins the client called that are not the app's own, whose requests failed
|
|
@@ -112,8 +113,9 @@ export interface DeepSessionFacts {
|
|
|
112
113
|
* The three request-shaped facts, computed from the log and from nothing else. The
|
|
113
114
|
* driver records `sessionRequests` and calls this; the values are exactly what the
|
|
114
115
|
* pre-log driver computed by filtering the same map (the sign-in request is the
|
|
115
|
-
* first same-origin non-GET after submit; the data requests are the
|
|
116
|
-
*
|
|
116
|
+
* first same-origin non-GET issued at or after the submit; the data requests are the
|
|
117
|
+
* same-origin
|
|
118
|
+
* XHR/fetch issued at or after the submit, the sign-in request itself excluded).
|
|
117
119
|
*/
|
|
118
120
|
export declare function deriveLegacyFacts(log: SessionRequest[]): Pick<DeepSessionFacts, 'authRequest' | 'postAuthDataAttempted' | 'postAuthData2xx'>;
|
|
119
121
|
/**
|
|
@@ -225,7 +227,7 @@ export interface DriveSessionOptions {
|
|
|
225
227
|
/**
|
|
226
228
|
* The session over an already-connected browser: navigate, inspect, sign in if the
|
|
227
229
|
* landing is a wall and credentials exist, settle, phase the same-origin request
|
|
228
|
-
* log against the
|
|
230
|
+
* log against the submit, re-enter once the sign-in was accepted, and
|
|
229
231
|
* hand the facts to `judge`. Pure protocol logic — no process, no filesystem, no
|
|
230
232
|
* socket — so every branch is testable against a fake `CdpLike`.
|
|
231
233
|
*
|
|
@@ -202,8 +202,9 @@ export function pinnedLocalPort(vars) {
|
|
|
202
202
|
* The three request-shaped facts, computed from the log and from nothing else. The
|
|
203
203
|
* driver records `sessionRequests` and calls this; the values are exactly what the
|
|
204
204
|
* pre-log driver computed by filtering the same map (the sign-in request is the
|
|
205
|
-
* first same-origin non-GET after submit; the data requests are the
|
|
206
|
-
*
|
|
205
|
+
* first same-origin non-GET issued at or after the submit; the data requests are the
|
|
206
|
+
* same-origin
|
|
207
|
+
* XHR/fetch issued at or after the submit, the sign-in request itself excluded).
|
|
207
208
|
*/
|
|
208
209
|
export function deriveLegacyFacts(log) {
|
|
209
210
|
const auth = log.find(r => r.phase === 'auth') ?? null;
|
|
@@ -636,7 +637,7 @@ export async function launchBrowser(bin, userDataDir, { signal } = {}) {
|
|
|
636
637
|
/**
|
|
637
638
|
* The session over an already-connected browser: navigate, inspect, sign in if the
|
|
638
639
|
* landing is a wall and credentials exist, settle, phase the same-origin request
|
|
639
|
-
* log against the
|
|
640
|
+
* log against the submit, re-enter once the sign-in was accepted, and
|
|
640
641
|
* hand the facts to `judge`. Pure protocol logic — no process, no filesystem, no
|
|
641
642
|
* socket — so every branch is testable against a fake `CdpLike`.
|
|
642
643
|
*
|
|
@@ -645,18 +646,27 @@ export async function launchBrowser(bin, userDataDir, { signal } = {}) {
|
|
|
645
646
|
export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
|
|
646
647
|
const origin = new URL(url).origin;
|
|
647
648
|
const requests = new Map();
|
|
649
|
+
let nextSeq = 0;
|
|
648
650
|
let lastActivity = Date.now();
|
|
649
651
|
cdp.on('Network.requestWillBeSent', p => {
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
652
|
+
const id = String(p.requestId);
|
|
653
|
+
// A redirect hop reuses the requestId, carrying the NEXT hop's method and
|
|
654
|
+
// url. The request the client made is the first hop and the status that
|
|
655
|
+
// judges it is the chain's last, so the first hop stays and the eventual
|
|
656
|
+
// response lands on it. Overwriting reads a POST that 302s as a GET, and
|
|
657
|
+
// then no sign-in request is ever found.
|
|
658
|
+
if (p.redirectResponse === undefined || !requests.has(id)) {
|
|
659
|
+
const req = p.request;
|
|
660
|
+
requests.set(id, {
|
|
661
|
+
url: String(req?.url ?? ''),
|
|
662
|
+
method: String(req?.method ?? 'GET'),
|
|
663
|
+
type: String(p.type ?? ''),
|
|
664
|
+
status: null,
|
|
665
|
+
mimeType: null,
|
|
666
|
+
failed: false,
|
|
667
|
+
seq: nextSeq++
|
|
668
|
+
});
|
|
669
|
+
}
|
|
660
670
|
lastActivity = Date.now();
|
|
661
671
|
});
|
|
662
672
|
cdp.on('Network.responseReceived', p => {
|
|
@@ -711,27 +721,23 @@ export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
|
|
|
711
721
|
const initiatorOf = (r) => isData(r) ? 'xhr'
|
|
712
722
|
: r.type === 'Document' ? 'document'
|
|
713
723
|
: 'other';
|
|
714
|
-
/** The same-origin request log, phased against the
|
|
715
|
-
*
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
return out;
|
|
734
|
-
};
|
|
724
|
+
/** The same-origin request log, phased against the submit. `postSeq` is the
|
|
725
|
+
* first `seq` the submit could issue — Infinity while no submit has happened —
|
|
726
|
+
* so before a submit every request so far is 'pre'; the sign-in request sits at
|
|
727
|
+
* or after the boundary but is 'auth', not 'post'. */
|
|
728
|
+
const sessionLog = (authId, postSeq) => [...requests]
|
|
729
|
+
.filter(([, r]) => sameOrigin(r))
|
|
730
|
+
.map(([id, r]) => ({
|
|
731
|
+
method: r.method,
|
|
732
|
+
path: pathOf(r.url),
|
|
733
|
+
status: r.status,
|
|
734
|
+
mimeType: r.mimeType,
|
|
735
|
+
failed: r.failed,
|
|
736
|
+
initiator: initiatorOf(r),
|
|
737
|
+
phase: id === authId ? 'auth'
|
|
738
|
+
: r.seq >= postSeq ? 'post'
|
|
739
|
+
: 'pre'
|
|
740
|
+
}));
|
|
735
741
|
const facts = (log, over) => ({
|
|
736
742
|
sessionRequests: log,
|
|
737
743
|
...deriveLegacyFacts(log),
|
|
@@ -749,13 +755,15 @@ export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
|
|
|
749
755
|
const unsubmitted = (over) => facts(sessionLog(null, Number.POSITIVE_INFINITY), over);
|
|
750
756
|
if (!before.hasPassword || credentials === null)
|
|
751
757
|
return judge(unsubmitted({}));
|
|
752
|
-
const submitMark = Date.now();
|
|
753
758
|
const filled = await evaluate(fillExpr(credentials.identifier, credentials.password));
|
|
754
759
|
if (!filled?.ok)
|
|
755
760
|
return judge(unsubmitted({ submitted: false }));
|
|
756
761
|
// Separate turn: the fill's input events schedule framework state updates that
|
|
757
762
|
// the submit handler must already see.
|
|
758
763
|
await sleep(300);
|
|
764
|
+
// Captured after the fill: background traffic before the submit must not be
|
|
765
|
+
// eligible as the sign-in request.
|
|
766
|
+
const submitSeq = nextSeq;
|
|
759
767
|
const submitted = await evaluate(SUBMIT_EXPR);
|
|
760
768
|
if (!submitted?.ok)
|
|
761
769
|
return judge(unsubmitted({ submitted: false }));
|
|
@@ -765,16 +773,14 @@ export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
|
|
|
765
773
|
// own 2xx is the precondition for judging anything, and it is EXCLUDED from the
|
|
766
774
|
// data evidence — a broken build satisfies "at least one same-origin 2xx" with
|
|
767
775
|
// exactly this request and nothing else.
|
|
768
|
-
const after = new Map([...requests].filter(([, r]) => r.at >= submitMark));
|
|
769
776
|
let authId = null;
|
|
770
|
-
for (const [id, r] of
|
|
771
|
-
if (sameOrigin(r) && r.method !== 'GET') {
|
|
777
|
+
for (const [id, r] of requests) {
|
|
778
|
+
if (r.seq >= submitSeq && sameOrigin(r) && r.method !== 'GET') {
|
|
772
779
|
authId = id;
|
|
773
780
|
break;
|
|
774
781
|
}
|
|
775
782
|
}
|
|
776
|
-
const authReq = authId
|
|
777
|
-
const authAt = authReq?.at ?? submitMark;
|
|
783
|
+
const authReq = authId === null ? null : requests.get(authId);
|
|
778
784
|
const now = await evaluate(INSPECT_EXPR);
|
|
779
785
|
const domJudgment = judgeRenderedDom(now?.html ?? '');
|
|
780
786
|
const leftAuthWall = !(now?.hasPassword ?? false) || (now?.pathname ?? '') !== before.pathname;
|
|
@@ -795,7 +801,7 @@ export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
|
|
|
795
801
|
lastActivity = Date.now();
|
|
796
802
|
await settle(() => lastActivity, RE_NAV_CAP_MS, quietMs);
|
|
797
803
|
}
|
|
798
|
-
return judge(facts(sessionLog(authId,
|
|
804
|
+
return judge(facts(sessionLog(authId, submitSeq), {
|
|
799
805
|
submitted: true,
|
|
800
806
|
foreignOriginFailures: foreignOriginFailures(),
|
|
801
807
|
leftAuthWall,
|
|
@@ -549,13 +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
|
-
|
|
554
|
-
// `.d.cts` in `types`, so the entry is exactly the twin `collectFiles` drops,
|
|
555
|
-
// and prepending it blind put it back at the head of the truncated blob.
|
|
556
|
-
const entry = pkg.entry !== null && surfaceFiles.includes(pkg.entry) ? pkg.entry : null;
|
|
557
|
-
const entryFirst = entry === null ? surfaceFiles : [entry, ...surfaceFiles.filter(f => f !== entry)];
|
|
558
|
-
for (const abs of entryFirst) {
|
|
556
|
+
for (const abs of surfaceFiles) {
|
|
559
557
|
let raw;
|
|
560
558
|
try {
|
|
561
559
|
raw = fs.readFileSync(abs, 'utf8');
|
|
@@ -87,7 +87,7 @@ export interface EcosystemProfile {
|
|
|
87
87
|
/**
|
|
88
88
|
* Packages whose declarations belong in THIS package's index, because this
|
|
89
89
|
* package exports names it does not declare — `hspec`/`hspec-core`,
|
|
90
|
-
* `axum`/`axum-core
|
|
90
|
+
* `axum`/`axum-core`.
|
|
91
91
|
*/
|
|
92
92
|
supplements?: (pkg: ResolvedPackage, cwd: string, io: EcosystemIo) => Promise<ResolvedPackage[]>;
|
|
93
93
|
/**
|
|
@@ -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,7 +96,9 @@ 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];
|
|
96
103
|
// A directory symlink is followed by its resolved target, so one pointing at
|
|
97
104
|
// an ancestor inside `root` walks the same subtree forever without this.
|
|
@@ -119,17 +126,19 @@ function walkSurface(root, profile) {
|
|
|
119
126
|
catch {
|
|
120
127
|
continue;
|
|
121
128
|
}
|
|
122
|
-
|
|
123
|
-
if (relReal.startsWith('..'))
|
|
129
|
+
if (!withinPackage(root, realPath, profile))
|
|
124
130
|
continue;
|
|
125
131
|
if (stat.isDirectory()) {
|
|
126
132
|
if (walked.has(realPath))
|
|
127
133
|
continue;
|
|
128
134
|
walked.add(realPath);
|
|
129
135
|
stack.push(realPath);
|
|
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.
|
|
130
139
|
}
|
|
131
|
-
else if (stat.isFile() && profile.isSurfaceFile(
|
|
132
|
-
out.
|
|
140
|
+
else if (stat.isFile() && profile.isSurfaceFile(entry.name))
|
|
141
|
+
out.add(realPath);
|
|
133
142
|
continue;
|
|
134
143
|
}
|
|
135
144
|
if (entry.isDirectory()) {
|
|
@@ -139,10 +148,23 @@ function walkSurface(root, profile) {
|
|
|
139
148
|
stack.push(full);
|
|
140
149
|
}
|
|
141
150
|
else if (entry.isFile() && profile.isSurfaceFile(entry.name))
|
|
142
|
-
out.
|
|
151
|
+
out.add(full);
|
|
143
152
|
}
|
|
144
153
|
}
|
|
145
|
-
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));
|
|
146
168
|
}
|
|
147
169
|
/**
|
|
148
170
|
* Drop a `.d.cts` / `.d.mts` that sits beside a `.d.ts` of the same name.
|
|
@@ -200,10 +222,48 @@ function dropDeadMajors(files, root, version) {
|
|
|
200
222
|
export function collectFiles(pkg, profile) {
|
|
201
223
|
const walked = walkSurface(pkg.root, profile);
|
|
202
224
|
const surface = dropDeadMajors(walked, pkg.root, pkg.version);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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];
|
|
207
267
|
}
|
|
208
268
|
function ingestBody(cache, pkg, profile, contentHash, supplements = []) {
|
|
209
269
|
const ecosystem = profile.id;
|
|
@@ -252,8 +312,9 @@ function ingestBody(cache, pkg, profile, contentHash, supplements = []) {
|
|
|
252
312
|
}
|
|
253
313
|
}
|
|
254
314
|
// A facade package indexes to a table of contents: `hspec` is 14 chunks of
|
|
255
|
-
// export lists and every signature is in `hspec-core`. Fill only the holes
|
|
256
|
-
//
|
|
315
|
+
// export lists and every signature is in `hspec-core`. Fill only the holes,
|
|
316
|
+
// and only one hop out: after one hop hspec has zero unresolved names left,
|
|
317
|
+
// and no package measured had anything for a second hop to fetch.
|
|
257
318
|
const found = supplements.length > 0 ? (profile.exportGap?.(pkg.root) ?? null) : null;
|
|
258
319
|
const gap = found !== null && !found.empty ? found : null;
|
|
259
320
|
for (const sup of gap === null ? [] : supplements) {
|
|
@@ -122,7 +122,7 @@ export declare function manifestCrates(cwd: string): Set<string> | undefined;
|
|
|
122
122
|
* The trigger is the hole, with no threshold — measured, and for the same reason
|
|
123
123
|
* as hackage: across twenty-two crates the unresolved fraction reads 100% on a
|
|
124
124
|
* crate with one re-export and 0% on a crate with none, so a ratio separates
|
|
125
|
-
* nothing.
|
|
125
|
+
* nothing.
|
|
126
126
|
*/
|
|
127
127
|
export declare function cargoExportGap(root: string): ExportGap;
|
|
128
128
|
/**
|
|
@@ -869,7 +869,7 @@ export function manifestCrates(cwd) {
|
|
|
869
869
|
}
|
|
870
870
|
return out;
|
|
871
871
|
}
|
|
872
|
-
// ── the facade gap
|
|
872
|
+
// ── the facade gap, cargo half ──────────────────────────────────────────────
|
|
873
873
|
/** A `pub use …;` statement, attributes and line breaks included. */
|
|
874
874
|
const PUB_USE_RE = /\bpub\s+use\s+([^;]+);/g;
|
|
875
875
|
/** Every item head that introduces a name, visibility ignored — a facade may
|
|
@@ -995,7 +995,7 @@ function moduleOfPath(relPath) {
|
|
|
995
995
|
* The trigger is the hole, with no threshold — measured, and for the same reason
|
|
996
996
|
* as hackage: across twenty-two crates the unresolved fraction reads 100% on a
|
|
997
997
|
* crate with one re-export and 0% on a crate with none, so a ratio separates
|
|
998
|
-
* nothing.
|
|
998
|
+
* nothing.
|
|
999
999
|
*/
|
|
1000
1000
|
export function cargoExportGap(root) {
|
|
1001
1001
|
const deps = runtimeDeps(root);
|
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, splitGoItems } 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';
|
|
@@ -658,16 +658,22 @@ function holdsByDefault(src) {
|
|
|
658
658
|
* line, and reading those keeps the very subpackage `selectOwnPackage` exists to
|
|
659
659
|
* drop. A regex cannot draw that line — a column-0 `import (` also sits inside a
|
|
660
660
|
* generator template's raw string and inside a doc comment's example — so the
|
|
661
|
-
* split is the surface scanner's
|
|
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.
|
|
662
664
|
* An `ImportPath` is a `string_lit`, so the backtick form is legal Go and
|
|
663
665
|
* dropping it silently drops everything reachable only through it.
|
|
664
666
|
*/
|
|
665
667
|
function importsOf(src) {
|
|
666
668
|
const out = [];
|
|
667
669
|
for (const item of splitGoItems(src)) {
|
|
668
|
-
if (
|
|
670
|
+
if (/^package\b/.test(item.text))
|
|
669
671
|
continue;
|
|
670
|
-
|
|
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))
|
|
671
677
|
out.push(m[1] ?? m[2]);
|
|
672
678
|
}
|
|
673
679
|
return out;
|
|
@@ -785,6 +791,7 @@ export function goContentFingerprintParts() {
|
|
|
785
791
|
String(selectOwnPackage),
|
|
786
792
|
String(declaresApi),
|
|
787
793
|
String(goMajor),
|
|
788
|
-
String(importsOf)
|
|
794
|
+
String(importsOf),
|
|
795
|
+
String(codeOnly)
|
|
789
796
|
];
|
|
790
797
|
}
|
|
@@ -600,8 +600,9 @@ export function manifestPackages(cwd) {
|
|
|
600
600
|
* re-export puts the name in the export list; a `module X` re-export puts
|
|
601
601
|
* nothing there at all, which is why `shouldBe` is invisible to the first.
|
|
602
602
|
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
603
|
+
* The trigger is the hole, with no threshold. Swept over the 299 modules with
|
|
604
|
+
* 5+ exports, the trigger count reads 33 at 50% and 18 at 95%, so the fraction
|
|
605
|
+
* is not carrying the decision and any value picked would just sound right.
|
|
605
606
|
*/
|
|
606
607
|
const EXPORT_NAME_RE = /^[A-Za-z_][\w']*$/;
|
|
607
608
|
/** `module X` inside an export list is a re-export; `Prelude` is base, and base is not fetched. */
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* `hspec` indexes to a table of contents and every signature is in `hspec-core`;
|
|
5
5
|
* `axum` re-exports `IntoResponse` and the trait lives in `axum-core`. Both are
|
|
6
6
|
* the same failure — a query retrieves the package's own chunks and not one of
|
|
7
|
-
* them defines the thing asked about —
|
|
8
|
-
*
|
|
7
|
+
* them defines the thing asked about — so the re-export is followed exactly one
|
|
8
|
+
* hop, to a dependency the package's own name prefixes.
|
|
9
9
|
*
|
|
10
10
|
* The boundary is shared; the parsing is not. Haskell states the gap in an export
|
|
11
11
|
* list, Rust in `pub use`, so each ecosystem answers the same three questions in
|
|
@@ -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.34",
|
|
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",
|