@mjasnikovs/pi-task 0.40.37 → 0.40.39
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.
|
@@ -215,6 +215,7 @@ export function runChild(spawn, invocation, cwd, signal, opts) {
|
|
|
215
215
|
let stdout = '';
|
|
216
216
|
let stderr = '';
|
|
217
217
|
let kill;
|
|
218
|
+
let stdinError;
|
|
218
219
|
const discardStdout = opts?.mode === 'text' && opts.discardStdout === true;
|
|
219
220
|
// Deliver the prompt on stdin, not argv. A large prompt — an inlined design
|
|
220
221
|
// doc, say — exceeds the OS argv ceiling and the spawn fails outright rather
|
|
@@ -237,9 +238,13 @@ export function runChild(spawn, invocation, cwd, signal, opts) {
|
|
|
237
238
|
});
|
|
238
239
|
if (usesStdin) {
|
|
239
240
|
// A child killed before it read the prompt leaves the pipe broken, and an
|
|
240
|
-
// unhandled EPIPE on stdin takes the whole process down.
|
|
241
|
-
//
|
|
242
|
-
|
|
241
|
+
// unhandled EPIPE on stdin takes the whole process down. Recorded rather
|
|
242
|
+
// than dropped: a child WE killed reports itself through `kill`, but one
|
|
243
|
+
// that read a truncated prompt and exited 0 would otherwise pass off an
|
|
244
|
+
// answer to half a spec as a clean run.
|
|
245
|
+
proc.stdin?.on?.('error', (e) => {
|
|
246
|
+
stdinError ??= e instanceof Error ? e : new Error(String(e));
|
|
247
|
+
});
|
|
243
248
|
// pi reads the prompt from stdin and waits for EOF, so write then end.
|
|
244
249
|
proc.stdin?.write(invocation.stdin);
|
|
245
250
|
proc.stdin?.end();
|
|
@@ -405,10 +410,14 @@ export function runChild(spawn, invocation, cwd, signal, opts) {
|
|
|
405
410
|
if (sink)
|
|
406
411
|
sink.flush();
|
|
407
412
|
const text = sink ? sink.text : undefined;
|
|
413
|
+
// pi waits for EOF before it runs, so a stdin error on a child nobody
|
|
414
|
+
// killed means it ran on a prompt that never finished arriving. Its own
|
|
415
|
+
// exit describes that half-spec, so it cannot stand as the verdict.
|
|
416
|
+
const truncated = kill === undefined ? stdinError : undefined;
|
|
408
417
|
settle({
|
|
409
418
|
stdout,
|
|
410
|
-
stderr,
|
|
411
|
-
exitCode: code ?? 0,
|
|
419
|
+
stderr: truncated ? `${stderr}\nprompt delivery failed: ${truncated.message}` : stderr,
|
|
420
|
+
exitCode: truncated ? (code ?? 0) || 1 : (code ?? 0),
|
|
412
421
|
aborted: kill !== undefined,
|
|
413
422
|
...(kill ? { kill } : {}),
|
|
414
423
|
text,
|
|
@@ -65,13 +65,19 @@ export declare function pinnedLocalPort(vars: Record<string, string>): number |
|
|
|
65
65
|
export interface SessionRequest {
|
|
66
66
|
method: string;
|
|
67
67
|
path: string;
|
|
68
|
+
/** Where the chain ENDED — the path `status` and `mimeType` actually describe.
|
|
69
|
+
* Absent in a pre-existing recorded session, where it falls back to `path`. */
|
|
70
|
+
finalPath?: string;
|
|
68
71
|
status: number | null;
|
|
69
72
|
mimeType: string | null;
|
|
70
73
|
failed: boolean;
|
|
71
74
|
/** CDP resource type, collapsed: what ISSUED this request. */
|
|
72
75
|
initiator: 'xhr' | 'document' | 'other';
|
|
73
|
-
/**
|
|
74
|
-
*
|
|
76
|
+
/** The chain redirected, so `status` and `mimeType` above describe a hop this
|
|
77
|
+
* entry does not name. Any rule that reads a status AS A FACT ABOUT `path`
|
|
78
|
+
* must skip these. */
|
|
79
|
+
redirected: boolean;
|
|
80
|
+
/** Before the sign-in request, the sign-in request itself, or after it. */
|
|
75
81
|
phase: 'pre' | 'auth' | 'post';
|
|
76
82
|
}
|
|
77
83
|
export interface DeepSessionFacts {
|
|
@@ -93,11 +99,17 @@ export interface DeepSessionFacts {
|
|
|
93
99
|
path: string;
|
|
94
100
|
status: number | null;
|
|
95
101
|
failed: boolean;
|
|
102
|
+
/** The sign-in chain redirected, so `status` is the hop it LANDED on. */
|
|
103
|
+
redirected?: boolean;
|
|
96
104
|
} | null;
|
|
97
|
-
/** Same-origin XHR/fetch requests issued at or after the
|
|
98
|
-
*
|
|
105
|
+
/** Same-origin XHR/fetch requests issued at or after the sign-in request,
|
|
106
|
+
* excluding that request itself. Derived: `sessionRequests` in phase 'post'. */
|
|
99
107
|
postAuthDataAttempted: number;
|
|
100
108
|
postAuthData2xx: number;
|
|
109
|
+
/** The origin the sign-in left for, when the submit addressed this app and the
|
|
110
|
+
* chain ended somewhere else — an external identity provider, which no
|
|
111
|
+
* declared credential pair can drive. Optional: absent means "not recorded". */
|
|
112
|
+
signInLeftOrigin?: string | null;
|
|
101
113
|
/** Origins the client called that are not the app's own, whose requests failed
|
|
102
114
|
* (a bundle pinned to a build-time base URL that is not the port under test). */
|
|
103
115
|
foreignOriginFailures: string[];
|
|
@@ -111,11 +123,8 @@ export interface DeepSessionFacts {
|
|
|
111
123
|
}
|
|
112
124
|
/**
|
|
113
125
|
* The three request-shaped facts, computed from the log and from nothing else. The
|
|
114
|
-
* driver records `sessionRequests` and calls this; the
|
|
115
|
-
*
|
|
116
|
-
* first same-origin non-GET issued at or after the submit; the data requests are
|
|
117
|
-
* the same-origin XHR/fetch issued at or after the sign-in request, that request
|
|
118
|
-
* itself excluded).
|
|
126
|
+
* driver records `sessionRequests` and calls this; the phases carry the whole
|
|
127
|
+
* derivation, so this reads them and decides nothing of its own.
|
|
119
128
|
*/
|
|
120
129
|
export declare function deriveLegacyFacts(log: SessionRequest[]): Pick<DeepSessionFacts, 'authRequest' | 'postAuthDataAttempted' | 'postAuthData2xx'>;
|
|
121
130
|
/**
|
|
@@ -227,7 +236,7 @@ export interface DriveSessionOptions {
|
|
|
227
236
|
/**
|
|
228
237
|
* The session over an already-connected browser: navigate, inspect, sign in if the
|
|
229
238
|
* landing is a wall and credentials exist, settle, phase the same-origin request
|
|
230
|
-
* log against the
|
|
239
|
+
* log against the sign-in request, re-enter once the sign-in was accepted, and
|
|
231
240
|
* hand the facts to `judge`. Pure protocol logic — no process, no filesystem, no
|
|
232
241
|
* socket — so every branch is testable against a fake `CdpLike`.
|
|
233
242
|
*
|
|
@@ -200,17 +200,20 @@ export function pinnedLocalPort(vars) {
|
|
|
200
200
|
}
|
|
201
201
|
/**
|
|
202
202
|
* The three request-shaped facts, computed from the log and from nothing else. The
|
|
203
|
-
* driver records `sessionRequests` and calls this; the
|
|
204
|
-
*
|
|
205
|
-
* first same-origin non-GET issued at or after the submit; the data requests are
|
|
206
|
-
* the same-origin XHR/fetch issued at or after the sign-in request, that request
|
|
207
|
-
* itself excluded).
|
|
203
|
+
* driver records `sessionRequests` and calls this; the phases carry the whole
|
|
204
|
+
* derivation, so this reads them and decides nothing of its own.
|
|
208
205
|
*/
|
|
209
206
|
export function deriveLegacyFacts(log) {
|
|
210
207
|
const auth = log.find(r => r.phase === 'auth') ?? null;
|
|
211
208
|
const data = log.filter(r => r.phase === 'post' && r.initiator === 'xhr');
|
|
212
209
|
return {
|
|
213
|
-
authRequest: auth === null ? null : ({
|
|
210
|
+
authRequest: auth === null ? null : ({
|
|
211
|
+
method: auth.method,
|
|
212
|
+
path: auth.path,
|
|
213
|
+
status: auth.status,
|
|
214
|
+
failed: auth.failed,
|
|
215
|
+
redirected: auth.redirected
|
|
216
|
+
}),
|
|
214
217
|
postAuthDataAttempted: data.length,
|
|
215
218
|
postAuthData2xx: data.filter(r => r.status !== null && r.status >= 200 && r.status < 300)
|
|
216
219
|
.length
|
|
@@ -220,6 +223,11 @@ export function deriveLegacyFacts(log) {
|
|
|
220
223
|
* answered No. 501 is included because a server that routes but implements
|
|
221
224
|
* nothing is the same dead call from the client's side. */
|
|
222
225
|
const MISSING_ROUTE_STATUS = new Set([404, 405, 501]);
|
|
226
|
+
/** A redirected entry's `status` and `mimeType` belong to its LAST hop, so the two
|
|
227
|
+
* rules below may read them only where that hop is still the client's own business.
|
|
228
|
+
* Before and during sign-in, a 302 to the login page is the normal unauthenticated
|
|
229
|
+
* flow; after sign-in, that same redirect IS the defect. */
|
|
230
|
+
const lastHopJudgesTheClient = (r) => !r.redirected || r.phase === 'post';
|
|
223
231
|
/**
|
|
224
232
|
* Judge a recorded session. The ONE thing that may FAIL is a session the SERVER
|
|
225
233
|
* authenticated (2xx on the sign-in request) whose client then could not use it:
|
|
@@ -264,6 +272,14 @@ export function judgeDeepSession(f) {
|
|
|
264
272
|
};
|
|
265
273
|
}
|
|
266
274
|
if (f.authRequest === null) {
|
|
275
|
+
if (f.signInLeftOrigin) {
|
|
276
|
+
return {
|
|
277
|
+
outcome: 'skip',
|
|
278
|
+
note: `signing in leaves this app for ${f.signInLeftOrigin} — an external identity `
|
|
279
|
+
+ 'provider cannot be driven with a declared credential pair, so the '
|
|
280
|
+
+ 'authenticated half of the app was NOT observed'
|
|
281
|
+
};
|
|
282
|
+
}
|
|
267
283
|
const pinned = f.foreignOriginFailures.length > 0 ?
|
|
268
284
|
` — the client calls ${f.foreignOriginFailures.join(', ')}, not the origin under test (a base URL baked in at build time)`
|
|
269
285
|
: '';
|
|
@@ -277,25 +293,29 @@ export function judgeDeepSession(f) {
|
|
|
277
293
|
// Deliberately narrow: 400/401/403/419/422 are a handler ANSWERING (a rejected
|
|
278
294
|
// password, a missing permission — the app working), 5xx is the server failing,
|
|
279
295
|
// and both keep their existing behaviour. Only "no such route" is here.
|
|
280
|
-
const missingRoute = (f.sessionRequests ?? []).find(r => r.initiator === 'xhr'
|
|
296
|
+
const missingRoute = (f.sessionRequests ?? []).find(r => r.initiator === 'xhr'
|
|
297
|
+
&& lastHopJudgesTheClient(r)
|
|
298
|
+
&& r.status !== null
|
|
299
|
+
&& MISSING_ROUTE_STATUS.has(r.status));
|
|
281
300
|
if (missingRoute) {
|
|
301
|
+
const landed = missingRoute.finalPath ?? missingRoute.path;
|
|
302
|
+
const hop = landed === missingRoute.path ? '' : ` → \`${landed}\``;
|
|
282
303
|
return {
|
|
283
304
|
outcome: 'fail',
|
|
284
|
-
detail: `\`${missingRoute.method} ${missingRoute.path}
|
|
285
|
-
+ 'the client sent this to a path the server does not route.
|
|
286
|
-
+ '
|
|
287
|
-
+ '
|
|
305
|
+
detail: `\`${missingRoute.method} ${missingRoute.path}\`${hop} → ${String(missingRoute.status)}: `
|
|
306
|
+
+ 'the client sent this to a path the server does not route. This is a dead client '
|
|
307
|
+
+ 'call — a base URL joined twice, a renamed route, a wrong method. No type or mock '
|
|
308
|
+
+ 'can produce a route that is not mounted.'
|
|
288
309
|
};
|
|
289
310
|
}
|
|
290
311
|
// Rule B — the SPA catch-all answering an API call. Any server with a
|
|
291
312
|
// `GET /*` → index.html fallback returns 200 for a route it does not have, so
|
|
292
313
|
// the status is healthy and the body is the app shell. A document navigation
|
|
293
314
|
// answered with HTML is normal; an XHR asking for data and getting HTML is a
|
|
294
|
-
// call that reached nothing.
|
|
295
|
-
//
|
|
296
|
-
// reads as HTML here while being a sign-in that worked.
|
|
315
|
+
// call that reached nothing. A fetch login that 302s to a page reads as HTML
|
|
316
|
+
// while being a sign-in that worked, hence the last-hop guard.
|
|
297
317
|
const swallowed = (f.sessionRequests ?? []).find(r => r.initiator === 'xhr'
|
|
298
|
-
&& r
|
|
318
|
+
&& lastHopJudgesTheClient(r)
|
|
299
319
|
&& (r.mimeType ?? '').startsWith('text/html'));
|
|
300
320
|
if (swallowed) {
|
|
301
321
|
return {
|
|
@@ -306,7 +326,7 @@ export function judgeDeepSession(f) {
|
|
|
306
326
|
+ 'sees a 200 it cannot parse. No status check can see this.'
|
|
307
327
|
};
|
|
308
328
|
}
|
|
309
|
-
const { method, path: p, status, failed } = f.authRequest;
|
|
329
|
+
const { method, path: p, status, failed, redirected: authRedirected } = f.authRequest;
|
|
310
330
|
if (failed || status === null || status < 200 || status >= 300) {
|
|
311
331
|
return {
|
|
312
332
|
outcome: 'skip',
|
|
@@ -317,6 +337,18 @@ export function judgeDeepSession(f) {
|
|
|
317
337
|
}
|
|
318
338
|
const signedIn = `signed in (\`${method} ${p}\` → ${status})`;
|
|
319
339
|
if (!f.leftAuthWall) {
|
|
340
|
+
// A form sign-in that redirects lands its status on the hop it reached, so a
|
|
341
|
+
// 200 here is the landing page's, not a verdict on the credentials. Landing
|
|
342
|
+
// back on the wall is what a REJECTED password looks like, and the gate's one
|
|
343
|
+
// FAIL needs the server to have said yes.
|
|
344
|
+
if (authRedirected === true) {
|
|
345
|
+
return {
|
|
346
|
+
outcome: 'skip',
|
|
347
|
+
note: `the sign-in request (\`${method} ${p}\`) redirected, so its ${String(status)} `
|
|
348
|
+
+ 'describes the page it landed on and not the credentials, and the client is '
|
|
349
|
+
+ 'still on the wall — the authenticated half of the app was NOT observed'
|
|
350
|
+
};
|
|
351
|
+
}
|
|
320
352
|
return {
|
|
321
353
|
outcome: 'fail',
|
|
322
354
|
detail: `${signedIn} but the client NEVER LEFT THE SIGN-IN WALL: the page is still `
|
|
@@ -641,7 +673,7 @@ export async function launchBrowser(bin, userDataDir, { signal } = {}) {
|
|
|
641
673
|
/**
|
|
642
674
|
* The session over an already-connected browser: navigate, inspect, sign in if the
|
|
643
675
|
* landing is a wall and credentials exist, settle, phase the same-origin request
|
|
644
|
-
* log against the
|
|
676
|
+
* log against the sign-in request, re-enter once the sign-in was accepted, and
|
|
645
677
|
* hand the facts to `judge`. Pure protocol logic — no process, no filesystem, no
|
|
646
678
|
* socket — so every branch is testable against a fake `CdpLike`.
|
|
647
679
|
*
|
|
@@ -712,18 +744,17 @@ export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
|
|
|
712
744
|
if (!before)
|
|
713
745
|
throw new Error('the page could not be inspected');
|
|
714
746
|
const sameOrigin = (r) => r.finalUrl.startsWith(`${origin}/`) || r.finalUrl === origin;
|
|
747
|
+
/** Which origin the client ASKED for — the first hop, before any redirect. */
|
|
748
|
+
const addressedOrigin = (r) => r.url.startsWith(`${origin}/`) || r.url === origin;
|
|
715
749
|
const isData = (r) => r.type === 'XHR' || r.type === 'Fetch';
|
|
716
750
|
const foreignOriginFailures = () => {
|
|
717
751
|
const out = new Set();
|
|
718
752
|
for (const r of requests.values()) {
|
|
719
753
|
if (sameOrigin(r) || !r.failed || !r.finalUrl.startsWith('http'))
|
|
720
754
|
continue;
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
catch {
|
|
725
|
-
// unparseable url — nothing to name
|
|
726
|
-
}
|
|
755
|
+
const o = originOf(r.finalUrl);
|
|
756
|
+
if (o)
|
|
757
|
+
out.add(o);
|
|
727
758
|
}
|
|
728
759
|
return [...out];
|
|
729
760
|
};
|
|
@@ -735,19 +766,27 @@ export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
|
|
|
735
766
|
* 'pre'; the sign-in request itself is 'auth', not 'post'. The boundary is the
|
|
736
767
|
* sign-in request, not the submit: a CSRF token or a beacon the submit fires
|
|
737
768
|
* BEFORE the login is not evidence about the authenticated client. */
|
|
738
|
-
const sessionLog = (authId, postSeq) =>
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
:
|
|
750
|
-
|
|
769
|
+
const sessionLog = (authId, postSeq) => {
|
|
770
|
+
const out = [];
|
|
771
|
+
for (const [id, r] of requests) {
|
|
772
|
+
if (!sameOrigin(r))
|
|
773
|
+
continue;
|
|
774
|
+
out.push({
|
|
775
|
+
method: r.method,
|
|
776
|
+
path: pathOf(r.url),
|
|
777
|
+
finalPath: pathOf(r.finalUrl),
|
|
778
|
+
status: r.status,
|
|
779
|
+
mimeType: r.mimeType,
|
|
780
|
+
failed: r.failed,
|
|
781
|
+
initiator: initiatorOf(r),
|
|
782
|
+
redirected: r.finalUrl !== r.url,
|
|
783
|
+
phase: id === authId ? 'auth'
|
|
784
|
+
: r.seq >= postSeq ? 'post'
|
|
785
|
+
: 'pre'
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
return out;
|
|
789
|
+
};
|
|
751
790
|
const facts = (log, over) => ({
|
|
752
791
|
sessionRequests: log,
|
|
753
792
|
...deriveLegacyFacts(log),
|
|
@@ -755,6 +794,7 @@ export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
|
|
|
755
794
|
credentialsFound: credentials !== null,
|
|
756
795
|
submitted: false,
|
|
757
796
|
foreignOriginFailures: foreignOriginFailures(),
|
|
797
|
+
signInLeftOrigin: null,
|
|
758
798
|
leftAuthWall: false,
|
|
759
799
|
urlBefore: before.url,
|
|
760
800
|
urlAfter: before.url,
|
|
@@ -780,29 +820,35 @@ export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
|
|
|
780
820
|
return judge(unsubmitted({ submitted: false }));
|
|
781
821
|
lastActivity = Date.now();
|
|
782
822
|
await settle(() => lastActivity, POST_SUBMIT_CAP_MS, quietMs);
|
|
823
|
+
const firstRequest = (pred) => {
|
|
824
|
+
for (const [id, r] of requests)
|
|
825
|
+
if (pred(r))
|
|
826
|
+
return id;
|
|
827
|
+
return null;
|
|
828
|
+
};
|
|
783
829
|
// The sign-in request: the first same-origin non-GET issued by the submit. Its
|
|
784
830
|
// own 2xx is the precondition for judging anything, and it is EXCLUDED from the
|
|
785
831
|
// data evidence — a broken build satisfies "at least one same-origin 2xx" with
|
|
786
832
|
// exactly this request and nothing else.
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
//
|
|
796
|
-
//
|
|
797
|
-
//
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
833
|
+
//
|
|
834
|
+
// The fallback covers a form that submits from the fill's own input events, so
|
|
835
|
+
// nothing arrives after the submit at all. Only a NAVIGATION qualifies there —
|
|
836
|
+
// the background traffic the fill races (beacons, telemetry) is XHR or fetch,
|
|
837
|
+
// never a document. The two windows overlap; the predicates carry the
|
|
838
|
+
// distinction.
|
|
839
|
+
const authId = firstRequest(r => r.seq >= submitSeq && sameOrigin(r) && r.method !== 'GET')
|
|
840
|
+
?? firstRequest(r => r.seq >= fillSeq && sameOrigin(r) && r.method !== 'GET' && r.type === 'Document');
|
|
841
|
+
// An SSO sign-in addresses this app and ends on the provider, so it is absent
|
|
842
|
+
// from the same-origin log above and "no request to our own origin" would
|
|
843
|
+
// misname it. Document-only for the same reason the authId fallback is: a
|
|
844
|
+
// telemetry beacon the fill races that happens to end off-origin would otherwise
|
|
845
|
+
// name a bogus identity provider.
|
|
846
|
+
const offsiteSignIn = authId !== null ? null : (firstRequest(r => r.seq >= fillSeq
|
|
847
|
+
&& r.method !== 'GET'
|
|
848
|
+
&& r.type === 'Document'
|
|
849
|
+
&& addressedOrigin(r)
|
|
850
|
+
&& !sameOrigin(r)
|
|
851
|
+
&& r.finalUrl.startsWith('http')));
|
|
806
852
|
const authReq = authId === null ? null : requests.get(authId);
|
|
807
853
|
const now = await evaluate(INSPECT_EXPR);
|
|
808
854
|
const domJudgment = judgeRenderedDom(now?.html ?? '');
|
|
@@ -826,7 +872,7 @@ export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
|
|
|
826
872
|
}
|
|
827
873
|
return judge(facts(sessionLog(authId, authReq?.seq ?? submitSeq), {
|
|
828
874
|
submitted: true,
|
|
829
|
-
|
|
875
|
+
signInLeftOrigin: offsiteSignIn && originOf(requests.get(offsiteSignIn).finalUrl),
|
|
830
876
|
leftAuthWall,
|
|
831
877
|
urlAfter: now?.url ?? before.url,
|
|
832
878
|
postAuthDomOk: domJudgment.ok,
|
|
@@ -841,3 +887,11 @@ function pathOf(url) {
|
|
|
841
887
|
return url;
|
|
842
888
|
}
|
|
843
889
|
}
|
|
890
|
+
function originOf(url) {
|
|
891
|
+
try {
|
|
892
|
+
return new URL(url).origin;
|
|
893
|
+
}
|
|
894
|
+
catch {
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
@@ -600,9 +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
|
-
* The trigger is the hole, with no threshold
|
|
604
|
-
*
|
|
605
|
-
*
|
|
603
|
+
* The trigger is the hole itself, with no fraction threshold: a threshold moves
|
|
604
|
+
* the count without changing which modules are actually missing signatures, so
|
|
605
|
+
* any value picked would only sound principled.
|
|
606
606
|
*/
|
|
607
607
|
const EXPORT_NAME_RE = /^[A-Za-z_][\w']*$/;
|
|
608
608
|
/** `module X` inside an export list is a re-export; `Prelude` is base, and base is not fetched. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.39",
|
|
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",
|