pi-fluency 0.1.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.
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/extensions/pi-fluency/analytics.ts +253 -0
- package/extensions/pi-fluency/analyzer.ts +171 -0
- package/extensions/pi-fluency/collector.ts +102 -0
- package/extensions/pi-fluency/context.ts +65 -0
- package/extensions/pi-fluency/diff.ts +147 -0
- package/extensions/pi-fluency/generation-marker.ts +51 -0
- package/extensions/pi-fluency/history-codec.ts +266 -0
- package/extensions/pi-fluency/index.ts +459 -0
- package/extensions/pi-fluency/overlay.ts +637 -0
- package/extensions/pi-fluency/retention.ts +48 -0
- package/extensions/pi-fluency/sanitize.ts +40 -0
- package/extensions/pi-fluency/setup.ts +29 -0
- package/extensions/pi-fluency/state-reducer.ts +192 -0
- package/extensions/pi-fluency/status.ts +39 -0
- package/extensions/pi-fluency/store.ts +589 -0
- package/extensions/pi-fluency/taxonomy.ts +73 -0
- package/extensions/pi-fluency/types.ts +138 -0
- package/extensions/pi-fluency/worker.ts +144 -0
- package/package.json +63 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { ErrantCategory, ErrantErrorType } from "./taxonomy.js";
|
|
2
|
+
|
|
3
|
+
export const SETTINGS_SCHEMA_VERSION = 3 as const;
|
|
4
|
+
export const HISTORY_SCHEMA_VERSION = 4 as const;
|
|
5
|
+
export const SCHEMA_VERSION = SETTINGS_SCHEMA_VERSION;
|
|
6
|
+
export const ANALYSIS_SCHEMA_VERSION = 3 as const;
|
|
7
|
+
|
|
8
|
+
export interface CollectedPrompt {
|
|
9
|
+
promptHash: string;
|
|
10
|
+
prose: string;
|
|
11
|
+
observedAt: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ContextScope = "sentence" | "previous-and-current" | "current-and-next";
|
|
15
|
+
|
|
16
|
+
export interface RawAnalyzerMistake {
|
|
17
|
+
original: string;
|
|
18
|
+
correction: string;
|
|
19
|
+
contextScope: ContextScope;
|
|
20
|
+
explanation: string;
|
|
21
|
+
errorType: ErrantErrorType;
|
|
22
|
+
patternKey: string;
|
|
23
|
+
confidence: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AnalyzerMistake extends RawAnalyzerMistake {
|
|
27
|
+
sourceExcerpt: string;
|
|
28
|
+
correctedExcerpt: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DemonstratedFix {
|
|
32
|
+
patternKey: string;
|
|
33
|
+
evidence: string;
|
|
34
|
+
confidence: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type AnalysisLanguage = "en" | "other";
|
|
38
|
+
|
|
39
|
+
export interface RawAnalysisResult {
|
|
40
|
+
schemaVersion: typeof ANALYSIS_SCHEMA_VERSION;
|
|
41
|
+
language: AnalysisLanguage;
|
|
42
|
+
mistakes: RawAnalyzerMistake[];
|
|
43
|
+
demonstratedFixes: DemonstratedFix[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AnalysisResult {
|
|
47
|
+
schemaVersion: typeof ANALYSIS_SCHEMA_VERSION;
|
|
48
|
+
language: AnalysisLanguage;
|
|
49
|
+
mistakes: AnalyzerMistake[];
|
|
50
|
+
demonstratedFixes: DemonstratedFix[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type OccurrenceDecision = "pending" | "accepted" | "dismissed";
|
|
54
|
+
|
|
55
|
+
export interface EnglishObservation {
|
|
56
|
+
promptHash: string;
|
|
57
|
+
observedAt: number;
|
|
58
|
+
localDate: string;
|
|
59
|
+
wordCount: number;
|
|
60
|
+
occurrenceIds: string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface MistakeOccurrence {
|
|
64
|
+
id: string;
|
|
65
|
+
promptHash: string;
|
|
66
|
+
patternId: string;
|
|
67
|
+
patternKey: string;
|
|
68
|
+
observedAt: number;
|
|
69
|
+
localDate: string;
|
|
70
|
+
decision: OccurrenceDecision;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface FluencyAnalyticsSnapshot {
|
|
74
|
+
observations: EnglishObservation[];
|
|
75
|
+
occurrences: MistakeOccurrence[];
|
|
76
|
+
patterns: MistakePattern[];
|
|
77
|
+
ignoredPatternKeys: string[];
|
|
78
|
+
ignoredCategories: ErrantCategory[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface MistakePattern {
|
|
82
|
+
id: string;
|
|
83
|
+
patternKey: string;
|
|
84
|
+
original: string;
|
|
85
|
+
correction: string;
|
|
86
|
+
sourceExcerpt: string;
|
|
87
|
+
correctedExcerpt: string;
|
|
88
|
+
explanation: string;
|
|
89
|
+
errorType: ErrantErrorType;
|
|
90
|
+
confidence: number;
|
|
91
|
+
firstSeenAt: number;
|
|
92
|
+
lastSeenAt: number;
|
|
93
|
+
occurrenceCount: number;
|
|
94
|
+
demonstratedFixCount: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type SnapshotPattern = MistakePattern;
|
|
98
|
+
|
|
99
|
+
export interface ReviewPattern extends MistakePattern {
|
|
100
|
+
pendingCount: number;
|
|
101
|
+
acceptedCount: number;
|
|
102
|
+
dismissedCount: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface FluencySettings {
|
|
106
|
+
schemaVersion: typeof SCHEMA_VERSION;
|
|
107
|
+
enabled: boolean;
|
|
108
|
+
consentedAt?: number;
|
|
109
|
+
provider?: string;
|
|
110
|
+
modelId?: string;
|
|
111
|
+
minimumConfidence: number;
|
|
112
|
+
retentionLimit: number;
|
|
113
|
+
ignoredPatternKeys: string[];
|
|
114
|
+
ignoredCategories: ErrantCategory[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const DEFAULT_SETTINGS: FluencySettings = {
|
|
118
|
+
schemaVersion: SCHEMA_VERSION,
|
|
119
|
+
enabled: false,
|
|
120
|
+
minimumConfidence: 0.8,
|
|
121
|
+
retentionLimit: 500,
|
|
122
|
+
ignoredPatternKeys: [],
|
|
123
|
+
ignoredCategories: [],
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
type HistoryEventBase = { schemaVersion: typeof HISTORY_SCHEMA_VERSION; at: number };
|
|
127
|
+
|
|
128
|
+
export type FluencyEvent =
|
|
129
|
+
| (HistoryEventBase & { type: "analysis"; prompt: CollectedPrompt; wordCount: number; result: AnalysisResult })
|
|
130
|
+
| (HistoryEventBase & { type: "review"; occurrenceIds: string[]; decision: Exclude<OccurrenceDecision, "pending"> })
|
|
131
|
+
| (HistoryEventBase & { type: "snapshot"; patterns: SnapshotPattern[]; observations: EnglishObservation[]; occurrences: MistakeOccurrence[]; processedPromptHashes: string[] });
|
|
132
|
+
|
|
133
|
+
export interface FluencyState {
|
|
134
|
+
patterns: Map<string, MistakePattern>;
|
|
135
|
+
observations: Map<string, EnglishObservation>;
|
|
136
|
+
occurrences: Map<string, MistakeOccurrence>;
|
|
137
|
+
processedPromptHashes: Set<string>;
|
|
138
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { AnalyzerConfigurationError, type Analyzer } from "./analyzer.js";
|
|
2
|
+
import type { AnalysisResult, CollectedPrompt, MistakePattern } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export interface WorkerSnapshot {
|
|
5
|
+
queued: number;
|
|
6
|
+
dropped: number;
|
|
7
|
+
running: boolean;
|
|
8
|
+
shuttingDown: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface WorkerOptions {
|
|
12
|
+
analyzer: Analyzer;
|
|
13
|
+
isIdle: () => boolean;
|
|
14
|
+
getPatterns: () => MistakePattern[];
|
|
15
|
+
onResult: (prompt: CollectedPrompt, result: AnalysisResult) => Promise<void>;
|
|
16
|
+
onError: (error: Error) => void;
|
|
17
|
+
onOverflow: (dropped: number) => void;
|
|
18
|
+
maxQueue?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DEFAULT_MAX_QUEUE = 10;
|
|
22
|
+
const ANALYSIS_TIMEOUT_MS = 30_000;
|
|
23
|
+
const RETRY_DELAY_MS = 500;
|
|
24
|
+
|
|
25
|
+
function normalizeError(error: unknown): Error {
|
|
26
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function abortableDelay(delayMs: number, signal: AbortSignal): Promise<void> {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
if (signal.aborted) {
|
|
32
|
+
reject(signal.reason);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const timer = setTimeout(resolve, delayMs);
|
|
36
|
+
signal.addEventListener("abort", () => {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
reject(signal.reason);
|
|
39
|
+
}, { once: true });
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class FluencyWorker {
|
|
44
|
+
private readonly queue: CollectedPrompt[] = [];
|
|
45
|
+
private readonly maxQueue: number;
|
|
46
|
+
private controller: AbortController | undefined;
|
|
47
|
+
private active: Promise<void> | undefined;
|
|
48
|
+
private dropped = 0;
|
|
49
|
+
private shuttingDown = false;
|
|
50
|
+
|
|
51
|
+
constructor(private readonly options: WorkerOptions) {
|
|
52
|
+
this.maxQueue = Number.isSafeInteger(options.maxQueue) && (options.maxQueue ?? -1) >= 0
|
|
53
|
+
? options.maxQueue as number
|
|
54
|
+
: DEFAULT_MAX_QUEUE;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
enqueue(prompt: CollectedPrompt): void {
|
|
58
|
+
if (this.shuttingDown) return;
|
|
59
|
+
this.queue.push(prompt);
|
|
60
|
+
while (this.queue.length > this.maxQueue) {
|
|
61
|
+
this.queue.shift();
|
|
62
|
+
this.dropped += 1;
|
|
63
|
+
this.options.onOverflow(this.dropped);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async drain(): Promise<void> {
|
|
68
|
+
if (this.active || this.shuttingDown || !this.options.isIdle()) return this.active;
|
|
69
|
+
this.active = this.run().finally(() => {
|
|
70
|
+
this.active = undefined;
|
|
71
|
+
});
|
|
72
|
+
return this.active;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async shutdown(): Promise<void> {
|
|
76
|
+
this.shuttingDown = true;
|
|
77
|
+
this.queue.length = 0;
|
|
78
|
+
this.controller?.abort();
|
|
79
|
+
await this.active?.catch(() => undefined);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getSnapshot(): WorkerSnapshot {
|
|
83
|
+
return {
|
|
84
|
+
queued: this.queue.length,
|
|
85
|
+
dropped: this.dropped,
|
|
86
|
+
running: this.active !== undefined,
|
|
87
|
+
shuttingDown: this.shuttingDown,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private async run(): Promise<void> {
|
|
92
|
+
while (!this.shuttingDown && this.options.isIdle()) {
|
|
93
|
+
const prompt = this.queue.shift();
|
|
94
|
+
if (!prompt) return;
|
|
95
|
+
try {
|
|
96
|
+
const result = await this.analyzeWithRetry(prompt);
|
|
97
|
+
await this.options.onResult(prompt, result);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (!this.shuttingDown) {
|
|
100
|
+
const normalized = normalizeError(error);
|
|
101
|
+
if (normalized instanceof AnalyzerConfigurationError) {
|
|
102
|
+
this.queue.unshift(prompt);
|
|
103
|
+
this.options.onError(normalized);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this.options.onError(normalized);
|
|
107
|
+
}
|
|
108
|
+
} finally {
|
|
109
|
+
this.controller = undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private async analyzeWithRetry(prompt: CollectedPrompt): Promise<AnalysisResult> {
|
|
115
|
+
let lastError: Error | undefined;
|
|
116
|
+
for (const delayMs of [0, RETRY_DELAY_MS]) {
|
|
117
|
+
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
|
+
}
|
|
123
|
+
if (this.shuttingDown) throw new DOMException("Aborted", "AbortError");
|
|
124
|
+
|
|
125
|
+
this.controller = new AbortController();
|
|
126
|
+
const signal = AbortSignal.any([
|
|
127
|
+
this.controller.signal,
|
|
128
|
+
AbortSignal.timeout(ANALYSIS_TIMEOUT_MS),
|
|
129
|
+
]);
|
|
130
|
+
try {
|
|
131
|
+
return await this.options.analyzer.analyze(prompt, this.options.getPatterns(), signal);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
const normalized = normalizeError(error);
|
|
134
|
+
if (
|
|
135
|
+
normalized.name === "AbortError"
|
|
136
|
+
|| this.shuttingDown
|
|
137
|
+
|| normalized instanceof AnalyzerConfigurationError
|
|
138
|
+
) throw normalized;
|
|
139
|
+
lastError = normalized;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
throw lastError ?? new Error("Analysis failed");
|
|
143
|
+
}
|
|
144
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-fluency",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Learn from language mistakes in your Pi prompts",
|
|
5
|
+
"author": "Ihar Trafimovich",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi-package",
|
|
9
|
+
"pi-extension",
|
|
10
|
+
"language-learning",
|
|
11
|
+
"grammar",
|
|
12
|
+
"writing"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/unutranyholas/pi-fluency.git"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/unutranyholas/pi-fluency#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/unutranyholas/pi-fluency/issues"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"extensions/pi-fluency/*.ts",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"test:watch": "vitest",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"check": "npm run typecheck && npm test",
|
|
36
|
+
"prepublishOnly": "npm run check"
|
|
37
|
+
},
|
|
38
|
+
"pi": {
|
|
39
|
+
"extensions": [
|
|
40
|
+
"./extensions/pi-fluency/index.ts"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@earendil-works/pi-ai": ">=0.80.10",
|
|
45
|
+
"@earendil-works/pi-coding-agent": ">=0.80.10",
|
|
46
|
+
"@earendil-works/pi-tui": ">=0.80.10"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"proper-lockfile": "^4.1.2"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@earendil-works/pi-ai": "^0.83.0",
|
|
53
|
+
"@earendil-works/pi-coding-agent": "^0.83.0",
|
|
54
|
+
"@earendil-works/pi-tui": "^0.83.0",
|
|
55
|
+
"@types/node": "^22.0.0",
|
|
56
|
+
"@types/proper-lockfile": "^4.1.4",
|
|
57
|
+
"typescript": "^5.8.0",
|
|
58
|
+
"vitest": "^3.2.0"
|
|
59
|
+
},
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": ">=22.19.0"
|
|
62
|
+
}
|
|
63
|
+
}
|