pi-ghost-autocomplete 0.3.1 → 0.4.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 +561 -0
- package/README.md +464 -236
- package/dist/bench-cli.js +1 -47
- package/dist/bench.d.ts +137 -5
- package/dist/bench.js +1 -241
- package/dist/config.d.ts +47 -1
- package/dist/context-aggregator.d.ts +18 -6
- package/dist/cycle-manager.d.ts +83 -0
- package/dist/entropy-stop.d.ts +105 -0
- package/dist/eval-dataset.d.ts +55 -0
- package/dist/fuzzy-matcher.d.ts +12 -1
- package/dist/ghost-editor.d.ts +332 -6
- package/dist/index.d.ts +21 -1
- package/dist/index.js +1 -210
- package/dist/intent.d.ts +179 -0
- package/dist/metrics.d.ts +103 -3
- package/dist/path-index.d.ts +4 -1
- package/dist/path-matcher.d.ts +13 -1
- package/dist/profile-store.d.ts +85 -0
- package/dist/profile.d.ts +167 -0
- package/dist/providers/cloud-provider.d.ts +14 -2
- package/dist/providers/local-provider.d.ts +12 -2
- package/dist/providers/openai-compat-provider.d.ts +35 -2
- package/dist/providers/race.d.ts +62 -2
- package/dist/providers/types.d.ts +109 -2
- package/dist/recent-paths.d.ts +4 -1
- package/dist/replay-cli.js +2 -0
- package/dist/replay.d.ts +102 -0
- package/package.json +90 -79
- package/dist/bench-cli.d.ts +0 -3
- package/dist/bench-cli.d.ts.map +0 -1
- package/dist/bench-cli.js.map +0 -1
- package/dist/bench.d.ts.map +0 -1
- package/dist/bench.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -187
- package/dist/config.js.map +0 -1
- package/dist/context-aggregator.d.ts.map +0 -1
- package/dist/context-aggregator.js +0 -58
- package/dist/context-aggregator.js.map +0 -1
- package/dist/fuzzy-matcher.d.ts.map +0 -1
- package/dist/fuzzy-matcher.js +0 -160
- package/dist/fuzzy-matcher.js.map +0 -1
- package/dist/ghost-editor.d.ts.map +0 -1
- package/dist/ghost-editor.js +0 -445
- package/dist/ghost-editor.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/metrics.d.ts.map +0 -1
- package/dist/metrics.js +0 -98
- package/dist/metrics.js.map +0 -1
- package/dist/path-index.d.ts.map +0 -1
- package/dist/path-index.js +0 -148
- package/dist/path-index.js.map +0 -1
- package/dist/path-matcher.d.ts.map +0 -1
- package/dist/path-matcher.js +0 -85
- package/dist/path-matcher.js.map +0 -1
- package/dist/providers/cloud-provider.d.ts.map +0 -1
- package/dist/providers/cloud-provider.js +0 -52
- package/dist/providers/cloud-provider.js.map +0 -1
- package/dist/providers/local-provider.d.ts.map +0 -1
- package/dist/providers/local-provider.js +0 -36
- package/dist/providers/local-provider.js.map +0 -1
- package/dist/providers/openai-compat-provider.d.ts.map +0 -1
- package/dist/providers/openai-compat-provider.js +0 -128
- package/dist/providers/openai-compat-provider.js.map +0 -1
- package/dist/providers/openai-completions-runner.d.ts +0 -28
- package/dist/providers/openai-completions-runner.d.ts.map +0 -1
- package/dist/providers/openai-completions-runner.js +0 -46
- package/dist/providers/openai-completions-runner.js.map +0 -1
- package/dist/providers/race.d.ts.map +0 -1
- package/dist/providers/race.js +0 -128
- package/dist/providers/race.js.map +0 -1
- package/dist/providers/truncate.d.ts +0 -7
- package/dist/providers/truncate.d.ts.map +0 -1
- package/dist/providers/truncate.js +0 -39
- package/dist/providers/truncate.js.map +0 -1
- package/dist/providers/types.d.ts.map +0 -1
- package/dist/providers/types.js +0 -2
- package/dist/providers/types.js.map +0 -1
- package/dist/recent-paths.d.ts.map +0 -1
- package/dist/recent-paths.js +0 -114
- package/dist/recent-paths.js.map +0 -1
package/dist/intent.d.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-License-Identifier: SSPL-1.0
|
|
3
|
+
* SPDX-FileCopyrightText: 2025 Alessandro Pungitore <alessandro@pungitore.ch>
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Intent detection for ghost autocompletion.
|
|
7
|
+
*
|
|
8
|
+
* Centralises all prose / path / filename heuristics so they can be shared
|
|
9
|
+
* between the deterministic fast-path engine and the LLM provider layer
|
|
10
|
+
* without circular dependencies.
|
|
11
|
+
*
|
|
12
|
+
* This module is the single source of truth for:
|
|
13
|
+
* - `PromptIntent` — discriminated union for intent classification
|
|
14
|
+
* - `classifyPromptIntent()` — regex-based intent classifier
|
|
15
|
+
* - `ACTION_VERBS` — canonical action-verb list (used by isPathContext,
|
|
16
|
+
* draftLooksLikePathContext, and fuzzy-matcher)
|
|
17
|
+
* - `looksLikeProse()` — single-line prose detection
|
|
18
|
+
* - `looksLikeProseContext()` — multi-line prose detection
|
|
19
|
+
* - `isPathContext()` — path-prefix context validation
|
|
20
|
+
* - `looksLikeFilename()` — completion output shape detection
|
|
21
|
+
* - `draftLooksLikePathContext()` — draft context shape detection
|
|
22
|
+
*
|
|
23
|
+
* Consumers import from here; legacy re-exports live in `fast-path-engine.ts`
|
|
24
|
+
* and `providers/truncate.ts` for backward compatibility with existing tests.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Coarse-grained intent of the current prompt line. Used to route the
|
|
28
|
+
* autocompletion pipeline:
|
|
29
|
+
*
|
|
30
|
+
* - `command` — shell-like command (starts with `/`, `$`, `!`, `@`)
|
|
31
|
+
* → deterministic + LLM
|
|
32
|
+
* - `question` — user asks a question (starts with how/what/why/...)
|
|
33
|
+
* → LLM only (skip deterministic)
|
|
34
|
+
* - `instruction` — imperative sentence (starts with a verb)
|
|
35
|
+
* → deterministic + LLM
|
|
36
|
+
* - `path-ref` — user is typing a file path or referencing files
|
|
37
|
+
* → deterministic only
|
|
38
|
+
* - `prose` — natural-language sentence (none of the above)
|
|
39
|
+
* → LLM only (skip deterministic)
|
|
40
|
+
*/
|
|
41
|
+
export type PromptIntent = "command" | "question" | "instruction" | "path-ref" | "prose";
|
|
42
|
+
/**
|
|
43
|
+
* Action verbs that strongly signal the user is about to reference a file
|
|
44
|
+
* path. Used by `isPathContext`, `draftLooksLikePathContext`, and the
|
|
45
|
+
* fuzzy matcher's `hasActionTrigger` gate.
|
|
46
|
+
*
|
|
47
|
+
* Single canonical source — do NOT duplicate elsewhere.
|
|
48
|
+
* Covers: file manipulation (open/edit/read/write/save), navigation (cd/ls),
|
|
49
|
+
* search (find/check/look), shell commands (cat/less/head/tail/touch/mkdir/
|
|
50
|
+
* rm/cp/mv), loading (load/import/include), and general (show/view/see/run/add).
|
|
51
|
+
*/
|
|
52
|
+
export declare const ACTION_VERBS: Set<string>;
|
|
53
|
+
/** Alternation group for use in regex character classes. Kept in sync
|
|
54
|
+
* with `ACTION_VERBS` — rebuild with `[...ACTION_VERBS].join("|")`.
|
|
55
|
+
* Exported so consumers (fast-path-engine, etc.) can build verb-aware
|
|
56
|
+
* regexes without duplicating the list. */
|
|
57
|
+
export declare const ACTION_VERBS_ALT: string;
|
|
58
|
+
/**
|
|
59
|
+
* Heuristic: does the text before the cursor look like natural-language prose
|
|
60
|
+
* rather than a command / path context? Returns true when there are 4+ word
|
|
61
|
+
* tokens (\w{2,}) — a strong signal the user is writing a sentence, not a
|
|
62
|
+
* filename.
|
|
63
|
+
*
|
|
64
|
+
* This prevents the fuzzy matcher from proposing `config.ts` when the user
|
|
65
|
+
* types "please add a con|" ("con" fuzzy-matches config.ts at score ~75).
|
|
66
|
+
*/
|
|
67
|
+
export declare function looksLikeProse(lineBeforeCursor: string): boolean;
|
|
68
|
+
/** Reason a multi-line prose context fired. Used for observability so
|
|
69
|
+
* callers can log *why* the deterministic path was suppressed. */
|
|
70
|
+
export type ProseContextReason = "density" | "cohesion" | "punctuation";
|
|
71
|
+
/** Detailed result of multi-line prose detection. `reason` is set iff
|
|
72
|
+
* `isProse` is true, naming the rule that triggered. */
|
|
73
|
+
export interface ProseContextResult {
|
|
74
|
+
isProse: boolean;
|
|
75
|
+
reason?: ProseContextReason;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Heuristic: do the surrounding lines indicate a prose context (the user
|
|
79
|
+
* is writing natural-language text across multiple lines)? Returns the
|
|
80
|
+
* verdict together with the matching reason for observability.
|
|
81
|
+
*
|
|
82
|
+
* Rules (first match wins):
|
|
83
|
+
*
|
|
84
|
+
* 1. **High word density**: the previous lines contain 8+ word tokens total
|
|
85
|
+
* (\w{2,}) — strong signal of paragraph-level prose.
|
|
86
|
+
* 2. **Lexical cohesion**: the previous lines share ≥2 word tokens with the
|
|
87
|
+
* current line — inspired by Hearst's TextTiling algorithm for
|
|
88
|
+
* detecting topic continuity.
|
|
89
|
+
* 3. **Sentence punctuation**: the previous non-empty line ends with a
|
|
90
|
+
* period or comma AND carries at least PUNCTUATION_MIN_PREV_TOKENS
|
|
91
|
+
* alpha word tokens. The token-count guard kills shell one-liners
|
|
92
|
+
* like `echo hello,` that would otherwise be misrouted.
|
|
93
|
+
* Semicolons are excluded (common in code).
|
|
94
|
+
*/
|
|
95
|
+
export declare function classifyProseContext(previousLines: string[], currentLineBeforeCursor: string): ProseContextResult;
|
|
96
|
+
/**
|
|
97
|
+
* Boolean wrapper kept for back-compat with existing tests / call sites
|
|
98
|
+
* that don't need the reason. Prefer `classifyProseContext` when the
|
|
99
|
+
* caller wants to log why prose mode triggered.
|
|
100
|
+
*
|
|
101
|
+
* @param previousLines — lines before the current line
|
|
102
|
+
* @param currentLineBeforeCursor — text on the current line before the cursor
|
|
103
|
+
*/
|
|
104
|
+
export declare function looksLikeProseContext(previousLines: string[], currentLineBeforeCursor: string): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Heuristic: is the path-prefix token in a context that suggests the user
|
|
107
|
+
* intends to type a filename? Returns true when:
|
|
108
|
+
* - The token is inside quotes
|
|
109
|
+
* - The token is preceded by an action verb + whitespace ("read src/|")
|
|
110
|
+
* - The token starts at column 0 or after only whitespace
|
|
111
|
+
* - The preceding character is a non-letter boundary (`=`, `:`, `,`, etc.)
|
|
112
|
+
*
|
|
113
|
+
* Returns false when the token is embedded in prose (e.g. "and then src/|").
|
|
114
|
+
*/
|
|
115
|
+
export declare function isPathContext(lineBeforeCursor: string, _token: {
|
|
116
|
+
token: string;
|
|
117
|
+
insideQuote: boolean;
|
|
118
|
+
}): boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Short human-readable description for each intent, suitable for injection
|
|
121
|
+
* into LLM system prompts or FIM comment headers. Single canonical source —
|
|
122
|
+
* both chat-style (`truncate.ts`) and FIM (`openai-compat-provider.ts`)
|
|
123
|
+
* providers use these so the wording stays consistent.
|
|
124
|
+
*/
|
|
125
|
+
export declare const INTENT_DESCRIPTION: Record<PromptIntent, string>;
|
|
126
|
+
/**
|
|
127
|
+
* Classify the user's current prompt intent. Uses simple regex / keyword
|
|
128
|
+
* heuristics to decide which completion pipeline to activate.
|
|
129
|
+
*
|
|
130
|
+
* The classifier examines only the current line before the cursor (fast)
|
|
131
|
+
* plus optional previous lines for multi-line prose detection.
|
|
132
|
+
*
|
|
133
|
+
* Returns one of: `command`, `question`, `instruction`, `path-ref`, `prose`.
|
|
134
|
+
*
|
|
135
|
+
* Classification order (first match wins):
|
|
136
|
+
* 1. `command` — line starts with `/`, `!`, `$`, `@`
|
|
137
|
+
* 2. `path-ref` — line ends with a path-like prefix (contains `/`) or is
|
|
138
|
+
* inside quotes after an action verb
|
|
139
|
+
* 3. `question` — line starts with a question word (how/what/why/...)
|
|
140
|
+
* 4. `instruction` — line starts with an action verb
|
|
141
|
+
* 5. `prose` — multi-line prose context detected, or single-line prose
|
|
142
|
+
* (4+ word tokens)
|
|
143
|
+
*/
|
|
144
|
+
export declare function classifyPromptIntent(lineBeforeCursor: string, previousLines?: string[]): PromptIntent;
|
|
145
|
+
/** Reason a prompt was classified as prose. `single-line` corresponds to
|
|
146
|
+
* `looksLikeProse` (4+ word tokens on the current line); the others
|
|
147
|
+
* mirror `ProseContextReason`. */
|
|
148
|
+
export type ProseReason = ProseContextReason | "single-line";
|
|
149
|
+
/** Detailed intent classification including the prose-rule reason when
|
|
150
|
+
* applicable. Lets the editor log *why* the deterministic path was
|
|
151
|
+
* suppressed without re-running the heuristic. */
|
|
152
|
+
export interface IntentClassification {
|
|
153
|
+
intent: PromptIntent;
|
|
154
|
+
proseReason?: ProseReason;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Detailed variant of `classifyPromptIntent`. Same classification order,
|
|
158
|
+
* but returns the prose-rule reason when intent === "prose" so callers
|
|
159
|
+
* can emit observability records.
|
|
160
|
+
*/
|
|
161
|
+
export declare function classifyPromptIntentDetailed(lineBeforeCursor: string, previousLines?: string[]): IntentClassification;
|
|
162
|
+
/**
|
|
163
|
+
* Heuristic: does the completion text look like a raw filename or path?
|
|
164
|
+
* Matches patterns like "src/foo/bar.ts", "config.json", "./path/to/file",
|
|
165
|
+
* and extensions like .ts, .js, .py, .json, .md, etc.
|
|
166
|
+
*
|
|
167
|
+
* Intentionally conservative: requires at least a 2-char extension with
|
|
168
|
+
* no trailing dot, and rejects common false positives like "hello.there"
|
|
169
|
+
* by requiring the extension part to look like a real file extension
|
|
170
|
+
* (starts with a letter, contains only letters/digits).
|
|
171
|
+
*/
|
|
172
|
+
export declare function looksLikeFilename(text: string): boolean;
|
|
173
|
+
/**
|
|
174
|
+
* Heuristic: does the user's draft text look like they're in a path-typing
|
|
175
|
+
* context? Returns true when the draft ends with a path-like prefix
|
|
176
|
+
* (contains `/`, optionally preceded by `./` or `~/`) or when the last
|
|
177
|
+
* few words include an action verb ("read", "open", "edit", etc.).
|
|
178
|
+
*/
|
|
179
|
+
export declare function draftLooksLikePathContext(draft: string): boolean;
|
package/dist/metrics.d.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-License-Identifier: SSPL-1.0
|
|
3
|
+
* SPDX-FileCopyrightText: 2025 Alessandro Pungitore <alessandro@pungitore.ch>
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Source of truth for all completion modes. Both the type union (compile-time)
|
|
7
|
+
* and the parser whitelist (runtime, in `bench.ts`) derive from this array so
|
|
8
|
+
* adding a mode means updating one place.
|
|
9
|
+
*/
|
|
10
|
+
export declare const COMPLETION_MODES: readonly ["deterministic", "llm", "cache", "trie", "speculative-warm", "provisional", "profile-slash"];
|
|
11
|
+
export type CompletionMode = (typeof COMPLETION_MODES)[number];
|
|
2
12
|
export type RequestResult = "produced" | "empty" | "rejected" | "aborted" | "error";
|
|
3
|
-
export type AcceptanceOutcome = "accepted" | "dismissed" | "stale" | "expired";
|
|
13
|
+
export type AcceptanceOutcome = "accepted" | "accepted-partial" | "dismissed" | "stale" | "expired";
|
|
14
|
+
export type QualityAttribution = "accepted" | "dismissed-attributed" | "unknown";
|
|
4
15
|
export type RejectionReason = "stale-text" | "newline" | "too-long" | "assistant-phrasing" | "duplicate-suffix" | "markdown" | "empty" | "echo-token";
|
|
5
16
|
export interface ProviderLatency {
|
|
6
17
|
provider: string;
|
|
@@ -25,7 +36,91 @@ export interface MetricsRecord {
|
|
|
25
36
|
* Never set for LLM mode (would risk leaking output text).
|
|
26
37
|
*/
|
|
27
38
|
matchedPath?: string;
|
|
39
|
+
/** Characters of the ghost the user accepted (full or partial). */
|
|
40
|
+
acceptedChars?: number;
|
|
41
|
+
/** Chars from suggestion that matched the user's eventual committed text. */
|
|
42
|
+
prefixMatchLen?: number;
|
|
43
|
+
/** Levenshtein distance between top-1 candidate and committed line. */
|
|
44
|
+
editDistance?: number;
|
|
45
|
+
/** Count of Ctrl+Right partial accepts on this candidate. */
|
|
46
|
+
partialAccepts?: number;
|
|
47
|
+
/** 1-based rank accepted at time of accept (M3 candidate sets). */
|
|
48
|
+
candidateRank?: number;
|
|
49
|
+
/** Number of candidates considered. */
|
|
50
|
+
candidateCount?: number;
|
|
51
|
+
/** Number of Alt+]/Alt+[ cycles before acceptance. */
|
|
52
|
+
cycledCount?: number;
|
|
53
|
+
/** Prefix-match length per rank — numeric only. */
|
|
54
|
+
prefixMatchLenByRank?: number[];
|
|
55
|
+
/** saved@k by k. */
|
|
56
|
+
savedAt?: {
|
|
57
|
+
"1"?: number;
|
|
58
|
+
"3"?: number;
|
|
59
|
+
"5"?: number;
|
|
60
|
+
};
|
|
61
|
+
/** Time the ghost was visible to the user, in ms. */
|
|
62
|
+
displayDurationMs?: number;
|
|
63
|
+
/** Whether quality fields above are reliable for this record. */
|
|
64
|
+
qualityAttribution?: QualityAttribution;
|
|
65
|
+
/** Provider time-to-first-token in ms (streaming providers only). */
|
|
66
|
+
ttftMs?: number;
|
|
67
|
+
/** Time from provisional ghost render to LLM swap, in ms. */
|
|
68
|
+
provisionalSwapMs?: number;
|
|
69
|
+
/** True iff provisional matched LLM within prefix-match >= 0.8. */
|
|
70
|
+
provisionalWasCorrect?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* True iff a provisional→LLM swap was rendered and the user dismissed or
|
|
73
|
+
* aborted within `SWAP_FLICKER_WINDOW_MS`. Tracks the perceived flicker
|
|
74
|
+
* cost of swaps — the `PI_GHOST_PROVISIONAL_PREFER=1` knob trades raw
|
|
75
|
+
* recall for a lower value of this rate.
|
|
76
|
+
*/
|
|
77
|
+
provisionalSwapFlicker?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* True iff the ghost was visible for less than `SHORT_HOVER_DISMISS_MS`
|
|
80
|
+
* before the user dismissed it. Mild negative signal — emitted on the
|
|
81
|
+
* non-accept record so M8 / future weighting can read it without joining
|
|
82
|
+
* separate streams. The actual hover duration is in `displayDurationMs`.
|
|
83
|
+
*/
|
|
84
|
+
shortHoverDismiss?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* True iff the user typed at least `TYPED_PAST_DIVERGENT_THRESHOLD`
|
|
87
|
+
* consecutive characters that diverged from the served ghost without
|
|
88
|
+
* accepting it (#5). The detection runs across consecutive stale
|
|
89
|
+
* finalizations on the same line, so a single keystroke doesn't trip
|
|
90
|
+
* it — the run resets on any accept. When set, the editor also applies
|
|
91
|
+
* a session-trie penalty against the (prefix, ghost) pair so future
|
|
92
|
+
* suggestions on the same prefix are de-prioritised.
|
|
93
|
+
*/
|
|
94
|
+
typedPastDivergent?: boolean;
|
|
95
|
+
/** Heuristic: acceptedChars * userAvgMsPerChar - perceivedLatencyMs. */
|
|
96
|
+
estimatedTimeSavedMs?: number;
|
|
97
|
+
/**
|
|
98
|
+
* True when the user deleted >50% of inserted text within the regret
|
|
99
|
+
* window after acceptance. See PRD §6 M5a.
|
|
100
|
+
*/
|
|
101
|
+
acceptRegret?: boolean;
|
|
102
|
+
/** Inserted characters subsequently deleted within the regret window. */
|
|
103
|
+
regretDeletedChars?: number;
|
|
104
|
+
/** Combined behavior fingerprint at the time this record was logged. */
|
|
105
|
+
versionFingerprint?: string;
|
|
106
|
+
/**
|
|
107
|
+
* Top candidate's raw `editProximityScore` (un-normalized output of the
|
|
108
|
+
* scorer the reranker received). Pure observability — bench/replay
|
|
109
|
+
* correlate it with `acceptance` to validate the feature weight. Set
|
|
110
|
+
* only when reranker actually ran and edit-history was enabled.
|
|
111
|
+
*/
|
|
112
|
+
editProximityScore?: number;
|
|
113
|
+
/**
|
|
114
|
+
* Size of the `EditHistoryRing` snapshot at predict time. Useful for
|
|
115
|
+
* filtering out records where the ring was still warming up.
|
|
116
|
+
*/
|
|
117
|
+
editHistorySize?: number;
|
|
28
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Debug records have a loose payload by necessity (different kinds carry
|
|
121
|
+
* different shapes). The redactor below scrubs any field that looks like
|
|
122
|
+
* raw prompt/completion content before serialization — see `redactDebug`.
|
|
123
|
+
*/
|
|
29
124
|
export interface DebugRecord {
|
|
30
125
|
ts: number;
|
|
31
126
|
requestId: string;
|
|
@@ -53,4 +148,9 @@ export declare class MetricsLogger {
|
|
|
53
148
|
flush(): Promise<void>;
|
|
54
149
|
private enqueue;
|
|
55
150
|
}
|
|
56
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Strip any debug-record field not on the pass-through whitelist. Replaces
|
|
153
|
+
* the value with a `[redacted: type=T len=N]` placeholder so debugging still
|
|
154
|
+
* sees that *something* was there, without leaking content. SEC-27.
|
|
155
|
+
*/
|
|
156
|
+
export declare function redactDebug(rec: DebugRecord): DebugRecord;
|
package/dist/path-index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-License-Identifier: SSPL-1.0
|
|
3
|
+
* SPDX-FileCopyrightText: 2025 Alessandro Pungitore <alessandro@pungitore.ch>
|
|
4
|
+
*/
|
|
1
5
|
export interface PathIndexOptions {
|
|
2
6
|
cap?: number;
|
|
3
7
|
signal?: AbortSignal;
|
|
@@ -17,4 +21,3 @@ export interface PathIndex {
|
|
|
17
21
|
* path completion this session".
|
|
18
22
|
*/
|
|
19
23
|
export declare function buildPathIndex(cwd: string, opts?: PathIndexOptions): Promise<PathIndex | null>;
|
|
20
|
-
//# sourceMappingURL=path-index.d.ts.map
|
package/dist/path-matcher.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-License-Identifier: SSPL-1.0
|
|
3
|
+
* SPDX-FileCopyrightText: 2025 Alessandro Pungitore <alessandro@pungitore.ch>
|
|
4
|
+
*/
|
|
1
5
|
import type { PathIndex } from "./path-index.js";
|
|
2
6
|
export interface PathToken {
|
|
3
7
|
token: string;
|
|
@@ -11,6 +15,15 @@ export interface PathPrefixMatch {
|
|
|
11
15
|
export interface FindPathPrefixOptions {
|
|
12
16
|
limit?: number;
|
|
13
17
|
recentPaths?: Set<string> | null;
|
|
18
|
+
/**
|
|
19
|
+
* PRD §M4 — bounded boost from persistent path frequency. Caller passes
|
|
20
|
+
* `(path) => profileStore.profilePathBoost(path)` which returns a value
|
|
21
|
+
* in `[0, MAX_PATH_BOOST]` (0.2). Multiplied by 1e3 in the score formula
|
|
22
|
+
* so it ranks below recent-mention (1e6) but above path length, breaking
|
|
23
|
+
* ties between equally-fresh matches in favor of historically-frequent
|
|
24
|
+
* paths. Cannot beat subtree (1e9) — exact session matches still win.
|
|
25
|
+
*/
|
|
26
|
+
profileBoost?: ((path: string) => number) | null;
|
|
14
27
|
}
|
|
15
28
|
/**
|
|
16
29
|
* Extract a path-like token from the end of `before` (text before the cursor).
|
|
@@ -29,4 +42,3 @@ export declare function extractPathPrefixToken(before: string): PathToken | null
|
|
|
29
42
|
* (nothing to insert).
|
|
30
43
|
*/
|
|
31
44
|
export declare function findPathPrefix(index: PathIndex, token: string, opts?: FindPathPrefixOptions | number): PathPrefixMatch | null;
|
|
32
|
-
//# sourceMappingURL=path-matcher.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-License-Identifier: SSPL-1.0
|
|
3
|
+
* SPDX-FileCopyrightText: 2025 Alessandro Pungitore <alessandro@pungitore.ch>
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Stateful wrapper around `profile.ts` for the editor. Holds the in-memory
|
|
7
|
+
* `ProfileIndex`, serializes writes, triggers periodic compaction, and
|
|
8
|
+
* exposes a synchronous lookup surface for the editor's hot path.
|
|
9
|
+
*
|
|
10
|
+
* Lifecycle:
|
|
11
|
+
* 1. `create(cwd)` — loads + indexes the JSONL once. Returns a disabled
|
|
12
|
+
* store if `PI_GHOST_PROFILE=off`. Errors during load yield an empty
|
|
13
|
+
* index (best-effort).
|
|
14
|
+
* 2. `captureCommit(text)` / `captureAccept(prefix, completion, weight)`
|
|
15
|
+
* — append to disk (async, fire-and-forget) and merge into the
|
|
16
|
+
* in-memory index so the next lookup sees them immediately.
|
|
17
|
+
* 3. Every `COMPACT_CHECK_EVERY` writes the store runs `compactIfNeeded`
|
|
18
|
+
* in the background. Compaction reloads the index after rewrite.
|
|
19
|
+
*
|
|
20
|
+
* `disabled` is a public boolean so callers can short-circuit lookups
|
|
21
|
+
* without an extra function call on the keystroke path.
|
|
22
|
+
*/
|
|
23
|
+
import { DEFAULT_DECAY_DAYS, type ProfileIndex } from "./profile.js";
|
|
24
|
+
export declare class ProfileStore {
|
|
25
|
+
private readonly cwd;
|
|
26
|
+
readonly disabled: boolean;
|
|
27
|
+
private index;
|
|
28
|
+
private writeQueue;
|
|
29
|
+
private writesSinceCompact;
|
|
30
|
+
private decayDays;
|
|
31
|
+
private constructor();
|
|
32
|
+
/**
|
|
33
|
+
* Build a store and load the JSONL into memory. Caller awaits before
|
|
34
|
+
* exposing the store to the editor so the index is hot on first
|
|
35
|
+
* keystroke. Disabled stores short-circuit load entirely.
|
|
36
|
+
*/
|
|
37
|
+
static create(cwd: string, env?: NodeJS.ProcessEnv): Promise<ProfileStore>;
|
|
38
|
+
getIndex(): ProfileIndex;
|
|
39
|
+
get decayDaysSetting(): number;
|
|
40
|
+
/**
|
|
41
|
+
* Capture user-committed text. Extracts paths + slash + trigrams, drops
|
|
42
|
+
* sensitive messages entirely. Returns the count of records emitted —
|
|
43
|
+
* useful for tests/diagnostics.
|
|
44
|
+
*/
|
|
45
|
+
captureCommit(text: string, ts?: number): number;
|
|
46
|
+
/**
|
|
47
|
+
* Capture an accept (full or partial). `weight` defaults to 3 to match
|
|
48
|
+
* PRD §M2 trie weights. Returns true when a record was emitted.
|
|
49
|
+
*/
|
|
50
|
+
captureAccept(prefix: string, completion: string, weight?: number, ts?: number): boolean;
|
|
51
|
+
/** Public lookup surfaces — thin wrappers around `profile.ts`. */
|
|
52
|
+
bestSlashMatch(prefix: string): {
|
|
53
|
+
cmd: string;
|
|
54
|
+
weight: number;
|
|
55
|
+
} | null;
|
|
56
|
+
profileBias(text: string): number;
|
|
57
|
+
profilePathBoost(path: string): number;
|
|
58
|
+
/**
|
|
59
|
+
* Provisional fallback (PRD §M3.5b) — look up an exact-prefix accept
|
|
60
|
+
* record. Returns the completion tokens + summed weight, or null when
|
|
61
|
+
* the prefix has never been the exact key of a captured accept.
|
|
62
|
+
* `prefix` is hashed in-process; the raw string never leaves memory.
|
|
63
|
+
*/
|
|
64
|
+
lookupAcceptByPrefix(prefix: string): {
|
|
65
|
+
completionTokens: string[];
|
|
66
|
+
weight: number;
|
|
67
|
+
} | null;
|
|
68
|
+
/** Flush any pending writes — call before process exit. */
|
|
69
|
+
flush(): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Append records both in-memory and to disk. In-memory merge happens
|
|
72
|
+
* synchronously so the very next lookup sees the new weight; the disk
|
|
73
|
+
* write is queued through `writeQueue` to serialize concurrent commits.
|
|
74
|
+
*/
|
|
75
|
+
private mergeAndPersist;
|
|
76
|
+
/**
|
|
77
|
+
* Inline equivalent of `buildIndex` for a small record batch. The
|
|
78
|
+
* fresh-record path skips decay since we just wrote `ts`. Keeping this
|
|
79
|
+
* separate from `profile.buildIndex` avoids reindexing the whole store
|
|
80
|
+
* on every keystroke.
|
|
81
|
+
*/
|
|
82
|
+
private applyToIndex;
|
|
83
|
+
}
|
|
84
|
+
/** Exported for tests — surface compactor knob without re-exporting `profile.ts`. */
|
|
85
|
+
export { DEFAULT_DECAY_DAYS };
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-License-Identifier: SSPL-1.0
|
|
3
|
+
* SPDX-FileCopyrightText: 2025 Alessandro Pungitore <alessandro@pungitore.ch>
|
|
4
|
+
*/
|
|
5
|
+
export declare const PROFILE_REL = ".pi/ghost-autocomplete/profile.jsonl";
|
|
6
|
+
export declare const DEFAULT_MAX_BYTES: number;
|
|
7
|
+
export declare const DEFAULT_DECAY_DAYS = 90;
|
|
8
|
+
export declare const DROP_BELOW_WEIGHT = 0.1;
|
|
9
|
+
export declare const MAX_NGRAM_N = 4;
|
|
10
|
+
export declare const TRIGRAM_N = 3;
|
|
11
|
+
export type ProfileKind = "ngram" | "path" | "slash" | "accept";
|
|
12
|
+
/**
|
|
13
|
+
* One on-disk record. `weight` accumulates across captures and decays over
|
|
14
|
+
* time. `accept` records carry both `prefixHash` (for exact-prefix learning)
|
|
15
|
+
* and `prefixTailTokens` + `completionTokens` (already tokenized).
|
|
16
|
+
*/
|
|
17
|
+
export interface ProfileRecord {
|
|
18
|
+
ts: number;
|
|
19
|
+
kind: ProfileKind;
|
|
20
|
+
weight: number;
|
|
21
|
+
/** `kind=ngram`: ordered token tuple. */
|
|
22
|
+
tokens?: string[];
|
|
23
|
+
/** Trigram length when `kind=ngram`. */
|
|
24
|
+
n?: number;
|
|
25
|
+
/** `kind=path` / `kind=slash`: literal value. */
|
|
26
|
+
value?: string;
|
|
27
|
+
/** `kind=accept`: SHA-256 hex digest of the prefix at accept time. */
|
|
28
|
+
prefixHash?: string;
|
|
29
|
+
/** `kind=accept`: trailing tokens of the prefix (cap 4). */
|
|
30
|
+
prefixTailTokens?: string[];
|
|
31
|
+
/** `kind=accept`: tokens of the completion (cap 8). */
|
|
32
|
+
completionTokens?: string[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* In-memory aggregated view of the profile. The maps are weight-sums per
|
|
36
|
+
* key. `acceptByPrefix` is keyed by `prefixHash` so exact-prefix history is
|
|
37
|
+
* available without storing the raw prefix.
|
|
38
|
+
*/
|
|
39
|
+
export interface ProfileIndex {
|
|
40
|
+
ngram: Map<string, number>;
|
|
41
|
+
path: Map<string, number>;
|
|
42
|
+
slash: Map<string, number>;
|
|
43
|
+
acceptByPrefix: Map<string, {
|
|
44
|
+
completionTokens: string[];
|
|
45
|
+
weight: number;
|
|
46
|
+
}>;
|
|
47
|
+
totalAcceptRecords: number;
|
|
48
|
+
}
|
|
49
|
+
export declare function emptyIndex(): ProfileIndex;
|
|
50
|
+
/**
|
|
51
|
+
* Tokenize a string into the same word-token form the profile stores. Drops
|
|
52
|
+
* the empty string, deduplicates whitespace, and lowercases nothing — code
|
|
53
|
+
* identifiers are case-sensitive.
|
|
54
|
+
*/
|
|
55
|
+
export declare function tokenize(text: string): string[];
|
|
56
|
+
/**
|
|
57
|
+
* Generate n-grams of size `n` from `tokens`. Returns empty when there are
|
|
58
|
+
* fewer than `n` tokens.
|
|
59
|
+
*/
|
|
60
|
+
export declare function ngrams(tokens: string[], n: number): string[][];
|
|
61
|
+
/**
|
|
62
|
+
* SHA-256 hex of a prefix string. The hash is the disk-storable surrogate
|
|
63
|
+
* for the prefix — same prefix always maps to the same key, but the raw
|
|
64
|
+
* prefix never leaves the process.
|
|
65
|
+
*/
|
|
66
|
+
export declare function prefixHash(prefix: string): string;
|
|
67
|
+
/**
|
|
68
|
+
* Extract candidate paths from free text. Same shape used by `recent-paths.ts`
|
|
69
|
+
* but kept independent so changes to recent-paths semantics don't drift
|
|
70
|
+
* the profile schema. Matches `foo/bar.ts`, `src/a/b.tsx`, etc.
|
|
71
|
+
*/
|
|
72
|
+
export declare function extractPaths(text: string): string[];
|
|
73
|
+
/**
|
|
74
|
+
* Extract `/slash-command` tokens from free text. Limited to a-z prefix
|
|
75
|
+
* plus alphanumerics/hyphen/underscore to avoid false matches on URLs
|
|
76
|
+
* (`https://…`) or expressions (`x/y`).
|
|
77
|
+
*/
|
|
78
|
+
export declare function extractSlash(text: string): string[];
|
|
79
|
+
/**
|
|
80
|
+
* Build profile records from a committed user message. Capture path +
|
|
81
|
+
* slash + trigram ngrams. Sanitize check runs FIRST: messages with
|
|
82
|
+
* sensitive content are dropped entirely (consistent with session trie).
|
|
83
|
+
*/
|
|
84
|
+
export declare function recordsFromCommit(text: string, ts?: number): ProfileRecord[];
|
|
85
|
+
/**
|
|
86
|
+
* Build an `accept` record for a full or partial acceptance. `prefix` is
|
|
87
|
+
* hashed; `completion` is tokenized. The raw prefix never appears in the
|
|
88
|
+
* returned record. Drops the whole record if either side fails sanitize.
|
|
89
|
+
*/
|
|
90
|
+
export declare function recordFromAccept(prefix: string, completion: string, weight?: number, ts?: number): ProfileRecord | null;
|
|
91
|
+
/**
|
|
92
|
+
* Append one or more records to the JSONL file. Returns silently on write
|
|
93
|
+
* failure — profile logging must never break ghost completion.
|
|
94
|
+
*/
|
|
95
|
+
export declare function appendRecords(cwd: string, recs: ProfileRecord[]): Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Parse the JSONL file into an array of records. Silently skips malformed
|
|
98
|
+
* lines (partial trailing write) and records missing required fields.
|
|
99
|
+
* Returns an empty array when the file does not exist.
|
|
100
|
+
*/
|
|
101
|
+
export declare function loadRecords(cwd: string): Promise<ProfileRecord[]>;
|
|
102
|
+
export declare function parseRecords(body: string): ProfileRecord[];
|
|
103
|
+
/**
|
|
104
|
+
* Build the in-memory aggregated index from a record list. Decays records
|
|
105
|
+
* older than `decayDays` by halving their weight; drops weights below
|
|
106
|
+
* `DROP_BELOW_WEIGHT` (0.1). `now` defaults to `Date.now()`; expose for
|
|
107
|
+
* deterministic tests.
|
|
108
|
+
*/
|
|
109
|
+
export declare function buildIndex(records: ProfileRecord[], opts?: {
|
|
110
|
+
decayDays?: number;
|
|
111
|
+
now?: number;
|
|
112
|
+
}): ProfileIndex;
|
|
113
|
+
/**
|
|
114
|
+
* Look up the weight of a trigram (already tokenized + joined). Returns 0
|
|
115
|
+
* when missing. Callers should also tokenize the candidate text the same
|
|
116
|
+
* way the writer did.
|
|
117
|
+
*/
|
|
118
|
+
export declare function ngramWeight(idx: ProfileIndex, tokens: string[]): number;
|
|
119
|
+
/**
|
|
120
|
+
* Profile-bias score for a candidate completion. PRD §M4
|
|
121
|
+
* "Ranking-from-prior": `α·log(1 + ngramWeight) + β·pathWeight + γ·slashWeight`.
|
|
122
|
+
* Returns 0 when the index is empty or the candidate has no signal —
|
|
123
|
+
* neutral input to the reranker.
|
|
124
|
+
*/
|
|
125
|
+
export declare function profileBias(idx: ProfileIndex, text: string, weights?: {
|
|
126
|
+
alpha?: number;
|
|
127
|
+
beta?: number;
|
|
128
|
+
gamma?: number;
|
|
129
|
+
}): number;
|
|
130
|
+
/**
|
|
131
|
+
* Bounded boost for deterministic path matches. Capped so persistent
|
|
132
|
+
* frequency cannot beat a fresh exact-prefix path-prefix match (PRD §M4
|
|
133
|
+
* "Deterministic path/fuzzy ranking"). The cap is a constant 0.2 — tune
|
|
134
|
+
* once we have replay data.
|
|
135
|
+
*/
|
|
136
|
+
export declare const MAX_PATH_BOOST = 0.2;
|
|
137
|
+
export declare function profilePathBoost(idx: ProfileIndex, path: string): number;
|
|
138
|
+
/**
|
|
139
|
+
* Slash-command frequency score. Used by the editor's slash fast-path:
|
|
140
|
+
* given a `/cmd-prefix`, return the best-matching full command by weight.
|
|
141
|
+
* Returns null when nothing in the index extends `prefix`.
|
|
142
|
+
*/
|
|
143
|
+
export declare function bestSlashMatch(idx: ProfileIndex, prefix: string): {
|
|
144
|
+
cmd: string;
|
|
145
|
+
weight: number;
|
|
146
|
+
} | null;
|
|
147
|
+
/**
|
|
148
|
+
* Rotate + compact when the file exceeds `maxBytes`. Strategy: load all
|
|
149
|
+
* records, decay them, sum weights per (kind, key), drop sub-threshold
|
|
150
|
+
* entries, write the compacted file back. Returns true when a rotation
|
|
151
|
+
* happened. Best-effort — swallows errors.
|
|
152
|
+
*/
|
|
153
|
+
export declare function compactIfNeeded(cwd: string, opts?: {
|
|
154
|
+
maxBytes?: number;
|
|
155
|
+
decayDays?: number;
|
|
156
|
+
now?: number;
|
|
157
|
+
}): Promise<boolean>;
|
|
158
|
+
/**
|
|
159
|
+
* Compact a record list in memory: decay aged weights, sum per (kind,key),
|
|
160
|
+
* drop sub-threshold. Pure function — exposed for unit tests.
|
|
161
|
+
*/
|
|
162
|
+
export declare function compactRecords(records: ProfileRecord[], opts?: {
|
|
163
|
+
decayDays?: number;
|
|
164
|
+
now?: number;
|
|
165
|
+
}): ProfileRecord[];
|
|
166
|
+
/** Parse `PI_GHOST_PROFILE_DECAY_DAYS`. Falls back to default when invalid. */
|
|
167
|
+
export declare function parseDecayDaysEnv(raw: string | undefined): number;
|
|
@@ -1,10 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-License-Identifier: SSPL-1.0
|
|
3
|
+
* SPDX-FileCopyrightText: 2025 Alessandro Pungitore <alessandro@pungitore.ch>
|
|
4
|
+
*/
|
|
1
5
|
import type { CloudProviderConfig } from "../config.js";
|
|
2
|
-
import type { GhostProvider, GhostRequest } from "./types.js";
|
|
6
|
+
import type { GhostProvider, GhostRequest, StreamDelta } from "./types.js";
|
|
3
7
|
export declare class CloudProvider implements GhostProvider {
|
|
4
8
|
private readonly config;
|
|
5
9
|
readonly name = "ghost-cloud";
|
|
6
10
|
constructor(config: CloudProviderConfig);
|
|
7
11
|
get debounceMs(): number;
|
|
8
12
|
predict(req: GhostRequest): Promise<string | null>;
|
|
13
|
+
/**
|
|
14
|
+
* PRD §M3.5a — stream tokens from the upstream provider via pi-ai's
|
|
15
|
+
* `stream()`. Yields `text_delta` events as `StreamDelta` chunks
|
|
16
|
+
* including a running `cumulative` to spare the editor from tracking
|
|
17
|
+
* concatenation state. `sanitizeCompletion` runs once on the final
|
|
18
|
+
* assembled text after the stream ends — running it per-chunk would
|
|
19
|
+
* truncate mid-token and break ghost rendering.
|
|
20
|
+
*/
|
|
21
|
+
predictStream(req: GhostRequest): AsyncIterable<StreamDelta>;
|
|
9
22
|
}
|
|
10
|
-
//# sourceMappingURL=cloud-provider.d.ts.map
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-License-Identifier: SSPL-1.0
|
|
3
|
+
* SPDX-FileCopyrightText: 2025 Alessandro Pungitore <alessandro@pungitore.ch>
|
|
4
|
+
*/
|
|
1
5
|
import type { LocalProviderConfig } from "../config.js";
|
|
2
|
-
import type { GhostProvider, GhostRequest } from "./types.js";
|
|
6
|
+
import type { GhostProvider, GhostRequest, StreamDelta } from "./types.js";
|
|
3
7
|
export declare class LocalProvider implements GhostProvider {
|
|
4
8
|
private readonly config;
|
|
5
9
|
readonly name = "ghost-local";
|
|
@@ -7,5 +11,11 @@ export declare class LocalProvider implements GhostProvider {
|
|
|
7
11
|
get debounceMs(): number;
|
|
8
12
|
preflight(): Promise<boolean>;
|
|
9
13
|
predict(req: GhostRequest): Promise<string | null>;
|
|
14
|
+
/**
|
|
15
|
+
* PRD §M3.5a — Ollama/vLLM streaming via the OpenAI-completions stream
|
|
16
|
+
* endpoint. Same opts as `predict`, just driven by the runner's
|
|
17
|
+
* streaming variant.
|
|
18
|
+
*/
|
|
19
|
+
predictStream(req: GhostRequest): AsyncIterable<StreamDelta>;
|
|
20
|
+
private callOpts;
|
|
10
21
|
}
|
|
11
|
-
//# sourceMappingURL=local-provider.d.ts.map
|