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.
Files changed (83) hide show
  1. package/LICENSE +561 -0
  2. package/README.md +464 -236
  3. package/dist/bench-cli.js +1 -47
  4. package/dist/bench.d.ts +137 -5
  5. package/dist/bench.js +1 -241
  6. package/dist/config.d.ts +47 -1
  7. package/dist/context-aggregator.d.ts +18 -6
  8. package/dist/cycle-manager.d.ts +83 -0
  9. package/dist/entropy-stop.d.ts +105 -0
  10. package/dist/eval-dataset.d.ts +55 -0
  11. package/dist/fuzzy-matcher.d.ts +12 -1
  12. package/dist/ghost-editor.d.ts +332 -6
  13. package/dist/index.d.ts +21 -1
  14. package/dist/index.js +1 -210
  15. package/dist/intent.d.ts +179 -0
  16. package/dist/metrics.d.ts +103 -3
  17. package/dist/path-index.d.ts +4 -1
  18. package/dist/path-matcher.d.ts +13 -1
  19. package/dist/profile-store.d.ts +85 -0
  20. package/dist/profile.d.ts +167 -0
  21. package/dist/providers/cloud-provider.d.ts +14 -2
  22. package/dist/providers/local-provider.d.ts +12 -2
  23. package/dist/providers/openai-compat-provider.d.ts +35 -2
  24. package/dist/providers/race.d.ts +62 -2
  25. package/dist/providers/types.d.ts +109 -2
  26. package/dist/recent-paths.d.ts +4 -1
  27. package/dist/replay-cli.js +2 -0
  28. package/dist/replay.d.ts +102 -0
  29. package/package.json +90 -79
  30. package/dist/bench-cli.d.ts +0 -3
  31. package/dist/bench-cli.d.ts.map +0 -1
  32. package/dist/bench-cli.js.map +0 -1
  33. package/dist/bench.d.ts.map +0 -1
  34. package/dist/bench.js.map +0 -1
  35. package/dist/config.d.ts.map +0 -1
  36. package/dist/config.js +0 -187
  37. package/dist/config.js.map +0 -1
  38. package/dist/context-aggregator.d.ts.map +0 -1
  39. package/dist/context-aggregator.js +0 -58
  40. package/dist/context-aggregator.js.map +0 -1
  41. package/dist/fuzzy-matcher.d.ts.map +0 -1
  42. package/dist/fuzzy-matcher.js +0 -160
  43. package/dist/fuzzy-matcher.js.map +0 -1
  44. package/dist/ghost-editor.d.ts.map +0 -1
  45. package/dist/ghost-editor.js +0 -445
  46. package/dist/ghost-editor.js.map +0 -1
  47. package/dist/index.d.ts.map +0 -1
  48. package/dist/index.js.map +0 -1
  49. package/dist/metrics.d.ts.map +0 -1
  50. package/dist/metrics.js +0 -98
  51. package/dist/metrics.js.map +0 -1
  52. package/dist/path-index.d.ts.map +0 -1
  53. package/dist/path-index.js +0 -148
  54. package/dist/path-index.js.map +0 -1
  55. package/dist/path-matcher.d.ts.map +0 -1
  56. package/dist/path-matcher.js +0 -85
  57. package/dist/path-matcher.js.map +0 -1
  58. package/dist/providers/cloud-provider.d.ts.map +0 -1
  59. package/dist/providers/cloud-provider.js +0 -52
  60. package/dist/providers/cloud-provider.js.map +0 -1
  61. package/dist/providers/local-provider.d.ts.map +0 -1
  62. package/dist/providers/local-provider.js +0 -36
  63. package/dist/providers/local-provider.js.map +0 -1
  64. package/dist/providers/openai-compat-provider.d.ts.map +0 -1
  65. package/dist/providers/openai-compat-provider.js +0 -128
  66. package/dist/providers/openai-compat-provider.js.map +0 -1
  67. package/dist/providers/openai-completions-runner.d.ts +0 -28
  68. package/dist/providers/openai-completions-runner.d.ts.map +0 -1
  69. package/dist/providers/openai-completions-runner.js +0 -46
  70. package/dist/providers/openai-completions-runner.js.map +0 -1
  71. package/dist/providers/race.d.ts.map +0 -1
  72. package/dist/providers/race.js +0 -128
  73. package/dist/providers/race.js.map +0 -1
  74. package/dist/providers/truncate.d.ts +0 -7
  75. package/dist/providers/truncate.d.ts.map +0 -1
  76. package/dist/providers/truncate.js +0 -39
  77. package/dist/providers/truncate.js.map +0 -1
  78. package/dist/providers/types.d.ts.map +0 -1
  79. package/dist/providers/types.js +0 -2
  80. package/dist/providers/types.js.map +0 -1
  81. package/dist/recent-paths.d.ts.map +0 -1
  82. package/dist/recent-paths.js +0 -114
  83. package/dist/recent-paths.js.map +0 -1
@@ -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
- export type CompletionMode = "deterministic" | "llm";
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
- //# sourceMappingURL=metrics.d.ts.map
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;
@@ -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
@@ -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