@phren/cli 0.0.1
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 +590 -0
- package/mcp/dist/capabilities/cli.js +61 -0
- package/mcp/dist/capabilities/index.js +15 -0
- package/mcp/dist/capabilities/mcp.js +61 -0
- package/mcp/dist/capabilities/types.js +57 -0
- package/mcp/dist/capabilities/vscode.js +61 -0
- package/mcp/dist/capabilities/web-ui.js +61 -0
- package/mcp/dist/cli-actions.js +302 -0
- package/mcp/dist/cli-config.js +580 -0
- package/mcp/dist/cli-extract.js +305 -0
- package/mcp/dist/cli-govern.js +371 -0
- package/mcp/dist/cli-graph.js +169 -0
- package/mcp/dist/cli-hooks-citations.js +44 -0
- package/mcp/dist/cli-hooks-context.js +56 -0
- package/mcp/dist/cli-hooks-globs.js +83 -0
- package/mcp/dist/cli-hooks-output.js +130 -0
- package/mcp/dist/cli-hooks-retrieval.js +2 -0
- package/mcp/dist/cli-hooks-session.js +1402 -0
- package/mcp/dist/cli-hooks.js +350 -0
- package/mcp/dist/cli-namespaces.js +989 -0
- package/mcp/dist/cli-ops.js +253 -0
- package/mcp/dist/cli-search.js +407 -0
- package/mcp/dist/cli.js +108 -0
- package/mcp/dist/content-archive.js +278 -0
- package/mcp/dist/content-citation.js +391 -0
- package/mcp/dist/content-dedup.js +622 -0
- package/mcp/dist/content-learning.js +472 -0
- package/mcp/dist/content-metadata.js +186 -0
- package/mcp/dist/content-validate.js +462 -0
- package/mcp/dist/core-finding.js +54 -0
- package/mcp/dist/core-project.js +36 -0
- package/mcp/dist/core-search.js +50 -0
- package/mcp/dist/data-access.js +400 -0
- package/mcp/dist/data-tasks.js +821 -0
- package/mcp/dist/embedding.js +344 -0
- package/mcp/dist/entrypoint.js +387 -0
- package/mcp/dist/finding-context.js +172 -0
- package/mcp/dist/finding-impact.js +181 -0
- package/mcp/dist/finding-journal.js +122 -0
- package/mcp/dist/finding-lifecycle.js +259 -0
- package/mcp/dist/governance-audit.js +22 -0
- package/mcp/dist/governance-locks.js +96 -0
- package/mcp/dist/governance-policy.js +648 -0
- package/mcp/dist/governance-scores.js +355 -0
- package/mcp/dist/hooks.js +449 -0
- package/mcp/dist/impact-scoring.js +22 -0
- package/mcp/dist/index-query.js +168 -0
- package/mcp/dist/index.js +205 -0
- package/mcp/dist/init-config.js +336 -0
- package/mcp/dist/init-preferences.js +62 -0
- package/mcp/dist/init-setup.js +1305 -0
- package/mcp/dist/init-shared.js +29 -0
- package/mcp/dist/init.js +1730 -0
- package/mcp/dist/link-checksums.js +62 -0
- package/mcp/dist/link-context.js +257 -0
- package/mcp/dist/link-doctor.js +591 -0
- package/mcp/dist/link-skills.js +212 -0
- package/mcp/dist/link.js +596 -0
- package/mcp/dist/logger.js +15 -0
- package/mcp/dist/machine-identity.js +38 -0
- package/mcp/dist/mcp-config.js +254 -0
- package/mcp/dist/mcp-data.js +315 -0
- package/mcp/dist/mcp-extract-facts.js +78 -0
- package/mcp/dist/mcp-extract.js +133 -0
- package/mcp/dist/mcp-finding.js +557 -0
- package/mcp/dist/mcp-graph.js +339 -0
- package/mcp/dist/mcp-hooks.js +256 -0
- package/mcp/dist/mcp-memory.js +58 -0
- package/mcp/dist/mcp-ops.js +328 -0
- package/mcp/dist/mcp-search.js +628 -0
- package/mcp/dist/mcp-session.js +651 -0
- package/mcp/dist/mcp-skills.js +189 -0
- package/mcp/dist/mcp-tasks.js +551 -0
- package/mcp/dist/mcp-types.js +7 -0
- package/mcp/dist/memory-ui-assets.js +6 -0
- package/mcp/dist/memory-ui-data.js +513 -0
- package/mcp/dist/memory-ui-graph.js +1910 -0
- package/mcp/dist/memory-ui-page.js +353 -0
- package/mcp/dist/memory-ui-scripts.js +1387 -0
- package/mcp/dist/memory-ui-server.js +1218 -0
- package/mcp/dist/memory-ui-styles.js +555 -0
- package/mcp/dist/memory-ui.js +9 -0
- package/mcp/dist/package-metadata.js +13 -0
- package/mcp/dist/phren-art.js +52 -0
- package/mcp/dist/phren-core.js +108 -0
- package/mcp/dist/phren-dotenv.js +67 -0
- package/mcp/dist/phren-paths.js +476 -0
- package/mcp/dist/proactivity.js +172 -0
- package/mcp/dist/profile-store.js +228 -0
- package/mcp/dist/project-config.js +85 -0
- package/mcp/dist/project-locator.js +25 -0
- package/mcp/dist/project-topics.js +1134 -0
- package/mcp/dist/provider-adapters.js +176 -0
- package/mcp/dist/runtime-profile.js +18 -0
- package/mcp/dist/session-checkpoints.js +131 -0
- package/mcp/dist/session-utils.js +68 -0
- package/mcp/dist/shared-content.js +8 -0
- package/mcp/dist/shared-embedding-cache.js +143 -0
- package/mcp/dist/shared-fragment-graph.js +456 -0
- package/mcp/dist/shared-governance.js +4 -0
- package/mcp/dist/shared-index.js +1334 -0
- package/mcp/dist/shared-ollama.js +192 -0
- package/mcp/dist/shared-paths.js +1 -0
- package/mcp/dist/shared-retrieval.js +796 -0
- package/mcp/dist/shared-search-fallback.js +375 -0
- package/mcp/dist/shared-sqljs.js +42 -0
- package/mcp/dist/shared-stemmer.js +171 -0
- package/mcp/dist/shared-vector-index.js +199 -0
- package/mcp/dist/shared.js +114 -0
- package/mcp/dist/shell-entry.js +209 -0
- package/mcp/dist/shell-input.js +943 -0
- package/mcp/dist/shell-palette.js +119 -0
- package/mcp/dist/shell-render.js +252 -0
- package/mcp/dist/shell-state-store.js +81 -0
- package/mcp/dist/shell-types.js +13 -0
- package/mcp/dist/shell-view-list.js +14 -0
- package/mcp/dist/shell-view.js +707 -0
- package/mcp/dist/shell.js +352 -0
- package/mcp/dist/skill-files.js +117 -0
- package/mcp/dist/skill-registry.js +279 -0
- package/mcp/dist/skill-state.js +28 -0
- package/mcp/dist/startup-embedding.js +57 -0
- package/mcp/dist/status.js +323 -0
- package/mcp/dist/synonyms.json +670 -0
- package/mcp/dist/task-hygiene.js +251 -0
- package/mcp/dist/task-lifecycle.js +347 -0
- package/mcp/dist/tasks-github.js +76 -0
- package/mcp/dist/telemetry.js +165 -0
- package/mcp/dist/test-global-setup.js +37 -0
- package/mcp/dist/tool-registry.js +104 -0
- package/mcp/dist/update.js +97 -0
- package/mcp/dist/utils.js +543 -0
- package/package.json +67 -0
- package/skills/README.md +7 -0
- package/skills/consolidate/SKILL.md +152 -0
- package/skills/discover/SKILL.md +175 -0
- package/skills/init/SKILL.md +216 -0
- package/skills/profiles/SKILL.md +121 -0
- package/skills/sync/SKILL.md +261 -0
- package/starter/README.md +74 -0
- package/starter/global/CLAUDE.md +89 -0
- package/starter/global/skills/humanize.md +30 -0
- package/starter/global/skills/pipeline.md +35 -0
- package/starter/global/skills/release.md +35 -0
- package/starter/machines.yaml +8 -0
- package/starter/my-api/.claude/skills/README.md +7 -0
- package/starter/my-api/CLAUDE.md +33 -0
- package/starter/my-api/FINDINGS.md +9 -0
- package/starter/my-api/summary.md +7 -0
- package/starter/my-api/tasks.md +7 -0
- package/starter/my-first-project/.claude/skills/README.md +7 -0
- package/starter/my-first-project/CLAUDE.md +49 -0
- package/starter/my-first-project/FINDINGS.md +24 -0
- package/starter/my-first-project/summary.md +11 -0
- package/starter/my-first-project/tasks.md +25 -0
- package/starter/my-frontend/.claude/skills/README.md +7 -0
- package/starter/my-frontend/CLAUDE.md +33 -0
- package/starter/my-frontend/FINDINGS.md +9 -0
- package/starter/my-frontend/summary.md +7 -0
- package/starter/my-frontend/tasks.md +7 -0
- package/starter/profiles/default.yaml +4 -0
- package/starter/profiles/personal.yaml +4 -0
- package/starter/profiles/work.yaml +4 -0
- package/starter/templates/README.md +7 -0
- package/starter/templates/frontend/CLAUDE.md +23 -0
- package/starter/templates/frontend/FINDINGS.md +7 -0
- package/starter/templates/frontend/reference/README.md +4 -0
- package/starter/templates/frontend/summary.md +7 -0
- package/starter/templates/frontend/tasks.md +11 -0
- package/starter/templates/library/CLAUDE.md +22 -0
- package/starter/templates/library/FINDINGS.md +7 -0
- package/starter/templates/library/reference/README.md +4 -0
- package/starter/templates/library/summary.md +7 -0
- package/starter/templates/library/tasks.md +11 -0
- package/starter/templates/monorepo/CLAUDE.md +21 -0
- package/starter/templates/monorepo/FINDINGS.md +7 -0
- package/starter/templates/monorepo/reference/README.md +4 -0
- package/starter/templates/monorepo/summary.md +7 -0
- package/starter/templates/monorepo/tasks.md +11 -0
- package/starter/templates/python-project/CLAUDE.md +21 -0
- package/starter/templates/python-project/FINDINGS.md +7 -0
- package/starter/templates/python-project/reference/README.md +4 -0
- package/starter/templates/python-project/summary.md +7 -0
- package/starter/templates/python-project/tasks.md +10 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import * as crypto from "crypto";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { appendAuditLog, debugLog, isRecord, memoryScoresFile, memoryUsageLogFile, runtimeFile } from "./shared.js";
|
|
5
|
+
import { withFileLock } from "./governance-locks.js";
|
|
6
|
+
import { errorMessage } from "./utils.js";
|
|
7
|
+
const GOVERNANCE_SCHEMA_VERSION = 1;
|
|
8
|
+
const DEFAULT_MEMORY_SCORES_FILE = {
|
|
9
|
+
schemaVersion: GOVERNANCE_SCHEMA_VERSION,
|
|
10
|
+
entries: {},
|
|
11
|
+
};
|
|
12
|
+
function usageLogFile(phrenPath) {
|
|
13
|
+
return memoryUsageLogFile(phrenPath);
|
|
14
|
+
}
|
|
15
|
+
function scoresJournalFile(phrenPath) {
|
|
16
|
+
return runtimeFile(phrenPath, "scores.jsonl");
|
|
17
|
+
}
|
|
18
|
+
function hasValidSchemaVersion(data) {
|
|
19
|
+
return !("schemaVersion" in data) || typeof data.schemaVersion === "number";
|
|
20
|
+
}
|
|
21
|
+
function isFiniteNumber(value) {
|
|
22
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
23
|
+
}
|
|
24
|
+
function isEntryScore(value) {
|
|
25
|
+
if (!isRecord(value))
|
|
26
|
+
return false;
|
|
27
|
+
return isFiniteNumber(value.impressions)
|
|
28
|
+
&& isFiniteNumber(value.helpful)
|
|
29
|
+
&& isFiniteNumber(value.repromptPenalty)
|
|
30
|
+
&& isFiniteNumber(value.regressionPenalty)
|
|
31
|
+
&& typeof value.lastUsedAt === "string";
|
|
32
|
+
}
|
|
33
|
+
function isVersionedEntries(data) {
|
|
34
|
+
return "entries" in data || "schemaVersion" in data;
|
|
35
|
+
}
|
|
36
|
+
function entriesObject(data) {
|
|
37
|
+
if (isRecord(data.entries))
|
|
38
|
+
return data.entries;
|
|
39
|
+
return data;
|
|
40
|
+
}
|
|
41
|
+
function validateScoresJson(filePath) {
|
|
42
|
+
try {
|
|
43
|
+
if (!fs.existsSync(filePath))
|
|
44
|
+
return true;
|
|
45
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
46
|
+
const data = JSON.parse(raw);
|
|
47
|
+
if (!isRecord(data))
|
|
48
|
+
return false;
|
|
49
|
+
if (isVersionedEntries(data) && !hasValidSchemaVersion(data))
|
|
50
|
+
return false;
|
|
51
|
+
if (isVersionedEntries(data) && !isRecord(data.entries))
|
|
52
|
+
return false;
|
|
53
|
+
return Object.values(entriesObject(data)).every((entry) => isEntryScore(entry));
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
debugLog(`validateScoresJson failed for ${filePath}: ${errorMessage(err)}`);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function normalizeVersionedEntries(data, guard) {
|
|
61
|
+
const out = {};
|
|
62
|
+
for (const [key, value] of Object.entries(entriesObject(data))) {
|
|
63
|
+
if (guard(value))
|
|
64
|
+
out[key] = value;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
schemaVersion: GOVERNANCE_SCHEMA_VERSION,
|
|
68
|
+
entries: out,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function readScoresFile(phrenPath) {
|
|
72
|
+
const file = memoryScoresFile(phrenPath);
|
|
73
|
+
try {
|
|
74
|
+
if (!fs.existsSync(file))
|
|
75
|
+
return { ...DEFAULT_MEMORY_SCORES_FILE.entries };
|
|
76
|
+
if (!validateScoresJson(file)) {
|
|
77
|
+
debugLog(`readScoresFile: ${file} failed validation, using defaults`);
|
|
78
|
+
return { ...DEFAULT_MEMORY_SCORES_FILE.entries };
|
|
79
|
+
}
|
|
80
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
81
|
+
return normalizeVersionedEntries(parsed, isEntryScore).entries;
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
debugLog(`readScoresFile failed for ${file}: ${errorMessage(err)}`);
|
|
85
|
+
return { ...DEFAULT_MEMORY_SCORES_FILE.entries };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function writeScoresFile(phrenPath, scores) {
|
|
89
|
+
const file = memoryScoresFile(phrenPath);
|
|
90
|
+
withFileLock(file, () => {
|
|
91
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
92
|
+
const tmpPath = path.join(path.dirname(file), `.tmp-${crypto.randomUUID()}`);
|
|
93
|
+
fs.writeFileSync(tmpPath, JSON.stringify({
|
|
94
|
+
schemaVersion: GOVERNANCE_SCHEMA_VERSION,
|
|
95
|
+
entries: scores,
|
|
96
|
+
}, null, 2) + "\n");
|
|
97
|
+
fs.renameSync(tmpPath, file);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
let scoresCache = null;
|
|
101
|
+
let scoresCachePath = null;
|
|
102
|
+
let scoresDirty = false;
|
|
103
|
+
function appendScoreJournal(phrenPath, key, delta) {
|
|
104
|
+
const file = scoresJournalFile(phrenPath);
|
|
105
|
+
const entry = { key, delta, at: new Date().toISOString() };
|
|
106
|
+
withFileLock(file, () => {
|
|
107
|
+
fs.appendFileSync(file, JSON.stringify(entry) + "\n");
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function readScoreJournal(phrenPath) {
|
|
111
|
+
const file = scoresJournalFile(phrenPath);
|
|
112
|
+
if (!fs.existsSync(file))
|
|
113
|
+
return [];
|
|
114
|
+
try {
|
|
115
|
+
return fs.readFileSync(file, "utf8")
|
|
116
|
+
.split("\n")
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
.map((line) => {
|
|
119
|
+
try {
|
|
120
|
+
return JSON.parse(line);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
|
|
124
|
+
process.stderr.write(`[phren] readScoreJournal parseLine: ${errorMessage(err)}\n`);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
.filter((entry) => entry !== null && typeof entry.key === "string" && typeof entry.delta === "object" && entry.delta !== null);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
debugLog(`readScoreJournal failed: ${errorMessage(err)}`);
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function claimScoreJournal(phrenPath) {
|
|
136
|
+
const file = scoresJournalFile(phrenPath);
|
|
137
|
+
let claimedFile = null;
|
|
138
|
+
withFileLock(file, () => {
|
|
139
|
+
if (!fs.existsSync(file))
|
|
140
|
+
return;
|
|
141
|
+
claimedFile = `${file}.${crypto.randomUUID()}.claim`;
|
|
142
|
+
fs.renameSync(file, claimedFile);
|
|
143
|
+
fs.writeFileSync(file, "");
|
|
144
|
+
});
|
|
145
|
+
if (!claimedFile)
|
|
146
|
+
return [];
|
|
147
|
+
try {
|
|
148
|
+
return fs.readFileSync(claimedFile, "utf8")
|
|
149
|
+
.split("\n")
|
|
150
|
+
.filter(Boolean)
|
|
151
|
+
.map((line) => {
|
|
152
|
+
try {
|
|
153
|
+
return JSON.parse(line);
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
|
|
157
|
+
process.stderr.write(`[phren] claimScoreJournal parseLine: ${errorMessage(err)}\n`);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
.filter((entry) => entry !== null && typeof entry.key === "string" && typeof entry.delta === "object" && entry.delta !== null);
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
debugLog(`claimScoreJournal failed: ${errorMessage(err)}`);
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
try {
|
|
169
|
+
fs.unlinkSync(claimedFile);
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
|
|
173
|
+
process.stderr.write(`[phren] claimScoreJournal unlinkClaim: ${errorMessage(err)}\n`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function aggregateJournalScores(entries) {
|
|
178
|
+
const aggregated = {};
|
|
179
|
+
for (const entry of entries) {
|
|
180
|
+
if (!aggregated[entry.key]) {
|
|
181
|
+
aggregated[entry.key] = { impressions: 0, helpful: 0, repromptPenalty: 0, regressionPenalty: 0, lastUsedAt: "" };
|
|
182
|
+
}
|
|
183
|
+
const current = aggregated[entry.key];
|
|
184
|
+
if (entry.delta.impressions)
|
|
185
|
+
current.impressions += entry.delta.impressions;
|
|
186
|
+
if (entry.delta.helpful)
|
|
187
|
+
current.helpful += entry.delta.helpful;
|
|
188
|
+
if (entry.delta.repromptPenalty)
|
|
189
|
+
current.repromptPenalty += entry.delta.repromptPenalty;
|
|
190
|
+
if (entry.delta.regressionPenalty)
|
|
191
|
+
current.regressionPenalty += entry.delta.regressionPenalty;
|
|
192
|
+
// Q24: carry the max journal timestamp so lastUsedAt is persisted correctly during flush
|
|
193
|
+
if (entry.at && entry.at > current.lastUsedAt)
|
|
194
|
+
current.lastUsedAt = entry.at;
|
|
195
|
+
}
|
|
196
|
+
return aggregated;
|
|
197
|
+
}
|
|
198
|
+
function ensureScoreEntry(scores, key) {
|
|
199
|
+
if (!scores[key]) {
|
|
200
|
+
scores[key] = {
|
|
201
|
+
impressions: 0,
|
|
202
|
+
helpful: 0,
|
|
203
|
+
repromptPenalty: 0,
|
|
204
|
+
regressionPenalty: 0,
|
|
205
|
+
lastUsedAt: new Date(0).toISOString(),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return scores[key];
|
|
209
|
+
}
|
|
210
|
+
function loadEntryScores(phrenPath) {
|
|
211
|
+
const file = memoryScoresFile(phrenPath);
|
|
212
|
+
if (scoresCache && scoresCachePath === file)
|
|
213
|
+
return scoresCache;
|
|
214
|
+
scoresCache = readScoresFile(phrenPath);
|
|
215
|
+
scoresCachePath = file;
|
|
216
|
+
scoresDirty = false;
|
|
217
|
+
return scoresCache;
|
|
218
|
+
}
|
|
219
|
+
function saveEntryScores(phrenPath, scores) {
|
|
220
|
+
scoresCache = scores;
|
|
221
|
+
scoresCachePath = memoryScoresFile(phrenPath);
|
|
222
|
+
scoresDirty = true;
|
|
223
|
+
}
|
|
224
|
+
export function flushEntryScores(phrenPath) {
|
|
225
|
+
// Invalidate journal cache since claimScoreJournal will clear the file
|
|
226
|
+
journalCache = null;
|
|
227
|
+
journalCachePath = null;
|
|
228
|
+
const journalEntries = claimScoreJournal(phrenPath);
|
|
229
|
+
if (journalEntries.length > 0) {
|
|
230
|
+
const scores = loadEntryScores(phrenPath);
|
|
231
|
+
const aggregated = aggregateJournalScores(journalEntries);
|
|
232
|
+
for (const [key, deltas] of Object.entries(aggregated)) {
|
|
233
|
+
const entry = ensureScoreEntry(scores, key);
|
|
234
|
+
entry.impressions += deltas.impressions;
|
|
235
|
+
entry.helpful += deltas.helpful;
|
|
236
|
+
entry.repromptPenalty += deltas.repromptPenalty;
|
|
237
|
+
entry.regressionPenalty += deltas.regressionPenalty;
|
|
238
|
+
// Q24: persist the max journal timestamp into lastUsedAt so recency boost advances correctly
|
|
239
|
+
if (deltas.lastUsedAt && deltas.lastUsedAt > entry.lastUsedAt) {
|
|
240
|
+
entry.lastUsedAt = deltas.lastUsedAt;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
saveEntryScores(phrenPath, scores);
|
|
244
|
+
}
|
|
245
|
+
if (scoresDirty && scoresCache && scoresCachePath === memoryScoresFile(phrenPath)) {
|
|
246
|
+
writeScoresFile(phrenPath, scoresCache);
|
|
247
|
+
scoresDirty = false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
export function entryScoreKey(project, filename, snippet) {
|
|
251
|
+
const short = snippet.replace(/\s+/g, " ").slice(0, 200);
|
|
252
|
+
const digest = crypto.createHash("sha1").update(`${project}:${filename}:${short}`).digest("hex").slice(0, 12);
|
|
253
|
+
return `${project}/${filename}:${digest}`;
|
|
254
|
+
}
|
|
255
|
+
export function recordInjection(phrenPath, key, sessionId) {
|
|
256
|
+
appendScoreJournal(phrenPath, key, { impressions: 1 });
|
|
257
|
+
const session = sessionId || "none";
|
|
258
|
+
const logFile = usageLogFile(phrenPath);
|
|
259
|
+
fs.mkdirSync(path.dirname(logFile), { recursive: true });
|
|
260
|
+
fs.appendFileSync(logFile, `${new Date().toISOString()}\tinject\t${session}\t${key}\n`);
|
|
261
|
+
try {
|
|
262
|
+
const stat = fs.statSync(logFile);
|
|
263
|
+
if (stat.size > 1_000_000) {
|
|
264
|
+
const content = fs.readFileSync(logFile, "utf8");
|
|
265
|
+
const lines = content.split("\n");
|
|
266
|
+
fs.writeFileSync(logFile, lines.slice(-500).join("\n"));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
debugLog(`Usage log rotation failed: ${errorMessage(err)}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
export function recordFeedback(phrenPath, key, feedback) {
|
|
274
|
+
const delta = {};
|
|
275
|
+
if (feedback === "helpful")
|
|
276
|
+
delta.helpful = 1;
|
|
277
|
+
if (feedback === "reprompt")
|
|
278
|
+
delta.repromptPenalty = 1;
|
|
279
|
+
if (feedback === "regression")
|
|
280
|
+
delta.regressionPenalty = 1;
|
|
281
|
+
appendScoreJournal(phrenPath, key, delta);
|
|
282
|
+
appendAuditLog(phrenPath, "memory_feedback", `key=${key} feedback=${feedback}`);
|
|
283
|
+
}
|
|
284
|
+
// Module-level cache for the journal aggregation used by getQualityMultiplier.
|
|
285
|
+
// Invalidated whenever flushEntryScores runs (at which point the journal is cleared).
|
|
286
|
+
let journalCache = null;
|
|
287
|
+
let journalCachePath = null;
|
|
288
|
+
function getJournalCache(phrenPath) {
|
|
289
|
+
const file = scoresJournalFile(phrenPath);
|
|
290
|
+
if (journalCache && journalCachePath === file)
|
|
291
|
+
return journalCache;
|
|
292
|
+
// Build the cache by reading the journal once and aggregating by key
|
|
293
|
+
const entries = readScoreJournal(phrenPath);
|
|
294
|
+
const cache = new Map();
|
|
295
|
+
for (const entry of entries) {
|
|
296
|
+
const cur = cache.get(entry.key) ?? { helpful: 0, repromptPenalty: 0, regressionPenalty: 0, impressions: 0, lastUsedAt: "" };
|
|
297
|
+
if (entry.delta.helpful)
|
|
298
|
+
cur.helpful += entry.delta.helpful;
|
|
299
|
+
if (entry.delta.repromptPenalty)
|
|
300
|
+
cur.repromptPenalty += entry.delta.repromptPenalty;
|
|
301
|
+
if (entry.delta.regressionPenalty)
|
|
302
|
+
cur.regressionPenalty += entry.delta.regressionPenalty;
|
|
303
|
+
if (entry.delta.impressions)
|
|
304
|
+
cur.impressions += entry.delta.impressions;
|
|
305
|
+
if (entry.at && entry.at > cur.lastUsedAt)
|
|
306
|
+
cur.lastUsedAt = entry.at;
|
|
307
|
+
cache.set(entry.key, cur);
|
|
308
|
+
}
|
|
309
|
+
journalCache = cache;
|
|
310
|
+
journalCachePath = file;
|
|
311
|
+
return cache;
|
|
312
|
+
}
|
|
313
|
+
export function getQualityMultiplier(phrenPath, key) {
|
|
314
|
+
const scores = loadEntryScores(phrenPath);
|
|
315
|
+
const entry = scores[key];
|
|
316
|
+
let helpful = entry ? entry.helpful : 0;
|
|
317
|
+
let repromptPenalty = entry ? entry.repromptPenalty : 0;
|
|
318
|
+
let regressionPenalty = entry ? entry.regressionPenalty : 0;
|
|
319
|
+
let impressions = entry ? entry.impressions : 0;
|
|
320
|
+
let lastUsedAt = entry ? entry.lastUsedAt : "";
|
|
321
|
+
// Use the cached journal aggregation to avoid O(n×m) reads during ranking
|
|
322
|
+
const journalAgg = getJournalCache(phrenPath).get(key);
|
|
323
|
+
const hasJournalData = journalAgg !== undefined;
|
|
324
|
+
if (journalAgg) {
|
|
325
|
+
helpful += journalAgg.helpful;
|
|
326
|
+
repromptPenalty += journalAgg.repromptPenalty;
|
|
327
|
+
regressionPenalty += journalAgg.regressionPenalty;
|
|
328
|
+
impressions += journalAgg.impressions;
|
|
329
|
+
if (journalAgg.lastUsedAt && journalAgg.lastUsedAt > lastUsedAt)
|
|
330
|
+
lastUsedAt = journalAgg.lastUsedAt;
|
|
331
|
+
}
|
|
332
|
+
if (!entry && !hasJournalData)
|
|
333
|
+
return 1;
|
|
334
|
+
let recencyBoost = 0;
|
|
335
|
+
if (lastUsedAt) {
|
|
336
|
+
const lastUsedMs = new Date(lastUsedAt).getTime();
|
|
337
|
+
if (!Number.isNaN(lastUsedMs)) {
|
|
338
|
+
const daysSinceUse = Math.max(0, (Date.now() - lastUsedMs) / 86_400_000);
|
|
339
|
+
if (daysSinceUse <= 7) {
|
|
340
|
+
recencyBoost = 0.15;
|
|
341
|
+
}
|
|
342
|
+
else if (daysSinceUse <= 30) {
|
|
343
|
+
recencyBoost = 0;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
recencyBoost = -0.1 * Math.min(3, (daysSinceUse - 30) / 30);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const frequencyBoost = impressions > 0 ? Math.min(0.2, Math.log2(impressions + 1) * 0.05) : 0;
|
|
351
|
+
const penalties = repromptPenalty + regressionPenalty * 2;
|
|
352
|
+
const feedbackScore = helpful * 0.15 - penalties * 0.2;
|
|
353
|
+
const raw = 1 + feedbackScore + recencyBoost + frequencyBoost;
|
|
354
|
+
return Math.max(0.2, Math.min(1.5, raw));
|
|
355
|
+
}
|