pi-fluency 0.1.2 → 0.2.0

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.
@@ -1,5 +1,6 @@
1
1
  import { AnalyzerConfigurationError, type Analyzer } from "./analyzer.js";
2
- import type { AnalysisResult, CollectedPrompt, MistakePattern } from "./types.js";
2
+ import type { AnalysisCommitFence } from "./store.js";
3
+ import type { AnalysisResult, CollectedPrompt, MistakePattern, PracticeTarget } from "./types.js";
3
4
 
4
5
  export interface WorkerSnapshot {
5
6
  queued: number;
@@ -8,24 +9,376 @@ export interface WorkerSnapshot {
8
9
  shuttingDown: boolean;
9
10
  }
10
11
 
12
+ export interface AnalyzerCoordinatorOwner {
13
+ readonly token: symbol;
14
+ }
15
+
16
+ interface OwnerState {
17
+ revoked: boolean;
18
+ revocationListeners: Set<() => void>;
19
+ }
20
+
21
+ interface OwnerRevocationSubscription {
22
+ promise: Promise<"revoked">;
23
+ unsubscribe: () => void;
24
+ }
25
+
26
+ interface SettledAnalysis<T> {
27
+ ok: boolean;
28
+ value?: T;
29
+ error?: Error;
30
+ }
31
+
32
+ interface ActiveAnalysis<T = unknown> {
33
+ requestToken: symbol;
34
+ ownerToken: symbol;
35
+ controller: AbortController;
36
+ settlement: Promise<SettledAnalysis<T>>;
37
+ settled: boolean;
38
+ invalidated: boolean;
39
+ }
40
+
41
+ export type ForegroundAnalysisOutcome =
42
+ | { kind: "success"; result: AnalysisResult }
43
+ | { kind: "busy" | "timeout" | "cancelled" | "quarantined" | "shutdown" }
44
+ | { kind: "error"; error: Error };
45
+
46
+ export interface ForegroundAnalysisOptions {
47
+ owner: AnalyzerCoordinatorOwner;
48
+ analyzer: Analyzer;
49
+ prompt: CollectedPrompt;
50
+ patterns: MistakePattern[];
51
+ selectedTargets?: readonly PracticeTarget[];
52
+ /** Absolute epoch deadline. */
53
+ deadline: number;
54
+ signal?: AbortSignal;
55
+ abortGraceMs?: number;
56
+ /** Runs after coordinator wait, immediately before provider call. */
57
+ authorize?: () => Promise<boolean>;
58
+ }
59
+
60
+ export class AnalyzerCoordinatorUnavailableError extends Error {
61
+ override readonly name = "AnalyzerCoordinatorUnavailableError";
62
+ }
63
+
64
+ function normalizeError(error: unknown): Error {
65
+ return error instanceof Error ? error : new Error(String(error));
66
+ }
67
+
68
+ /** Process-local serializer. State contains no extension callbacks, stores, or UI contexts. */
69
+ export class AnalyzerCoordinator {
70
+ private readonly owners = new Map<symbol, OwnerState>();
71
+ private active: ActiveAnalysis | undefined;
72
+ private foregroundPending = 0;
73
+ private quarantined = false;
74
+ private readonly waiters = new Set<() => void>();
75
+
76
+ attachOwner(): AnalyzerCoordinatorOwner {
77
+ const token = Symbol("pi-fluency-analyzer-owner");
78
+ this.owners.set(token, { revoked: false, revocationListeners: new Set() });
79
+ return { token };
80
+ }
81
+
82
+ isQuarantined(): boolean {
83
+ return this.quarantined;
84
+ }
85
+
86
+ canAcceptBackground(owner: AnalyzerCoordinatorOwner): boolean {
87
+ return this.isCurrentOwner(owner) && !this.quarantined;
88
+ }
89
+
90
+ private isCurrentOwner(owner: AnalyzerCoordinatorOwner): boolean {
91
+ return this.owners.get(owner.token)?.revoked === false;
92
+ }
93
+
94
+ private changed(): void {
95
+ for (const resolve of this.waiters) resolve();
96
+ this.waiters.clear();
97
+ }
98
+
99
+ private waitForChange(maximumMs?: number): Promise<void> {
100
+ return new Promise((resolve) => {
101
+ let timer: ReturnType<typeof setTimeout> | undefined;
102
+ const finish = () => {
103
+ if (timer !== undefined) clearTimeout(timer);
104
+ this.waiters.delete(finish);
105
+ resolve();
106
+ };
107
+ this.waiters.add(finish);
108
+ if (maximumMs !== undefined) timer = setTimeout(finish, Math.max(0, maximumMs));
109
+ });
110
+ }
111
+
112
+ private start<T>(owner: AnalyzerCoordinatorOwner, task: (signal: AbortSignal) => Promise<T>): ActiveAnalysis<T> {
113
+ if (this.active) throw new Error("Analyzer coordinator overlap");
114
+ const controller = new AbortController();
115
+ const requestToken = Symbol("pi-fluency-analyzer-request");
116
+ const active: ActiveAnalysis<T> = {
117
+ requestToken,
118
+ ownerToken: owner.token,
119
+ controller,
120
+ settled: false,
121
+ invalidated: false,
122
+ settlement: undefined as unknown as Promise<SettledAnalysis<T>>,
123
+ };
124
+ let taskPromise: Promise<T>;
125
+ try {
126
+ taskPromise = task(controller.signal);
127
+ } catch (error) {
128
+ taskPromise = Promise.reject(error);
129
+ }
130
+ active.settlement = taskPromise.then(
131
+ (value): SettledAnalysis<T> => ({ ok: true, value }),
132
+ (error): SettledAnalysis<T> => ({ ok: false, error: normalizeError(error) }),
133
+ )
134
+ .finally(() => {
135
+ active.settled = true;
136
+ if (this.active?.requestToken === requestToken) this.active = undefined;
137
+ this.quarantined = false;
138
+ this.changed();
139
+ });
140
+ this.active = active as ActiveAnalysis;
141
+ this.changed();
142
+ return active;
143
+ }
144
+
145
+ private subscribeOwnerRevocation(owner: AnalyzerCoordinatorOwner): OwnerRevocationSubscription {
146
+ const state = this.owners.get(owner.token);
147
+ if (!state || state.revoked) {
148
+ return { promise: Promise.resolve("revoked"), unsubscribe: () => undefined };
149
+ }
150
+ let listener!: () => void;
151
+ const promise = new Promise<"revoked">((resolve) => {
152
+ listener = () => {
153
+ state.revocationListeners.delete(listener);
154
+ resolve("revoked");
155
+ };
156
+ state.revocationListeners.add(listener);
157
+ });
158
+ return {
159
+ promise,
160
+ unsubscribe: () => state.revocationListeners.delete(listener),
161
+ };
162
+ }
163
+
164
+ private async raceOwnerRevocation<T>(owner: AnalyzerCoordinatorOwner, operation: Promise<T>): Promise<T | "revoked"> {
165
+ const subscription = this.subscribeOwnerRevocation(owner);
166
+ try {
167
+ return await Promise.race([operation, subscription.promise]);
168
+ } finally {
169
+ subscription.unsubscribe();
170
+ }
171
+ }
172
+
173
+ async runBackground<T>(
174
+ owner: AnalyzerCoordinatorOwner,
175
+ task: (signal: AbortSignal) => Promise<T>,
176
+ ): Promise<T> {
177
+ while (true) {
178
+ if (!this.isCurrentOwner(owner)) throw new DOMException("Aborted", "AbortError");
179
+ if (this.quarantined) throw new AnalyzerCoordinatorUnavailableError("Analyzer coordinator quarantined");
180
+ if (this.active || this.foregroundPending > 0) {
181
+ await this.raceOwnerRevocation(owner, this.waitForChange());
182
+ continue;
183
+ }
184
+ const active = this.start(owner, task);
185
+ const settled = await this.raceOwnerRevocation(owner, active.settlement);
186
+ if (settled === "revoked") throw new DOMException("Aborted", "AbortError");
187
+ if (!this.isCurrentOwner(owner) || active.invalidated) throw new DOMException("Aborted", "AbortError");
188
+ if (settled.ok) return settled.value as T;
189
+ throw settled.error ?? new Error("Analysis failed");
190
+ }
191
+ }
192
+
193
+ private async abortWithGrace(active: ActiveAnalysis, graceMs: number): Promise<boolean> {
194
+ active.invalidated = true;
195
+ active.controller.abort(new DOMException("Aborted", "AbortError"));
196
+ if (active.settled) return true;
197
+ let timer: ReturnType<typeof setTimeout> | undefined;
198
+ try {
199
+ await Promise.race([
200
+ active.settlement.then(() => undefined),
201
+ new Promise<void>((resolve) => { timer = setTimeout(resolve, Math.max(0, graceMs)); }),
202
+ ]);
203
+ } finally {
204
+ if (timer !== undefined) clearTimeout(timer);
205
+ }
206
+ if (!active.settled) {
207
+ this.quarantined = true;
208
+ this.changed();
209
+ return false;
210
+ }
211
+ return true;
212
+ }
213
+
214
+ async analyzeForeground(options: ForegroundAnalysisOptions): Promise<ForegroundAnalysisOutcome> {
215
+ const graceMs = Math.max(0, Math.min(100, options.abortGraceMs ?? 100));
216
+ if (!Number.isFinite(options.deadline)) return { kind: "error", error: new Error("Invalid analysis deadline") };
217
+ if (!this.isCurrentOwner(options.owner)) return { kind: "shutdown" };
218
+ if (this.quarantined) return { kind: "quarantined" };
219
+ this.foregroundPending += 1;
220
+ this.changed();
221
+ try {
222
+ while (this.active) {
223
+ if (options.signal?.aborted) {
224
+ const active = this.active;
225
+ if (active) await this.abortWithGrace(active, graceMs);
226
+ return { kind: "cancelled" };
227
+ }
228
+ const remaining = options.deadline - Date.now();
229
+ if (remaining <= 0) {
230
+ const active = this.active;
231
+ if (active) await this.abortWithGrace(active, graceMs);
232
+ return { kind: "busy" };
233
+ }
234
+ let onAbort: (() => void) | undefined;
235
+ try {
236
+ const cancellation = options.signal === undefined
237
+ ? new Promise<never>(() => undefined)
238
+ : new Promise<void>((resolve) => {
239
+ onAbort = resolve;
240
+ options.signal!.addEventListener("abort", onAbort, { once: true });
241
+ if (options.signal!.aborted) resolve();
242
+ });
243
+ await Promise.race([this.waitForChange(remaining), cancellation]);
244
+ } finally {
245
+ if (onAbort !== undefined) options.signal!.removeEventListener("abort", onAbort);
246
+ }
247
+ }
248
+ if (!this.isCurrentOwner(options.owner)) return { kind: "shutdown" };
249
+ if (this.quarantined) return { kind: "quarantined" };
250
+ if (options.signal?.aborted) return { kind: "cancelled" };
251
+ if (Date.now() >= options.deadline) return { kind: "timeout" };
252
+ if (options.authorize !== undefined) {
253
+ let timer: ReturnType<typeof setTimeout> | undefined;
254
+ let onAbort: (() => void) | undefined;
255
+ try {
256
+ const remaining = Math.max(0, options.deadline - Date.now());
257
+ const deadline = new Promise<"deadline">((resolve) => { timer = setTimeout(() => resolve("deadline"), remaining); });
258
+ const cancelled = options.signal === undefined
259
+ ? new Promise<never>(() => undefined)
260
+ : new Promise<"cancel">((resolve) => {
261
+ onAbort = () => resolve("cancel");
262
+ options.signal!.addEventListener("abort", onAbort, { once: true });
263
+ if (options.signal!.aborted) onAbort();
264
+ });
265
+ const authorized = await Promise.race([options.authorize(), deadline, cancelled]);
266
+ if (authorized === "deadline") return { kind: "timeout" };
267
+ if (authorized === "cancel") return { kind: "cancelled" };
268
+ if (!authorized) return { kind: "cancelled" };
269
+ } catch (error) {
270
+ return { kind: "error", error: normalizeError(error) };
271
+ } finally {
272
+ if (timer !== undefined) clearTimeout(timer);
273
+ if (onAbort !== undefined) options.signal!.removeEventListener("abort", onAbort);
274
+ }
275
+ }
276
+ if (!this.isCurrentOwner(options.owner)) return { kind: "shutdown" };
277
+ if (Date.now() >= options.deadline) return { kind: "timeout" };
278
+
279
+ const active = this.start(options.owner, (signal) => options.analyzer.analyze(
280
+ options.prompt,
281
+ options.patterns,
282
+ signal,
283
+ options.selectedTargets,
284
+ ));
285
+ const remaining = Math.max(0, options.deadline - Date.now());
286
+ let deadlineTimer: ReturnType<typeof setTimeout> | undefined;
287
+ let onAbort: (() => void) | undefined;
288
+ let outcome: SettledAnalysis<AnalysisResult> | "deadline" | "cancel" | "revoked";
289
+ try {
290
+ const deadlineRace = new Promise<"deadline">((resolve) => {
291
+ deadlineTimer = setTimeout(() => resolve("deadline"), remaining);
292
+ });
293
+ const cancelRace = options.signal === undefined
294
+ ? new Promise<never>(() => undefined)
295
+ : new Promise<"cancel">((resolve) => {
296
+ onAbort = () => resolve("cancel");
297
+ options.signal!.addEventListener("abort", onAbort, { once: true });
298
+ if (options.signal!.aborted) onAbort();
299
+ });
300
+ outcome = await this.raceOwnerRevocation(options.owner, Promise.race([
301
+ active.settlement,
302
+ deadlineRace,
303
+ cancelRace,
304
+ ]));
305
+ } finally {
306
+ if (deadlineTimer !== undefined) clearTimeout(deadlineTimer);
307
+ if (onAbort !== undefined) options.signal!.removeEventListener("abort", onAbort);
308
+ }
309
+ if (outcome === "deadline" || outcome === "cancel" || outcome === "revoked") {
310
+ await this.abortWithGrace(active, graceMs);
311
+ if (outcome === "cancel") return { kind: "cancelled" };
312
+ if (outcome === "revoked") return { kind: "shutdown" };
313
+ return { kind: "timeout" };
314
+ }
315
+ if (!this.isCurrentOwner(options.owner)) return { kind: "shutdown" };
316
+ if (active.invalidated) return { kind: "cancelled" };
317
+ if (outcome.ok) return { kind: "success", result: outcome.value as AnalysisResult };
318
+ return { kind: "error", error: outcome.error ?? new Error("Analysis failed") };
319
+ } finally {
320
+ this.foregroundPending -= 1;
321
+ this.changed();
322
+ }
323
+ }
324
+
325
+ async shutdownOwner(owner: AnalyzerCoordinatorOwner, abortGraceMs = 100): Promise<void> {
326
+ const state = this.owners.get(owner.token);
327
+ if (!state || state.revoked) return;
328
+ state.revoked = true;
329
+ for (const listener of state.revocationListeners) listener();
330
+ state.revocationListeners.clear();
331
+ this.owners.delete(owner.token);
332
+ this.changed();
333
+ const active = this.active;
334
+ if (active?.ownerToken === owner.token) {
335
+ await this.abortWithGrace(active, Math.max(0, Math.min(100, abortGraceMs)));
336
+ }
337
+ }
338
+ }
339
+
340
+ const COORDINATOR_SYMBOL = Symbol.for("pi-fluency.analyzer-coordinator.v1");
341
+ interface GlobalCoordinatorSlot { version: 1; coordinator: AnalyzerCoordinator }
342
+
343
+ export function getProcessAnalyzerCoordinator(): AnalyzerCoordinator {
344
+ const globals = globalThis as typeof globalThis & { [COORDINATOR_SYMBOL]?: GlobalCoordinatorSlot };
345
+ const current = globals[COORDINATOR_SYMBOL];
346
+ if (current?.version === 1
347
+ && typeof current.coordinator?.attachOwner === "function"
348
+ && typeof current.coordinator?.analyzeForeground === "function"
349
+ && typeof current.coordinator?.runBackground === "function") return current.coordinator;
350
+ const coordinator = new AnalyzerCoordinator();
351
+ globals[COORDINATOR_SYMBOL] = { version: 1, coordinator };
352
+ return coordinator;
353
+ }
354
+
355
+ export interface WorkerAnalyzerConfiguration {
356
+ fingerprint: string;
357
+ analyzer: Analyzer;
358
+ }
359
+
360
+ export interface BackgroundAnalysisJob {
361
+ prompt: CollectedPrompt;
362
+ fence?: AnalysisCommitFence;
363
+ }
364
+
11
365
  export interface WorkerOptions {
12
366
  analyzer: Analyzer;
367
+ /** Fresh request-scoped authorization/configuration resolver. Undefined discards stale job. */
368
+ getAnalyzerConfiguration?: (job: BackgroundAnalysisJob) => WorkerAnalyzerConfiguration | undefined | Promise<WorkerAnalyzerConfiguration | undefined>;
13
369
  isIdle: () => boolean;
14
370
  getPatterns: () => MistakePattern[];
15
- onResult: (prompt: CollectedPrompt, result: AnalysisResult) => Promise<void>;
371
+ onResult: (prompt: CollectedPrompt, result: AnalysisResult, fence?: AnalysisCommitFence) => Promise<void>;
16
372
  onError: (error: Error) => void;
17
373
  onOverflow: (dropped: number) => void;
18
374
  maxQueue?: number;
375
+ coordinator?: AnalyzerCoordinator;
19
376
  }
20
377
 
21
378
  const DEFAULT_MAX_QUEUE = 10;
22
379
  const ANALYSIS_TIMEOUT_MS = 30_000;
23
380
  const RETRY_DELAY_MS = 500;
24
381
 
25
- function normalizeError(error: unknown): Error {
26
- return error instanceof Error ? error : new Error(String(error));
27
- }
28
-
29
382
  function abortableDelay(delayMs: number, signal: AbortSignal): Promise<void> {
30
383
  return new Promise((resolve, reject) => {
31
384
  if (signal.aborted) {
@@ -41,22 +394,28 @@ function abortableDelay(delayMs: number, signal: AbortSignal): Promise<void> {
41
394
  }
42
395
 
43
396
  export class FluencyWorker {
44
- private readonly queue: CollectedPrompt[] = [];
397
+ private readonly queue: BackgroundAnalysisJob[] = [];
45
398
  private readonly maxQueue: number;
399
+ private readonly coordinator: AnalyzerCoordinator;
400
+ private readonly owner: AnalyzerCoordinatorOwner;
46
401
  private controller: AbortController | undefined;
47
402
  private active: Promise<void> | undefined;
48
403
  private dropped = 0;
49
404
  private shuttingDown = false;
405
+ private analyzerConfiguration: WorkerAnalyzerConfiguration;
50
406
 
51
407
  constructor(private readonly options: WorkerOptions) {
52
408
  this.maxQueue = Number.isSafeInteger(options.maxQueue) && (options.maxQueue ?? -1) >= 0
53
409
  ? options.maxQueue as number
54
410
  : DEFAULT_MAX_QUEUE;
411
+ this.coordinator = options.coordinator ?? getProcessAnalyzerCoordinator();
412
+ this.owner = this.coordinator.attachOwner();
413
+ this.analyzerConfiguration = { fingerprint: "legacy-static-analyzer", analyzer: options.analyzer };
55
414
  }
56
415
 
57
- enqueue(prompt: CollectedPrompt): void {
58
- if (this.shuttingDown) return;
59
- this.queue.push(prompt);
416
+ enqueue(prompt: CollectedPrompt, fence?: AnalysisCommitFence): void {
417
+ if (this.shuttingDown || !this.coordinator.canAcceptBackground(this.owner)) return;
418
+ this.queue.push({ prompt, ...(fence === undefined ? {} : { fence: { ...fence } }) });
60
419
  while (this.queue.length > this.maxQueue) {
61
420
  this.queue.shift();
62
421
  this.dropped += 1;
@@ -64,6 +423,10 @@ export class FluencyWorker {
64
423
  }
65
424
  }
66
425
 
426
+ analyzeForeground(options: Omit<ForegroundAnalysisOptions, "owner">): Promise<ForegroundAnalysisOutcome> {
427
+ return this.coordinator.analyzeForeground({ ...options, owner: this.owner });
428
+ }
429
+
67
430
  async drain(): Promise<void> {
68
431
  if (this.active || this.shuttingDown || !this.options.isIdle()) return this.active;
69
432
  this.active = this.run().finally(() => {
@@ -76,6 +439,7 @@ export class FluencyWorker {
76
439
  this.shuttingDown = true;
77
440
  this.queue.length = 0;
78
441
  this.controller?.abort();
442
+ await this.coordinator.shutdownOwner(this.owner);
79
443
  await this.active?.catch(() => undefined);
80
444
  }
81
445
 
@@ -90,16 +454,21 @@ export class FluencyWorker {
90
454
 
91
455
  private async run(): Promise<void> {
92
456
  while (!this.shuttingDown && this.options.isIdle()) {
93
- const prompt = this.queue.shift();
94
- if (!prompt) return;
457
+ const job = this.queue.shift();
458
+ if (!job) return;
95
459
  try {
96
- const result = await this.analyzeWithRetry(prompt);
97
- await this.options.onResult(prompt, result);
460
+ const result = await this.analyzeWithRetry(job);
461
+ if (result !== undefined && !this.shuttingDown) await this.options.onResult(job.prompt, result, job.fence);
98
462
  } catch (error) {
99
463
  if (!this.shuttingDown) {
100
464
  const normalized = normalizeError(error);
465
+ if (normalized instanceof AnalyzerCoordinatorUnavailableError) {
466
+ this.queue.length = 0;
467
+ this.options.onError(normalized);
468
+ return;
469
+ }
101
470
  if (normalized instanceof AnalyzerConfigurationError) {
102
- this.queue.unshift(prompt);
471
+ this.queue.unshift(job);
103
472
  this.options.onError(normalized);
104
473
  return;
105
474
  }
@@ -108,33 +477,45 @@ export class FluencyWorker {
108
477
  } finally {
109
478
  this.controller = undefined;
110
479
  }
480
+ await Promise.resolve();
111
481
  }
112
482
  }
113
483
 
114
- private async analyzeWithRetry(prompt: CollectedPrompt): Promise<AnalysisResult> {
484
+ private async analyzeWithRetry(job: BackgroundAnalysisJob): Promise<AnalysisResult | undefined> {
115
485
  let lastError: Error | undefined;
116
486
  for (const delayMs of [0, RETRY_DELAY_MS]) {
117
487
  if (this.shuttingDown) throw new DOMException("Aborted", "AbortError");
118
- if (delayMs > 0) {
119
- const signal = this.controller?.signal;
120
- if (!signal) throw new Error("Analysis retry lost abort controller");
121
- await abortableDelay(delayMs, signal);
122
- }
488
+ this.controller = new AbortController();
489
+ if (delayMs > 0) await abortableDelay(delayMs, this.controller.signal);
123
490
  if (this.shuttingDown) throw new DOMException("Aborted", "AbortError");
124
491
 
125
- this.controller = new AbortController();
126
- const signal = AbortSignal.any([
127
- this.controller.signal,
128
- AbortSignal.timeout(ANALYSIS_TIMEOUT_MS),
129
- ]);
130
492
  try {
131
- return await this.options.analyzer.analyze(prompt, this.options.getPatterns(), signal);
493
+ return await this.coordinator.runBackground(this.owner, async (coordinatorSignal) => {
494
+ // Resolve durable authorization/configuration only after coordinator waits. Keep it
495
+ // adjacent to each provider attempt so queued jobs cannot use pre-wait consent.
496
+ const configured = this.options.getAnalyzerConfiguration?.(job);
497
+ let fresh: WorkerAnalyzerConfiguration | undefined;
498
+ if (configured !== undefined && typeof (configured as Promise<WorkerAnalyzerConfiguration | undefined>).then === "function") {
499
+ fresh = await (configured as Promise<WorkerAnalyzerConfiguration | undefined>);
500
+ } else {
501
+ fresh = configured as WorkerAnalyzerConfiguration | undefined;
502
+ }
503
+ if (this.options.getAnalyzerConfiguration !== undefined && fresh === undefined) return undefined;
504
+ if (fresh !== undefined) this.analyzerConfiguration = fresh;
505
+ const timeoutSignal = AbortSignal.timeout(ANALYSIS_TIMEOUT_MS);
506
+ return this.analyzerConfiguration.analyzer.analyze(
507
+ job.prompt,
508
+ this.options.getPatterns(),
509
+ AbortSignal.any([this.controller!.signal, timeoutSignal, coordinatorSignal]),
510
+ );
511
+ });
132
512
  } catch (error) {
133
513
  const normalized = normalizeError(error);
134
514
  if (
135
515
  normalized.name === "AbortError"
136
516
  || this.shuttingDown
137
517
  || normalized instanceof AnalyzerConfigurationError
518
+ || normalized instanceof AnalyzerCoordinatorUnavailableError
138
519
  ) throw normalized;
139
520
  lastError = normalized;
140
521
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-fluency",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "English writing analytics for human-authored Pi prompts",
5
5
  "author": "Ihar Trafimovich",
6
6
  "type": "module",