@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.
Files changed (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +590 -0
  3. package/mcp/dist/capabilities/cli.js +61 -0
  4. package/mcp/dist/capabilities/index.js +15 -0
  5. package/mcp/dist/capabilities/mcp.js +61 -0
  6. package/mcp/dist/capabilities/types.js +57 -0
  7. package/mcp/dist/capabilities/vscode.js +61 -0
  8. package/mcp/dist/capabilities/web-ui.js +61 -0
  9. package/mcp/dist/cli-actions.js +302 -0
  10. package/mcp/dist/cli-config.js +580 -0
  11. package/mcp/dist/cli-extract.js +305 -0
  12. package/mcp/dist/cli-govern.js +371 -0
  13. package/mcp/dist/cli-graph.js +169 -0
  14. package/mcp/dist/cli-hooks-citations.js +44 -0
  15. package/mcp/dist/cli-hooks-context.js +56 -0
  16. package/mcp/dist/cli-hooks-globs.js +83 -0
  17. package/mcp/dist/cli-hooks-output.js +130 -0
  18. package/mcp/dist/cli-hooks-retrieval.js +2 -0
  19. package/mcp/dist/cli-hooks-session.js +1402 -0
  20. package/mcp/dist/cli-hooks.js +350 -0
  21. package/mcp/dist/cli-namespaces.js +989 -0
  22. package/mcp/dist/cli-ops.js +253 -0
  23. package/mcp/dist/cli-search.js +407 -0
  24. package/mcp/dist/cli.js +108 -0
  25. package/mcp/dist/content-archive.js +278 -0
  26. package/mcp/dist/content-citation.js +391 -0
  27. package/mcp/dist/content-dedup.js +622 -0
  28. package/mcp/dist/content-learning.js +472 -0
  29. package/mcp/dist/content-metadata.js +186 -0
  30. package/mcp/dist/content-validate.js +462 -0
  31. package/mcp/dist/core-finding.js +54 -0
  32. package/mcp/dist/core-project.js +36 -0
  33. package/mcp/dist/core-search.js +50 -0
  34. package/mcp/dist/data-access.js +400 -0
  35. package/mcp/dist/data-tasks.js +821 -0
  36. package/mcp/dist/embedding.js +344 -0
  37. package/mcp/dist/entrypoint.js +387 -0
  38. package/mcp/dist/finding-context.js +172 -0
  39. package/mcp/dist/finding-impact.js +181 -0
  40. package/mcp/dist/finding-journal.js +122 -0
  41. package/mcp/dist/finding-lifecycle.js +259 -0
  42. package/mcp/dist/governance-audit.js +22 -0
  43. package/mcp/dist/governance-locks.js +96 -0
  44. package/mcp/dist/governance-policy.js +648 -0
  45. package/mcp/dist/governance-scores.js +355 -0
  46. package/mcp/dist/hooks.js +449 -0
  47. package/mcp/dist/impact-scoring.js +22 -0
  48. package/mcp/dist/index-query.js +168 -0
  49. package/mcp/dist/index.js +205 -0
  50. package/mcp/dist/init-config.js +336 -0
  51. package/mcp/dist/init-preferences.js +62 -0
  52. package/mcp/dist/init-setup.js +1305 -0
  53. package/mcp/dist/init-shared.js +29 -0
  54. package/mcp/dist/init.js +1730 -0
  55. package/mcp/dist/link-checksums.js +62 -0
  56. package/mcp/dist/link-context.js +257 -0
  57. package/mcp/dist/link-doctor.js +591 -0
  58. package/mcp/dist/link-skills.js +212 -0
  59. package/mcp/dist/link.js +596 -0
  60. package/mcp/dist/logger.js +15 -0
  61. package/mcp/dist/machine-identity.js +38 -0
  62. package/mcp/dist/mcp-config.js +254 -0
  63. package/mcp/dist/mcp-data.js +315 -0
  64. package/mcp/dist/mcp-extract-facts.js +78 -0
  65. package/mcp/dist/mcp-extract.js +133 -0
  66. package/mcp/dist/mcp-finding.js +557 -0
  67. package/mcp/dist/mcp-graph.js +339 -0
  68. package/mcp/dist/mcp-hooks.js +256 -0
  69. package/mcp/dist/mcp-memory.js +58 -0
  70. package/mcp/dist/mcp-ops.js +328 -0
  71. package/mcp/dist/mcp-search.js +628 -0
  72. package/mcp/dist/mcp-session.js +651 -0
  73. package/mcp/dist/mcp-skills.js +189 -0
  74. package/mcp/dist/mcp-tasks.js +551 -0
  75. package/mcp/dist/mcp-types.js +7 -0
  76. package/mcp/dist/memory-ui-assets.js +6 -0
  77. package/mcp/dist/memory-ui-data.js +513 -0
  78. package/mcp/dist/memory-ui-graph.js +1910 -0
  79. package/mcp/dist/memory-ui-page.js +353 -0
  80. package/mcp/dist/memory-ui-scripts.js +1387 -0
  81. package/mcp/dist/memory-ui-server.js +1218 -0
  82. package/mcp/dist/memory-ui-styles.js +555 -0
  83. package/mcp/dist/memory-ui.js +9 -0
  84. package/mcp/dist/package-metadata.js +13 -0
  85. package/mcp/dist/phren-art.js +52 -0
  86. package/mcp/dist/phren-core.js +108 -0
  87. package/mcp/dist/phren-dotenv.js +67 -0
  88. package/mcp/dist/phren-paths.js +476 -0
  89. package/mcp/dist/proactivity.js +172 -0
  90. package/mcp/dist/profile-store.js +228 -0
  91. package/mcp/dist/project-config.js +85 -0
  92. package/mcp/dist/project-locator.js +25 -0
  93. package/mcp/dist/project-topics.js +1134 -0
  94. package/mcp/dist/provider-adapters.js +176 -0
  95. package/mcp/dist/runtime-profile.js +18 -0
  96. package/mcp/dist/session-checkpoints.js +131 -0
  97. package/mcp/dist/session-utils.js +68 -0
  98. package/mcp/dist/shared-content.js +8 -0
  99. package/mcp/dist/shared-embedding-cache.js +143 -0
  100. package/mcp/dist/shared-fragment-graph.js +456 -0
  101. package/mcp/dist/shared-governance.js +4 -0
  102. package/mcp/dist/shared-index.js +1334 -0
  103. package/mcp/dist/shared-ollama.js +192 -0
  104. package/mcp/dist/shared-paths.js +1 -0
  105. package/mcp/dist/shared-retrieval.js +796 -0
  106. package/mcp/dist/shared-search-fallback.js +375 -0
  107. package/mcp/dist/shared-sqljs.js +42 -0
  108. package/mcp/dist/shared-stemmer.js +171 -0
  109. package/mcp/dist/shared-vector-index.js +199 -0
  110. package/mcp/dist/shared.js +114 -0
  111. package/mcp/dist/shell-entry.js +209 -0
  112. package/mcp/dist/shell-input.js +943 -0
  113. package/mcp/dist/shell-palette.js +119 -0
  114. package/mcp/dist/shell-render.js +252 -0
  115. package/mcp/dist/shell-state-store.js +81 -0
  116. package/mcp/dist/shell-types.js +13 -0
  117. package/mcp/dist/shell-view-list.js +14 -0
  118. package/mcp/dist/shell-view.js +707 -0
  119. package/mcp/dist/shell.js +352 -0
  120. package/mcp/dist/skill-files.js +117 -0
  121. package/mcp/dist/skill-registry.js +279 -0
  122. package/mcp/dist/skill-state.js +28 -0
  123. package/mcp/dist/startup-embedding.js +57 -0
  124. package/mcp/dist/status.js +323 -0
  125. package/mcp/dist/synonyms.json +670 -0
  126. package/mcp/dist/task-hygiene.js +251 -0
  127. package/mcp/dist/task-lifecycle.js +347 -0
  128. package/mcp/dist/tasks-github.js +76 -0
  129. package/mcp/dist/telemetry.js +165 -0
  130. package/mcp/dist/test-global-setup.js +37 -0
  131. package/mcp/dist/tool-registry.js +104 -0
  132. package/mcp/dist/update.js +97 -0
  133. package/mcp/dist/utils.js +543 -0
  134. package/package.json +67 -0
  135. package/skills/README.md +7 -0
  136. package/skills/consolidate/SKILL.md +152 -0
  137. package/skills/discover/SKILL.md +175 -0
  138. package/skills/init/SKILL.md +216 -0
  139. package/skills/profiles/SKILL.md +121 -0
  140. package/skills/sync/SKILL.md +261 -0
  141. package/starter/README.md +74 -0
  142. package/starter/global/CLAUDE.md +89 -0
  143. package/starter/global/skills/humanize.md +30 -0
  144. package/starter/global/skills/pipeline.md +35 -0
  145. package/starter/global/skills/release.md +35 -0
  146. package/starter/machines.yaml +8 -0
  147. package/starter/my-api/.claude/skills/README.md +7 -0
  148. package/starter/my-api/CLAUDE.md +33 -0
  149. package/starter/my-api/FINDINGS.md +9 -0
  150. package/starter/my-api/summary.md +7 -0
  151. package/starter/my-api/tasks.md +7 -0
  152. package/starter/my-first-project/.claude/skills/README.md +7 -0
  153. package/starter/my-first-project/CLAUDE.md +49 -0
  154. package/starter/my-first-project/FINDINGS.md +24 -0
  155. package/starter/my-first-project/summary.md +11 -0
  156. package/starter/my-first-project/tasks.md +25 -0
  157. package/starter/my-frontend/.claude/skills/README.md +7 -0
  158. package/starter/my-frontend/CLAUDE.md +33 -0
  159. package/starter/my-frontend/FINDINGS.md +9 -0
  160. package/starter/my-frontend/summary.md +7 -0
  161. package/starter/my-frontend/tasks.md +7 -0
  162. package/starter/profiles/default.yaml +4 -0
  163. package/starter/profiles/personal.yaml +4 -0
  164. package/starter/profiles/work.yaml +4 -0
  165. package/starter/templates/README.md +7 -0
  166. package/starter/templates/frontend/CLAUDE.md +23 -0
  167. package/starter/templates/frontend/FINDINGS.md +7 -0
  168. package/starter/templates/frontend/reference/README.md +4 -0
  169. package/starter/templates/frontend/summary.md +7 -0
  170. package/starter/templates/frontend/tasks.md +11 -0
  171. package/starter/templates/library/CLAUDE.md +22 -0
  172. package/starter/templates/library/FINDINGS.md +7 -0
  173. package/starter/templates/library/reference/README.md +4 -0
  174. package/starter/templates/library/summary.md +7 -0
  175. package/starter/templates/library/tasks.md +11 -0
  176. package/starter/templates/monorepo/CLAUDE.md +21 -0
  177. package/starter/templates/monorepo/FINDINGS.md +7 -0
  178. package/starter/templates/monorepo/reference/README.md +4 -0
  179. package/starter/templates/monorepo/summary.md +7 -0
  180. package/starter/templates/monorepo/tasks.md +11 -0
  181. package/starter/templates/python-project/CLAUDE.md +21 -0
  182. package/starter/templates/python-project/FINDINGS.md +7 -0
  183. package/starter/templates/python-project/reference/README.md +4 -0
  184. package/starter/templates/python-project/summary.md +7 -0
  185. package/starter/templates/python-project/tasks.md +10 -0
@@ -0,0 +1,391 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { debugLog, EXEC_TIMEOUT_MS, EXEC_TIMEOUT_QUICK_MS } from "./shared.js";
4
+ import { errorMessage, runGitOrThrow } from "./utils.js";
5
+ import { findingIdFromLine } from "./finding-impact.js";
6
+ import { METADATA_REGEX, isArchiveStart, isArchiveEnd } from "./content-metadata.js";
7
+ export const FINDING_PROVENANCE_SOURCES = [
8
+ "human",
9
+ "agent",
10
+ "hook",
11
+ "extract",
12
+ "consolidation",
13
+ "unknown",
14
+ ];
15
+ export function isFindingProvenanceSource(value) {
16
+ if (!value)
17
+ return false;
18
+ return FINDING_PROVENANCE_SOURCES.includes(value);
19
+ }
20
+ export function getHeadCommit(cwd) {
21
+ try {
22
+ const commit = runGitOrThrow(cwd, ["rev-parse", "HEAD"], EXEC_TIMEOUT_QUICK_MS).trim();
23
+ return commit || undefined;
24
+ }
25
+ catch (err) {
26
+ debugLog(`getHeadCommit: git rev-parse HEAD failed in ${cwd}: ${errorMessage(err)}`);
27
+ return undefined;
28
+ }
29
+ }
30
+ export function getRepoRoot(cwd) {
31
+ try {
32
+ const root = runGitOrThrow(cwd, ["rev-parse", "--show-toplevel"], EXEC_TIMEOUT_QUICK_MS).trim();
33
+ return root || undefined;
34
+ }
35
+ catch (err) {
36
+ debugLog(`getRepoRoot: not a git repo or git unavailable in ${cwd}: ${errorMessage(err)}`);
37
+ return undefined;
38
+ }
39
+ }
40
+ export function inferCitationLocation(repoPath, commit) {
41
+ try {
42
+ const raw = runGitOrThrow(repoPath, ["show", "--pretty=format:", "--unified=0", "--no-color", commit], EXEC_TIMEOUT_MS);
43
+ let currentFile = "";
44
+ for (const line of raw.split("\n")) {
45
+ if (line.startsWith("+++ b/")) {
46
+ currentFile = line.slice(6).trim();
47
+ continue;
48
+ }
49
+ const hunk = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
50
+ if (hunk && currentFile) {
51
+ return { file: currentFile, line: Number.parseInt(hunk[1], 10) };
52
+ }
53
+ }
54
+ }
55
+ catch (err) {
56
+ debugLog(`citationLocationFromCommit: git show failed: ${errorMessage(err)}`);
57
+ }
58
+ return {};
59
+ }
60
+ export function buildCitationComment(citation) {
61
+ return `<!-- phren:cite ${JSON.stringify(citation)} -->`;
62
+ }
63
+ function readSourceToken(match) {
64
+ if (!match?.[1])
65
+ return undefined;
66
+ const raw = match[1].trim();
67
+ if (!raw)
68
+ return undefined;
69
+ if (raw.startsWith("\"") && raw.endsWith("\"") && raw.length >= 2) {
70
+ return raw.slice(1, -1);
71
+ }
72
+ return raw;
73
+ }
74
+ export function buildSourceComment(source) {
75
+ const parts = [];
76
+ if (source.source)
77
+ parts.push(source.source);
78
+ if (source.machine)
79
+ parts.push(`machine:${source.machine}`);
80
+ if (source.actor)
81
+ parts.push(`actor:${source.actor}`);
82
+ if (source.tool)
83
+ parts.push(`tool:${source.tool}`);
84
+ if (source.model)
85
+ parts.push(`model:${source.model}`);
86
+ if (source.session_id)
87
+ parts.push(`session:${source.session_id}`);
88
+ if (source.scope)
89
+ parts.push(`scope:${source.scope}`);
90
+ return parts.length > 0 ? `<!-- source:${parts.join(" ")} -->` : "";
91
+ }
92
+ export function parseSourceComment(line) {
93
+ const sourceMatch = line.match(METADATA_REGEX.source);
94
+ if (!sourceMatch)
95
+ return null;
96
+ const payload = sourceMatch[1];
97
+ const firstToken = payload.trim().split(/\s+/)[0] || "";
98
+ const sourceRaw = (firstToken && !firstToken.includes(":") ? firstToken : undefined) ??
99
+ readSourceToken(payload.match(/(?:^|\s)source:(".*?"|\S+)/)) ??
100
+ readSourceToken(payload.match(/(?:^|\s)kind:(".*?"|\S+)/));
101
+ const source = isFindingProvenanceSource(sourceRaw) ? sourceRaw : undefined;
102
+ const machine = readSourceToken(payload.match(/(?:^|\s)machine:(".*?"|\S+)/)) ??
103
+ readSourceToken(payload.match(/(?:^|\s)host:(".*?"|\S+)/));
104
+ const actor = readSourceToken(payload.match(/(?:^|\s)actor:(".*?"|\S+)/)) ??
105
+ readSourceToken(payload.match(/(?:^|\s)agent:(".*?"|\S+)/));
106
+ const tool = readSourceToken(payload.match(/(?:^|\s)tool:(".*?"|\S+)/));
107
+ const model = readSourceToken(payload.match(/(?:^|\s)model:(".*?"|\S+)/));
108
+ const session_id = readSourceToken(payload.match(/(?:^|\s)session:(".*?"|\S+)/)) ??
109
+ readSourceToken(payload.match(/(?:^|\s)session_id:(".*?"|\S+)/));
110
+ const rawScope = readSourceToken(payload.match(/(?:^|\s)scope:(".*?"|\S+)/));
111
+ const scope = rawScope === undefined
112
+ ? undefined
113
+ : (rawScope.trim() ? rawScope.trim() : "shared");
114
+ if (!source && !machine && !actor && !tool && !model && !session_id && !scope)
115
+ return null;
116
+ return { source, machine, actor, tool, model, session_id, scope };
117
+ }
118
+ export function parseCitationComment(line) {
119
+ // Find opening marker and closing --> to handle multiline/escaped JSON.
120
+ // Uses marker-based extraction instead of regex to support multiline JSON.
121
+ const markerMatch = line.match(METADATA_REGEX.citationMarker);
122
+ if (!markerMatch)
123
+ return null;
124
+ const jsonStart = markerMatch.index + markerMatch[0].length;
125
+ const endMarker = line.indexOf("-->", jsonStart);
126
+ if (endMarker === -1)
127
+ return null;
128
+ const jsonStr = line.slice(jsonStart, endMarker).trim();
129
+ if (!jsonStr.startsWith("{"))
130
+ return null;
131
+ try {
132
+ const parsed = JSON.parse(jsonStr);
133
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
134
+ return null;
135
+ // Default created_at to empty string if missing, but still require it to be string-like
136
+ const created_at = typeof parsed.created_at === "string" ? parsed.created_at : "";
137
+ if (!created_at)
138
+ return null;
139
+ return {
140
+ created_at,
141
+ repo: typeof parsed.repo === "string" ? parsed.repo : undefined,
142
+ file: typeof parsed.file === "string" ? parsed.file : undefined,
143
+ line: typeof parsed.line === "number" ? parsed.line : undefined,
144
+ commit: typeof parsed.commit === "string" ? parsed.commit : undefined,
145
+ supersedes: typeof parsed.supersedes === "string" ? parsed.supersedes : undefined,
146
+ task_item: typeof parsed.task_item === "string" ? parsed.task_item : undefined,
147
+ };
148
+ }
149
+ catch (err) {
150
+ debugLog(`parseCitationComment: malformed citation JSON: ${errorMessage(err)}`);
151
+ return null;
152
+ }
153
+ }
154
+ function resolveCitationFile(citation) {
155
+ if (!citation.file)
156
+ return null;
157
+ if (citation.repo) {
158
+ const resolved = path.resolve(citation.repo, citation.file);
159
+ const repoRoot = path.resolve(citation.repo);
160
+ // Require resolved path to stay inside the repo to prevent file probing
161
+ if (resolved !== repoRoot && !resolved.startsWith(repoRoot + path.sep))
162
+ return null;
163
+ return resolved;
164
+ }
165
+ if (path.isAbsolute(citation.file))
166
+ return citation.file;
167
+ return path.resolve(citation.file);
168
+ }
169
+ // Session-scoped caches for git I/O during citation validation.
170
+ // Keyed by "repo\0commit" and "repo\0file\0line" respectively.
171
+ const commitExistsCache = new Map();
172
+ const blameCache = new Map();
173
+ function commitExists(repoPath, commit) {
174
+ const key = `${repoPath}\0${commit}`;
175
+ const cached = commitExistsCache.get(key);
176
+ if (cached !== undefined)
177
+ return cached;
178
+ try {
179
+ runGitOrThrow(repoPath, ["cat-file", "-e", `${commit}^{commit}`], EXEC_TIMEOUT_QUICK_MS);
180
+ commitExistsCache.set(key, true);
181
+ return true;
182
+ }
183
+ catch (err) {
184
+ debugLog(`commitExists: commit ${commit} not found in ${repoPath}: ${errorMessage(err)}`);
185
+ commitExistsCache.set(key, false);
186
+ return false;
187
+ }
188
+ }
189
+ function cachedBlame(repoPath, relFile, line) {
190
+ const key = `${repoPath}\0${relFile}\0${line}`;
191
+ const cached = blameCache.get(key);
192
+ if (cached !== undefined)
193
+ return cached;
194
+ try {
195
+ const out = runGitOrThrow(repoPath, ["blame", "-L", `${line},${line}`, "--porcelain", relFile], 10_000).trim();
196
+ const first = out.split("\n")[0] || "";
197
+ blameCache.set(key, first);
198
+ return first;
199
+ }
200
+ catch (err) {
201
+ debugLog(`cachedBlame: git blame failed for ${relFile}:${line}: ${errorMessage(err)}`);
202
+ blameCache.set(key, false);
203
+ return false;
204
+ }
205
+ }
206
+ export function validateFindingCitation(citation) {
207
+ if (citation.repo && !fs.existsSync(citation.repo))
208
+ return false;
209
+ if (citation.commit && citation.repo && !commitExists(citation.repo, citation.commit))
210
+ return false;
211
+ const resolvedFile = resolveCitationFile(citation);
212
+ if (resolvedFile) {
213
+ if (!fs.existsSync(resolvedFile))
214
+ return false;
215
+ if (citation.line !== undefined) {
216
+ if (!Number.isInteger(citation.line) || citation.line < 1)
217
+ return false;
218
+ const lineCount = fs.readFileSync(resolvedFile, "utf8").split("\n").length;
219
+ if (citation.line > lineCount)
220
+ return false;
221
+ if (citation.commit && citation.repo) {
222
+ const relFile = path.isAbsolute(resolvedFile)
223
+ ? path.relative(citation.repo, resolvedFile)
224
+ : resolvedFile;
225
+ const first = cachedBlame(citation.repo, relFile, citation.line);
226
+ if (first === false || !first.startsWith(citation.commit))
227
+ return false;
228
+ }
229
+ }
230
+ }
231
+ return true;
232
+ }
233
+ function parseLearningDateHeading(line) {
234
+ const match = line.match(/^## (\d{4}-\d{2}-\d{2})$/);
235
+ return match ? match[1] : null;
236
+ }
237
+ function isDateStale(headingDate, ttlDays) {
238
+ const ts = Date.parse(`${headingDate}T00:00:00Z`);
239
+ if (Number.isNaN(ts))
240
+ return false;
241
+ const ageDays = Math.floor((Date.now() - ts) / 86400000);
242
+ return ageDays > ttlDays;
243
+ }
244
+ function ageDaysForDate(headingDate) {
245
+ const ts = Date.parse(`${headingDate}T00:00:00Z`);
246
+ if (Number.isNaN(ts))
247
+ return null;
248
+ return Math.floor((Date.now() - ts) / 86400000);
249
+ }
250
+ const DEFAULT_DECAY = {
251
+ d30: 1.0,
252
+ d60: 0.85,
253
+ d90: 0.65,
254
+ d120: 0.45,
255
+ };
256
+ // Treat undated findings as oldest decay tier so they eventually get filtered.
257
+ // Previously 0.7 which meant undated findings never decayed.
258
+ const DEFAULT_UNDATED_CONFIDENCE = DEFAULT_DECAY.d120;
259
+ function confidenceForAge(ageDays, decay) {
260
+ const { d30 = 1.0, d60 = 0.85, d90 = 0.65, d120 = 0.45 } = decay;
261
+ if (ageDays <= 0)
262
+ return 1.0;
263
+ if (ageDays <= 30)
264
+ return 1.0 - ((1.0 - d30) * (ageDays / 30));
265
+ if (ageDays <= 60)
266
+ return d30 - ((d30 - d60) * ((ageDays - 30) / 30));
267
+ if (ageDays <= 90)
268
+ return d60 - ((d60 - d90) * ((ageDays - 60) / 30));
269
+ if (ageDays <= 120)
270
+ return d90 - ((d90 - d120) * ((ageDays - 90) / 30));
271
+ return d120; // don't decay further past d120; TTL handles final expiry
272
+ }
273
+ export function filterTrustedFindings(content, ttlDays) {
274
+ return filterTrustedFindingsDetailed(content, { ttlDays }).content;
275
+ }
276
+ export function filterTrustedFindingsDetailed(content, opts) {
277
+ const options = typeof opts === "number" ? { ttlDays: opts } : opts;
278
+ const ttlDays = options.ttlDays ?? 120;
279
+ const minConfidence = options.minConfidence ?? 0.35;
280
+ const decay = {
281
+ ...DEFAULT_DECAY,
282
+ ...(options.decay || {}),
283
+ };
284
+ const highImpactFindingIds = options.highImpactFindingIds;
285
+ const project = options.project;
286
+ const lines = content.split("\n");
287
+ const out = [];
288
+ const issues = [];
289
+ let currentDate = null;
290
+ let headingBuffer = [];
291
+ let inDetails = false;
292
+ const flushHeading = (hasEntries) => {
293
+ if (headingBuffer.length === 0)
294
+ return;
295
+ if (hasEntries) {
296
+ out.push(...headingBuffer);
297
+ if (out.length > 0 && out[out.length - 1] !== "")
298
+ out.push("");
299
+ }
300
+ headingBuffer = [];
301
+ };
302
+ for (let i = 0; i < lines.length; i++) {
303
+ const line = lines[i];
304
+ if (isArchiveStart(line)) {
305
+ inDetails = true;
306
+ continue;
307
+ }
308
+ if (isArchiveEnd(line)) {
309
+ inDetails = false;
310
+ continue;
311
+ }
312
+ if (inDetails)
313
+ continue;
314
+ const headingDate = parseLearningDateHeading(line);
315
+ if (headingDate) {
316
+ flushHeading(false);
317
+ currentDate = headingDate;
318
+ headingBuffer = [line];
319
+ continue;
320
+ }
321
+ if (line.startsWith("# ")) {
322
+ if (out.length === 0)
323
+ out.push(line, "");
324
+ continue;
325
+ }
326
+ if (!line.startsWith("- "))
327
+ continue;
328
+ // Determine the effective date for this bullet: heading date, inline created tag, or citation
329
+ const next = lines[i + 1] ?? "";
330
+ const citation = parseCitationComment(next);
331
+ let effectiveDate = currentDate;
332
+ if (!effectiveDate) {
333
+ const inlineCreated = line.match(METADATA_REGEX.createdDate);
334
+ if (inlineCreated) {
335
+ effectiveDate = inlineCreated[1];
336
+ }
337
+ else if (citation?.created_at) {
338
+ const citationDate = citation.created_at.slice(0, 10);
339
+ if (/^\d{4}-\d{2}-\d{2}$/.test(citationDate)) {
340
+ effectiveDate = citationDate;
341
+ }
342
+ }
343
+ }
344
+ const stale = effectiveDate ? isDateStale(effectiveDate, ttlDays) : false;
345
+ if (stale) {
346
+ issues.push({ date: effectiveDate || "unknown", bullet: line, reason: "stale" });
347
+ if (citation)
348
+ i++;
349
+ continue;
350
+ }
351
+ let confidence;
352
+ if (effectiveDate) {
353
+ const age = ageDaysForDate(effectiveDate);
354
+ confidence = age !== null ? confidenceForAge(age, decay) : DEFAULT_UNDATED_CONFIDENCE;
355
+ }
356
+ else {
357
+ confidence = DEFAULT_UNDATED_CONFIDENCE;
358
+ }
359
+ if (citation && !validateFindingCitation(citation)) {
360
+ issues.push({ date: effectiveDate || "unknown", bullet: line, reason: "invalid_citation" });
361
+ i++;
362
+ continue;
363
+ }
364
+ if (!citation)
365
+ confidence *= 0.8;
366
+ const provenance = parseSourceComment(line)?.source ?? "unknown";
367
+ if (provenance === "human")
368
+ confidence *= 1.1;
369
+ if (provenance === "extract")
370
+ confidence *= 0.9;
371
+ if (project && highImpactFindingIds?.size) {
372
+ const findingId = findingIdFromLine(line);
373
+ if (highImpactFindingIds.has(findingId))
374
+ confidence *= 1.15;
375
+ }
376
+ confidence = Math.max(0, Math.min(1, confidence));
377
+ if (confidence < minConfidence) {
378
+ issues.push({ date: effectiveDate || "unknown", bullet: line, reason: "stale" });
379
+ if (citation)
380
+ i++;
381
+ continue;
382
+ }
383
+ flushHeading(true);
384
+ out.push(line);
385
+ if (citation) {
386
+ out.push(next);
387
+ i++;
388
+ }
389
+ }
390
+ return { content: out.join("\n").trim(), issues };
391
+ }