wtf-p 0.3.0 → 0.5.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 (183) hide show
  1. package/README.md +146 -208
  2. package/bin/commands/doctor.js +88 -2
  3. package/bin/commands/install-logic.js +127 -36
  4. package/bin/commands/status.js +22 -4
  5. package/bin/commands/update.js +5 -4
  6. package/bin/install.js +66 -26
  7. package/bin/lib/analyze-impact.js +105 -0
  8. package/bin/lib/bib-format.js +161 -0
  9. package/bin/lib/bib-index.js +104 -0
  10. package/bin/lib/checkpoint.js +183 -0
  11. package/bin/lib/citation-fetcher.js +299 -0
  12. package/bin/lib/citation-ranker.js +133 -0
  13. package/bin/lib/context-primer.js +214 -0
  14. package/bin/lib/manifest.js +80 -2
  15. package/bin/lib/scholar-lookup.js +188 -0
  16. package/bin/lib/semantic-scholar.js +184 -0
  17. package/bin/lib/utils.js +22 -1
  18. package/bin/uninstall.js +270 -111
  19. package/core/write-the-f-paper/references/agent-model-matrix.md +62 -0
  20. package/core/write-the-f-paper/references/checkpoints.md +160 -9
  21. package/core/write-the-f-paper/references/context-fidelity.md +153 -0
  22. package/core/write-the-f-paper/references/deviation-rules.md +150 -0
  23. package/core/write-the-f-paper/references/git-integration.md +80 -0
  24. package/core/write-the-f-paper/references/orchestrator-pattern.md +167 -0
  25. package/core/write-the-f-paper/references/planning-config.md +135 -0
  26. package/core/write-the-f-paper/references/ui-brand.md +186 -0
  27. package/core/write-the-f-paper/templates/UAT.md +80 -0
  28. package/core/write-the-f-paper/templates/base-prefs.yaml +42 -0
  29. package/core/write-the-f-paper/templates/config.json +31 -5
  30. package/core/write-the-f-paper/workflows/create-outline.wcn.md +1 -1
  31. package/core/write-the-f-paper/workflows/execute-outline.md +163 -0
  32. package/core/write-the-f-paper/workflows/execute-outline.wcn.md +73 -0
  33. package/core/write-the-f-paper/workflows/execute-section.wcn.md +141 -169
  34. package/core/write-the-f-paper/workflows/plan-section.wcn.md +96 -103
  35. package/core/write-the-f-paper/workflows/research-gap.md +27 -3
  36. package/core/write-the-f-paper/workflows/research-gap.wcn.md +18 -1
  37. package/core/write-the-f-paper/workflows/verify-work.md +251 -0
  38. package/core/write-the-f-paper/workflows/verify-work.wcn.md +74 -0
  39. package/package.json +19 -7
  40. package/vendors/claude/.claude-plugin/plugin.json +2 -2
  41. package/vendors/claude/agents/wtfp/argument-verifier.md +189 -0
  42. package/vendors/claude/agents/wtfp/citation-expert.md +45 -0
  43. package/vendors/claude/agents/wtfp/citation-formatter.md +42 -0
  44. package/vendors/claude/agents/wtfp/coherence-checker.md +167 -0
  45. package/vendors/claude/agents/wtfp/outliner.md +259 -0
  46. package/vendors/claude/agents/wtfp/plan-checker.md +249 -0
  47. package/vendors/claude/agents/wtfp/prose-polisher.md +166 -0
  48. package/vendors/claude/agents/wtfp/research-synthesizer.md +215 -0
  49. package/vendors/claude/agents/wtfp/section-planner.md +364 -0
  50. package/vendors/claude/agents/wtfp/section-reviewer.md +182 -0
  51. package/vendors/claude/agents/wtfp/section-writer.md +312 -0
  52. package/vendors/claude/commands/wtfp/add-todo.md +108 -0
  53. package/vendors/claude/commands/wtfp/analyze-bib.md +60 -249
  54. package/vendors/claude/commands/wtfp/audit-milestone.md +286 -0
  55. package/vendors/claude/commands/wtfp/check-refs.md +60 -131
  56. package/vendors/claude/commands/wtfp/check-todos.md +152 -0
  57. package/vendors/claude/commands/wtfp/checkpoint.md +105 -0
  58. package/vendors/claude/commands/wtfp/contribute.md +1 -1
  59. package/vendors/claude/commands/wtfp/create-outline.md +109 -195
  60. package/vendors/claude/commands/wtfp/create-poster.md +1 -1
  61. package/vendors/claude/commands/wtfp/create-slides.md +1 -1
  62. package/vendors/claude/commands/wtfp/discuss-section.md +1 -1
  63. package/vendors/claude/commands/wtfp/execute-outline.md +257 -0
  64. package/vendors/claude/commands/wtfp/export-latex.md +1 -1
  65. package/vendors/claude/commands/wtfp/help.md +174 -280
  66. package/vendors/claude/commands/wtfp/insert-section.md +1 -1
  67. package/vendors/claude/commands/wtfp/list-assumptions.md +1 -1
  68. package/vendors/claude/commands/wtfp/map-project.md +35 -124
  69. package/vendors/claude/commands/wtfp/new-paper.md +88 -362
  70. package/vendors/claude/commands/wtfp/pause-writing.md +1 -1
  71. package/vendors/claude/commands/wtfp/plan-milestone-gaps.md +255 -0
  72. package/vendors/claude/commands/wtfp/plan-revision.md +1 -1
  73. package/vendors/claude/commands/wtfp/plan-section.md +165 -190
  74. package/vendors/claude/commands/wtfp/polish-prose.md +66 -118
  75. package/vendors/claude/commands/wtfp/progress.md +82 -254
  76. package/vendors/claude/commands/wtfp/quick.md +107 -0
  77. package/vendors/claude/commands/wtfp/remove-section.md +1 -1
  78. package/vendors/claude/commands/wtfp/report-bug.md +1 -1
  79. package/vendors/claude/commands/wtfp/request-feature.md +1 -1
  80. package/vendors/claude/commands/wtfp/research-gap.md +138 -151
  81. package/vendors/claude/commands/wtfp/resume-writing.md +1 -1
  82. package/vendors/claude/commands/wtfp/review-section.md +55 -333
  83. package/vendors/claude/commands/wtfp/settings.md +197 -0
  84. package/vendors/claude/commands/wtfp/submit-milestone.md +248 -99
  85. package/vendors/claude/commands/wtfp/update.md +112 -0
  86. package/vendors/claude/commands/wtfp/verify-work.md +254 -0
  87. package/vendors/claude/commands/wtfp/write-section.md +144 -180
  88. package/vendors/claude/mcp/research-server/package.json +13 -0
  89. package/vendors/claude/mcp/research-server/src/index.js +133 -0
  90. package/vendors/gemini/agents/wtfp/argument-verifier.md +184 -0
  91. package/vendors/gemini/agents/wtfp/citation-expert.md +41 -0
  92. package/vendors/gemini/agents/wtfp/citation-formatter.md +38 -0
  93. package/vendors/gemini/agents/wtfp/coherence-checker.md +162 -0
  94. package/vendors/gemini/agents/wtfp/outliner.md +252 -0
  95. package/vendors/gemini/agents/wtfp/plan-checker.md +244 -0
  96. package/vendors/gemini/agents/wtfp/prose-polisher.md +158 -0
  97. package/vendors/gemini/agents/wtfp/research-synthesizer.md +205 -0
  98. package/vendors/gemini/agents/wtfp/section-planner.md +355 -0
  99. package/vendors/gemini/agents/wtfp/section-reviewer.md +175 -0
  100. package/vendors/gemini/agents/wtfp/section-writer.md +304 -0
  101. package/vendors/gemini/commands/wtfp/add-todo.toml +102 -0
  102. package/vendors/gemini/commands/wtfp/analyze-bib.toml +126 -0
  103. package/vendors/gemini/commands/wtfp/audit-milestone.toml +278 -0
  104. package/vendors/gemini/commands/wtfp/check-refs.toml +123 -0
  105. package/vendors/gemini/commands/wtfp/check-todos.toml +145 -0
  106. package/vendors/gemini/commands/wtfp/checkpoint.toml +96 -0
  107. package/vendors/gemini/commands/wtfp/contribute.toml +269 -0
  108. package/vendors/gemini/commands/wtfp/create-outline.toml +180 -0
  109. package/vendors/gemini/commands/wtfp/create-poster.toml +36 -0
  110. package/vendors/gemini/commands/wtfp/create-slides.toml +35 -0
  111. package/vendors/gemini/commands/wtfp/discuss-section.toml +62 -0
  112. package/vendors/gemini/commands/wtfp/execute-outline.toml +247 -0
  113. package/vendors/gemini/commands/wtfp/export-latex.toml +209 -0
  114. package/vendors/gemini/commands/wtfp/help.toml +184 -0
  115. package/vendors/gemini/commands/wtfp/insert-section.toml +123 -0
  116. package/vendors/gemini/commands/wtfp/list-assumptions.toml +104 -0
  117. package/vendors/gemini/commands/wtfp/map-project.toml +99 -0
  118. package/vendors/gemini/commands/wtfp/new-paper.toml +160 -0
  119. package/vendors/gemini/commands/wtfp/pause-writing.toml +140 -0
  120. package/vendors/gemini/commands/wtfp/plan-milestone-gaps.toml +249 -0
  121. package/vendors/gemini/commands/wtfp/plan-revision.toml +171 -0
  122. package/vendors/gemini/commands/wtfp/plan-section.toml +212 -0
  123. package/vendors/gemini/commands/wtfp/polish-prose.toml +114 -0
  124. package/vendors/gemini/commands/wtfp/progress.toml +145 -0
  125. package/vendors/gemini/commands/wtfp/quick.toml +96 -0
  126. package/vendors/gemini/commands/wtfp/remove-section.toml +140 -0
  127. package/vendors/gemini/commands/wtfp/report-bug.toml +126 -0
  128. package/vendors/gemini/commands/wtfp/request-feature.toml +155 -0
  129. package/vendors/gemini/commands/wtfp/research-gap.toml +214 -0
  130. package/vendors/gemini/commands/wtfp/resume-writing.toml +136 -0
  131. package/vendors/gemini/commands/wtfp/review-section.toml +121 -0
  132. package/vendors/gemini/commands/wtfp/settings.toml +191 -0
  133. package/vendors/gemini/commands/wtfp/submit-milestone.toml +351 -0
  134. package/vendors/gemini/commands/wtfp/update.toml +107 -0
  135. package/vendors/gemini/commands/wtfp/verify-work.toml +245 -0
  136. package/vendors/gemini/commands/wtfp/write-section.toml +228 -0
  137. package/vendors/opencode/agents/wtfp/argument-verifier.md +184 -0
  138. package/vendors/opencode/agents/wtfp/citation-expert.md +41 -0
  139. package/vendors/opencode/agents/wtfp/citation-formatter.md +38 -0
  140. package/vendors/opencode/agents/wtfp/coherence-checker.md +162 -0
  141. package/vendors/opencode/agents/wtfp/outliner.md +252 -0
  142. package/vendors/opencode/agents/wtfp/plan-checker.md +244 -0
  143. package/vendors/opencode/agents/wtfp/prose-polisher.md +158 -0
  144. package/vendors/opencode/agents/wtfp/research-synthesizer.md +205 -0
  145. package/vendors/opencode/agents/wtfp/section-planner.md +355 -0
  146. package/vendors/opencode/agents/wtfp/section-reviewer.md +175 -0
  147. package/vendors/opencode/agents/wtfp/section-writer.md +304 -0
  148. package/vendors/opencode/commands/wtfp/add-todo.md +103 -0
  149. package/vendors/opencode/commands/wtfp/analyze-bib.md +127 -0
  150. package/vendors/opencode/commands/wtfp/audit-milestone.md +279 -0
  151. package/vendors/opencode/commands/wtfp/check-refs.md +124 -0
  152. package/vendors/opencode/commands/wtfp/check-todos.md +146 -0
  153. package/vendors/opencode/commands/wtfp/checkpoint.md +97 -0
  154. package/vendors/opencode/commands/wtfp/contribute.md +266 -0
  155. package/vendors/opencode/commands/wtfp/create-outline.md +181 -0
  156. package/vendors/opencode/commands/wtfp/create-poster.md +37 -0
  157. package/vendors/opencode/commands/wtfp/create-slides.md +36 -0
  158. package/vendors/opencode/commands/wtfp/discuss-section.md +63 -0
  159. package/vendors/opencode/commands/wtfp/execute-outline.md +248 -0
  160. package/vendors/opencode/commands/wtfp/export-latex.md +210 -0
  161. package/vendors/opencode/commands/wtfp/help.md +185 -0
  162. package/vendors/opencode/commands/wtfp/insert-section.md +124 -0
  163. package/vendors/opencode/commands/wtfp/list-assumptions.md +105 -0
  164. package/vendors/opencode/commands/wtfp/map-project.md +100 -0
  165. package/vendors/opencode/commands/wtfp/new-paper.md +161 -0
  166. package/vendors/opencode/commands/wtfp/pause-writing.md +141 -0
  167. package/vendors/opencode/commands/wtfp/plan-milestone-gaps.md +250 -0
  168. package/vendors/opencode/commands/wtfp/plan-revision.md +174 -0
  169. package/vendors/opencode/commands/wtfp/plan-section.md +214 -0
  170. package/vendors/opencode/commands/wtfp/polish-prose.md +115 -0
  171. package/vendors/opencode/commands/wtfp/progress.md +146 -0
  172. package/vendors/opencode/commands/wtfp/quick.md +97 -0
  173. package/vendors/opencode/commands/wtfp/remove-section.md +141 -0
  174. package/vendors/opencode/commands/wtfp/report-bug.md +127 -0
  175. package/vendors/opencode/commands/wtfp/request-feature.md +156 -0
  176. package/vendors/opencode/commands/wtfp/research-gap.md +215 -0
  177. package/vendors/opencode/commands/wtfp/resume-writing.md +137 -0
  178. package/vendors/opencode/commands/wtfp/review-section.md +123 -0
  179. package/vendors/opencode/commands/wtfp/settings.md +192 -0
  180. package/vendors/opencode/commands/wtfp/submit-milestone.md +352 -0
  181. package/vendors/opencode/commands/wtfp/update.md +108 -0
  182. package/vendors/opencode/commands/wtfp/verify-work.md +246 -0
  183. package/vendors/opencode/commands/wtfp/write-section.md +228 -0
@@ -0,0 +1,299 @@
1
+ const https = require('https');
2
+ const querystring = require('querystring');
3
+ const s2 = require('./semantic-scholar');
4
+ const scholar = require('./scholar-lookup');
5
+ const ranker = require('./citation-ranker');
6
+ const bibFormat = require('./bib-format');
7
+
8
+ /**
9
+ * WTF-P Citation Fetcher v0.4.0
10
+ *
11
+ * Orchestrates tiered search:
12
+ * 1. Semantic Scholar (Primary, Free)
13
+ * 2. SerpAPI (Optional, Seminal/Paid)
14
+ * 3. CrossRef (Fallback)
15
+ *
16
+ * Usage:
17
+ * node citation-fetcher.js "<query>" --intent=<intent> --year=<year>
18
+ */
19
+
20
+ // --- CrossRef Fallback ---
21
+ async function searchCrossRef(query, limit = 5) {
22
+ return new Promise((resolve, reject) => {
23
+ const params = {
24
+ query: query,
25
+ rows: limit,
26
+ sort: 'relevance',
27
+ select: 'DOI,title,author,issued,type,container-title,volume,issue,page,abstract'
28
+ };
29
+
30
+ const options = {
31
+ hostname: 'api.crossref.org',
32
+ path: `/works?${querystring.stringify(params)}`,
33
+ method: 'GET',
34
+ headers: {
35
+ 'User-Agent': 'WTF-P/0.4.0 (citation-expert)'
36
+ }
37
+ };
38
+
39
+ const req = https.request(options, (res) => {
40
+ let data = '';
41
+ res.on('data', c => data += c);
42
+ res.on('end', () => {
43
+ if (res.statusCode !== 200) return resolve([]);
44
+ try {
45
+ const json = JSON.parse(data);
46
+ const items = json.message.items || [];
47
+ resolve(items.map(mapCrossRefToPaper));
48
+ } catch (e) {
49
+ resolve([]);
50
+ }
51
+ });
52
+ });
53
+
54
+ req.on('error', () => resolve([]));
55
+ req.end();
56
+ });
57
+ }
58
+
59
+ function mapCrossRefToPaper(item) {
60
+ const title = item.title ? item.title[0] : 'Untitled';
61
+ const year = item.issued && item.issued['date-parts'] ? item.issued['date-parts'][0][0] : null;
62
+
63
+ return {
64
+ source: 'crossref',
65
+ title: title,
66
+ year: year,
67
+ doi: item.DOI,
68
+ venue: item['container-title'] ? item['container-title'][0] : null,
69
+ authors: (item.author || []).map(a => ({ name: `${a.family}, ${a.given}` })),
70
+ abstract: item.abstract ? item.abstract.replace(/<[^>]*>?/gm, '').trim() : null,
71
+ citationCount: 0, // CrossRef doesn't give citation counts easily
72
+ externalIds: { DOI: item.DOI }
73
+ };
74
+ }
75
+
76
+ // --- Mapper ---
77
+
78
+ function mapS2ToPaper(item) {
79
+ return {
80
+ source: 'semantic_scholar',
81
+ paperId: item.paperId,
82
+ title: item.title,
83
+ year: item.year,
84
+ venue: item.venue,
85
+ authors: item.authors || [],
86
+ citationCount: item.citationCount || 0,
87
+ abstract: item.abstract,
88
+ externalIds: item.externalIds || {},
89
+ doi: (item.externalIds && item.externalIds.DOI) || null,
90
+ openAccessPdf: item.openAccessPdf
91
+ };
92
+ }
93
+
94
+ function mapScholarToPaper(item) {
95
+ return {
96
+ source: 'google_scholar',
97
+ scholarClusterId: item.clusterId,
98
+ title: item.title,
99
+ year: item.year,
100
+ venue: item.venue,
101
+ authors: item.authors || [],
102
+ citationCount: item.citationCount || 0,
103
+ abstract: item.snippet, // Use snippet as abstract fallback
104
+ externalIds: {},
105
+ doi: null, // Scholar doesn't reliably give DOIs
106
+ openAccessPdf: item.link ? { url: item.link } : null
107
+ };
108
+ }
109
+
110
+ // --- Deduplication ---
111
+
112
+ function fingerprint(title, year) {
113
+ if (!title) return `unknown::${Math.random()}`;
114
+ const normalized = title.toLowerCase().replace(/[^\w\s]/g, '').replace(/\s+/g, ' ').trim();
115
+ return `${normalized}::${year || '????'}`;
116
+ }
117
+
118
+ function deduplicatePapers(papers) {
119
+ const seen = new Map();
120
+
121
+ for (const paper of papers) {
122
+ // Priority: DOI > S2ID > ScholarID > Fingerprint
123
+ const key = paper.doi
124
+ || paper.paperId
125
+ || paper.scholarClusterId
126
+ || fingerprint(paper.title, paper.year);
127
+
128
+ if (!seen.has(key)) {
129
+ seen.set(key, paper);
130
+ } else {
131
+ // Merge: prefer richer metadata
132
+ // S2 usually has best metadata (abstracts, verified authors).
133
+ // Scholar has best citation counts.
134
+ // CrossRef has verified DOIs.
135
+
136
+ const existing = seen.get(key);
137
+ let merged = { ...existing };
138
+
139
+ // If new one is S2, take its metadata but keep higher citation count
140
+ if (paper.source === 'semantic_scholar') {
141
+ merged = { ...paper, citationCount: Math.max(existing.citationCount, paper.citationCount) };
142
+ if (existing.scholarClusterId) merged.scholarClusterId = existing.scholarClusterId;
143
+ }
144
+ // If new one is Scholar, just update citation count and maybe ID
145
+ else if (paper.source === 'google_scholar') {
146
+ merged.citationCount = Math.max(merged.citationCount, paper.citationCount);
147
+ merged.scholarClusterId = paper.clusterId;
148
+ }
149
+
150
+ seen.set(key, merged);
151
+ }
152
+ }
153
+
154
+ return Array.from(seen.values());
155
+ }
156
+
157
+ // --- Main Search Logic ---
158
+
159
+ async function search(query, options = {}) {
160
+ const limit = options.limit || 10;
161
+ const intent = options.intent || 'balanced';
162
+
163
+ let papers = [];
164
+ const errors = [];
165
+
166
+ const searchPromises = [];
167
+
168
+ // 1. S2 Search (Always)
169
+ searchPromises.push(
170
+ s2.search(query, { limit: limit * 2, year: options.year })
171
+ .then(res => res.map(mapS2ToPaper))
172
+ .catch(e => {
173
+ errors.push(`S2 Error: ${e.message}`);
174
+ return [];
175
+ })
176
+ );
177
+
178
+ // 2. Scholar Search (Conditional)
179
+ if ((intent === 'seminal' || options.useScholar) && scholar.isAvailable()) {
180
+ searchPromises.push(
181
+ scholar.search(query, { limit: limit, yearLow: options.year, yearHigh: options.year })
182
+ .then(res => res.map(mapScholarToPaper))
183
+ .catch(e => {
184
+ errors.push(`Scholar Error: ${e.message}`);
185
+ return [];
186
+ })
187
+ );
188
+ }
189
+
190
+ const results = await Promise.all(searchPromises);
191
+ results.forEach(r => papers.push(...r));
192
+
193
+ // 3. CrossRef Fallback (if few results)
194
+ if (papers.length < 5) {
195
+ try {
196
+ const crResults = await searchCrossRef(query, limit);
197
+ papers.push(...crResults);
198
+ } catch (e) {
199
+ errors.push(`CrossRef Error: ${e.message}`);
200
+ }
201
+ }
202
+
203
+ // 4. Deduplicate
204
+ const unique = deduplicatePapers(papers);
205
+
206
+ // 5. Rank
207
+ const ranked = ranker.rank(unique, intent);
208
+
209
+ // 6. Format to BibTeX
210
+ const formatted = ranked.slice(0, limit).map(p => {
211
+ // Generate key
212
+ const firstAuthor = p.authors && p.authors.length > 0
213
+ ? (p.authors[0].name ? p.authors[0].name.split(',')[0].trim().split(' ').pop().toLowerCase() : 'unknown')
214
+ : 'unknown';
215
+
216
+ // Handle S2 author format variants or string parsing
217
+ let familyName = 'unknown';
218
+ if (p.authors && p.authors.length > 0) {
219
+ const nameParts = p.authors[0].name.split(' ');
220
+ familyName = nameParts[nameParts.length - 1].toLowerCase().replace(/[^a-z]/g, '');
221
+ }
222
+
223
+ const shortTitle = p.title.split(/\s+/)[0].toLowerCase().replace(/[^a-z0-9]/g, '');
224
+ const key = `${familyName}${p.year || '????'}${shortTitle}`;
225
+
226
+ const bibData = {
227
+ key: key,
228
+ entryType: 'article',
229
+ author: p.authors.map(a => a.name).join(' and '),
230
+ title: p.title,
231
+ year: p.year ? p.year.toString() : null,
232
+ venue: p.venue,
233
+ booktitle: p.venue,
234
+ abstract: p.abstract,
235
+ doi: p.doi,
236
+ url: p.openAccessPdf ? p.openAccessPdf.url : (p.doi ? `https://doi.org/${p.doi}` : null),
237
+ google_scholar_id: p.scholarClusterId
238
+ };
239
+
240
+ const provenance = {
241
+ wtfp_status: p.doi ? 'official' : 'partial',
242
+ wtfp_source: p.source,
243
+ wtfp_citations: p.citationCount,
244
+ wtfp_velocity: p.wtfp_velocity,
245
+ wtfp_s2_id: p.paperId,
246
+ wtfp_scholar_id: p.scholarClusterId
247
+ };
248
+
249
+ return {
250
+ ...p,
251
+ bibtex: bibFormat.format(bibData, provenance)
252
+ };
253
+ });
254
+
255
+ return {
256
+ results: formatted,
257
+ metadata: {
258
+ query,
259
+ total: unique.length,
260
+ returned: formatted.length,
261
+ errors
262
+ }
263
+ };
264
+ }
265
+
266
+
267
+ // --- CLI Handling ---
268
+
269
+ if (require.main === module) {
270
+ const args = process.argv.slice(2);
271
+ const query = args.find(a => !a.startsWith('--'));
272
+
273
+ if (!query) {
274
+ console.error('Usage: node citation-fetcher.js "<query>" [--intent=seminal] [--year=2023]');
275
+ process.exit(1);
276
+ }
277
+
278
+ const intentArg = args.find(a => a.startsWith('--intent='));
279
+ const yearArg = args.find(a => a.startsWith('--year='));
280
+ const limitArg = args.find(a => a.startsWith('--limit='));
281
+
282
+ const options = {
283
+ intent: intentArg ? intentArg.split('=')[1] : 'balanced',
284
+ year: yearArg ? yearArg.split('=')[1] : null,
285
+ limit: limitArg ? parseInt(limitArg.split('=')[1]) : 10
286
+ };
287
+
288
+ search(query, options).then(result => {
289
+ // console.log(JSON.stringify(result, null, 2));
290
+ // For now, output just the bibtex entries as text for easy reading, or JSON?
291
+ // The previous fetcher output JSON. Let's stick to JSON array of results.
292
+ console.log(JSON.stringify(result.results, null, 2));
293
+ }).catch(e => {
294
+ console.error(e);
295
+ process.exit(1);
296
+ });
297
+ }
298
+
299
+ module.exports = { search };
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Citation Ranker
3
+ *
4
+ * Ranks papers based on multidimensional impact score:
5
+ * - Citation Count (Log scaled)
6
+ * - Velocity (Citations per month)
7
+ * - Recency (Decay over time)
8
+ * - Venue (Tiered scoring)
9
+ */
10
+
11
+ // Venue Tiers
12
+ const VENUE_TIERS = {
13
+ TIER_1: [
14
+ // AI/ML
15
+ "neurips", "nips", "icml", "iclr", "cvpr", "iccv", "eccv", "acl", "emnlp", "naacl",
16
+ "aaai", "ijcai", "kdd", "www", "sigir", "chi",
17
+ // Systems
18
+ "osdi", "sosp", "nsdi", "eurosys", "atc", "fast", "sigcomm", "mobicom",
19
+ // Journals
20
+ "nature", "science", "pnas", "cell", "lancet", "nejm", "jama",
21
+ "ieee transactions", "acm computing surveys", "jmlr", "journal of machine learning research"
22
+ ],
23
+ TIER_2: [
24
+ "coling", "wsdm", "cikm", "pakdd", "ijcnn", "icra", "iros",
25
+ "middleware", "cloud", "sc", "hpdc", "cluster"
26
+ ],
27
+ PREPRINT: [
28
+ "arxiv", "biorxiv", "medrxiv", "ssrn", "workshop"
29
+ ]
30
+ };
31
+
32
+ /**
33
+ * Score a venue name
34
+ * @param {string} venue
35
+ * @returns {number} 0.0 to 1.0
36
+ */
37
+ function scoreVenue(venue) {
38
+ if (!venue) return 0.2;
39
+ const v = venue.toLowerCase();
40
+
41
+ if (VENUE_TIERS.TIER_1.some(t => v.includes(t))) return 1.0;
42
+ if (VENUE_TIERS.TIER_2.some(t => v.includes(t))) return 0.7;
43
+ if (VENUE_TIERS.PREPRINT.some(t => v.includes(t))) return 0.3;
44
+
45
+ return 0.5; // Unknown venue default
46
+ }
47
+
48
+ /**
49
+ * Calculate citation velocity
50
+ * @param {number} citations
51
+ * @param {number} year
52
+ * @returns {number} Citations per month
53
+ */
54
+ function calculateVelocity(citations, year) {
55
+ if (!citations || !year) return 0;
56
+ const now = new Date();
57
+ const currentYear = now.getFullYear();
58
+ const currentMonth = now.getMonth(); // 0-11
59
+
60
+ // Calculate months since publication (assuming Jan 1st of year)
61
+ // If current year, use months passed. If past year, (diff * 12) + currentMonth.
62
+ let monthsSince = (currentYear - year) * 12 + currentMonth;
63
+
64
+ // Guard against divide by zero or negative (future dates)
65
+ monthsSince = Math.max(1, monthsSince);
66
+
67
+ return citations / monthsSince;
68
+ }
69
+
70
+ /**
71
+ * Calculate impact score for a paper
72
+ * @param {Object} paper
73
+ * @param {string} intent "seminal" | "recent" | "specific" | "balanced"
74
+ */
75
+ function calculateScore(paper, intent = "balanced") {
76
+ const now = new Date().getFullYear();
77
+ const year = paper.year || now;
78
+ const age = Math.max(0, now - year);
79
+ const citations = paper.citationCount || 0;
80
+ const velocity = calculateVelocity(citations, year);
81
+
82
+ // Normalize factors (approximate ranges)
83
+ // Citations: log10(100,000) = 5. So score is 0-1.
84
+ const citationScore = Math.min(1, Math.log10(citations + 1) / 5);
85
+
86
+ // Velocity: 100 cites/month is huge. Normalize to 0-1.
87
+ const velocityScore = Math.min(1, velocity / 100);
88
+
89
+ // Recency: Linear decay over 10 years. 0 if > 10 years old.
90
+ const recencyScore = Math.max(0, 1 - (age / 10));
91
+
92
+ const venueScore = scoreVenue(paper.venue);
93
+
94
+ // Weights based on intent
95
+ const weights = {
96
+ seminal: { citation: 0.6, velocity: 0.2, recency: 0.1, venue: 0.1 },
97
+ recent: { citation: 0.2, velocity: 0.3, recency: 0.4, venue: 0.1 },
98
+ specific: { citation: 0.1, velocity: 0.1, recency: 0.1, venue: 0.1 }, // Ranking less important for specific lookup
99
+ balanced: { citation: 0.4, velocity: 0.3, recency: 0.2, venue: 0.1 }
100
+ };
101
+
102
+ const w = weights[intent] || weights.balanced;
103
+
104
+ return (
105
+ citationScore * w.citation +
106
+ velocityScore * w.velocity +
107
+ recencyScore * w.recency +
108
+ venueScore * w.venue
109
+ );
110
+ }
111
+
112
+ /**
113
+ * Rank a list of papers
114
+ * @param {Array} papers
115
+ * @param {string} intent
116
+ * @returns {Array} Sorted papers with score attached
117
+ */
118
+ function rank(papers, intent = "balanced") {
119
+ const scored = papers.map(p => ({
120
+ ...p,
121
+ wtfp_score: calculateScore(p, intent),
122
+ wtfp_velocity: calculateVelocity(p.citationCount, p.year)
123
+ }));
124
+
125
+ return scored.sort((a, b) => b.wtfp_score - a.wtfp_score);
126
+ }
127
+
128
+ module.exports = {
129
+ rank,
130
+ calculateScore,
131
+ calculateVelocity,
132
+ scoreVenue
133
+ };
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Context Primer - Section-specific context extraction
3
+ *
4
+ * Reduces context burn for long papers by loading only relevant
5
+ * portions of PROJECT.md, argument-map.md, etc. for each task.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ /**
12
+ * Section type → relevant context files mapping
13
+ */
14
+ const SECTION_CONTEXT_MAP = {
15
+ abstract: ['argument-map.md', 'outline.md', 'narrative-arc.md'],
16
+ introduction: ['argument-map.md', 'narrative-arc.md'],
17
+ 'literature-review': ['argument-map.md'],
18
+ 'related-work': ['argument-map.md'],
19
+ methods: ['argument-map.md'],
20
+ results: ['argument-map.md'],
21
+ discussion: ['argument-map.md', 'narrative-arc.md'],
22
+ conclusion: ['argument-map.md', 'narrative-arc.md'],
23
+ // Grant-specific
24
+ 'specific-aims': ['argument-map.md'],
25
+ background: ['argument-map.md', 'narrative-arc.md'],
26
+ approach: ['argument-map.md'],
27
+ };
28
+
29
+ /**
30
+ * Extract relevant sections from PROJECT.md based on task type
31
+ * @param {string} projectContent - Full PROJECT.md content
32
+ * @param {string} taskType - Type of task (plan, write, review, research)
33
+ * @returns {string} - Filtered PROJECT.md content
34
+ */
35
+ function filterProject(projectContent, taskType) {
36
+ if (!projectContent) return '';
37
+
38
+ // Always include: What This Is, Core Argument
39
+ const alwaysSections = ['What This Is', 'Core Argument'];
40
+
41
+ // Task-specific sections
42
+ const taskSections = {
43
+ plan: ['Requirements', 'Target Audience', 'Constraints'],
44
+ write: ['Core Argument', 'Target Audience'],
45
+ review: ['Requirements', 'Core Argument', 'Constraints'],
46
+ research: ['Core Argument', 'Target Audience', 'Key Decisions'],
47
+ polish: ['Target Audience'],
48
+ };
49
+
50
+ const needed = [...alwaysSections, ...(taskSections[taskType] || alwaysSections)];
51
+ return extractSections(projectContent, needed);
52
+ }
53
+
54
+ /**
55
+ * Extract relevant structure files for a section type
56
+ * @param {string} planningDir - Path to .planning directory
57
+ * @param {string} sectionSlug - Section slug (e.g., 'introduction', 'methods')
58
+ * @returns {Object} - Map of filename → content
59
+ */
60
+ function getStructureContext(planningDir, sectionSlug) {
61
+ const structureDir = path.join(planningDir, 'structure');
62
+ const result = {};
63
+
64
+ // Normalize section slug for lookup
65
+ const normalized = sectionSlug.toLowerCase().replace(/\d+-/, '').replace(/\s+/g, '-');
66
+
67
+ // Find matching context files
68
+ const relevantFiles = SECTION_CONTEXT_MAP[normalized] || ['argument-map.md'];
69
+
70
+ for (const file of relevantFiles) {
71
+ const filePath = path.join(structureDir, file);
72
+ if (fs.existsSync(filePath)) {
73
+ result[file] = fs.readFileSync(filePath, 'utf8');
74
+ }
75
+ }
76
+
77
+ return result;
78
+ }
79
+
80
+ /**
81
+ * Load section-specific context (CONTEXT.md + RESEARCH.md)
82
+ * @param {string} sectionDir - Path to section directory
83
+ * @returns {Object} - { context, research }
84
+ */
85
+ function getSectionContext(sectionDir) {
86
+ const result = { context: '', research: '' };
87
+
88
+ if (!fs.existsSync(sectionDir)) return result;
89
+
90
+ const files = fs.readdirSync(sectionDir);
91
+ for (const f of files) {
92
+ if (f.endsWith('-CONTEXT.md')) {
93
+ result.context = fs.readFileSync(path.join(sectionDir, f), 'utf8');
94
+ }
95
+ if (f.endsWith('-RESEARCH.md')) {
96
+ result.research = fs.readFileSync(path.join(sectionDir, f), 'utf8');
97
+ }
98
+ }
99
+
100
+ return result;
101
+ }
102
+
103
+ /**
104
+ * Get prior section summaries relevant to current section
105
+ * @param {string} sectionsDir - Path to .planning/sections/
106
+ * @param {number} currentSection - Current section number
107
+ * @param {number} limit - Max characters to return
108
+ * @returns {string} - Concatenated relevant summaries
109
+ */
110
+ function getPriorSummaries(sectionsDir, currentSection, limit = 3000) {
111
+ if (!fs.existsSync(sectionsDir)) return '';
112
+
113
+ const dirs = fs.readdirSync(sectionsDir)
114
+ .filter(d => {
115
+ const num = parseInt(d.split('-')[0], 10);
116
+ return !isNaN(num) && num < currentSection;
117
+ })
118
+ .sort();
119
+
120
+ let output = '';
121
+ // Work backwards from most recent, within limit
122
+ for (const dir of dirs.reverse()) {
123
+ const dirPath = path.join(sectionsDir, dir);
124
+ const summaries = fs.readdirSync(dirPath).filter(f => f.endsWith('-SUMMARY.md'));
125
+
126
+ for (const s of summaries) {
127
+ const content = fs.readFileSync(path.join(dirPath, s), 'utf8');
128
+ // Extract just the header section (up to first ---)
129
+ const header = content.split('---')[0] || content.slice(0, 500);
130
+ if (output.length + header.length > limit) return output;
131
+ output = header + '\n---\n' + output;
132
+ }
133
+ }
134
+
135
+ return output;
136
+ }
137
+
138
+ /**
139
+ * Build complete primed context for an agent spawn
140
+ * @param {Object} options
141
+ * @param {string} options.planningDir - Path to .planning/
142
+ * @param {string} options.sectionDir - Path to section directory
143
+ * @param {number} options.sectionNumber - Section number
144
+ * @param {string} options.sectionSlug - Section slug
145
+ * @param {string} options.taskType - plan|write|review|research|polish
146
+ * @returns {Object} - All context pieces, filtered for relevance
147
+ */
148
+ function primeContext({ planningDir, sectionDir, sectionNumber, sectionSlug, taskType }) {
149
+ const projectPath = path.join(planningDir, 'PROJECT.md');
150
+ const statePath = path.join(planningDir, 'STATE.md');
151
+ const roadmapPath = path.join(planningDir, 'ROADMAP.md');
152
+ const sectionsDir = path.join(planningDir, 'sections');
153
+
154
+ const projectContent = fs.existsSync(projectPath)
155
+ ? fs.readFileSync(projectPath, 'utf8') : '';
156
+
157
+ return {
158
+ project: filterProject(projectContent, taskType),
159
+ state: fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf8') : '',
160
+ roadmap: fs.existsSync(roadmapPath) ? fs.readFileSync(roadmapPath, 'utf8') : '',
161
+ structure: getStructureContext(planningDir, sectionSlug),
162
+ section: getSectionContext(sectionDir),
163
+ priorSummaries: getPriorSummaries(sectionsDir, sectionNumber),
164
+ };
165
+ }
166
+
167
+ // --- Helpers ---
168
+
169
+ /**
170
+ * Extract named sections from markdown content
171
+ * @param {string} content - Markdown content
172
+ * @param {string[]} sectionNames - Section heading names to extract
173
+ * @returns {string} - Extracted sections joined
174
+ */
175
+ function extractSections(content, sectionNames) {
176
+ if (!content) return '';
177
+
178
+ const lines = content.split('\n');
179
+ const extracted = [];
180
+ let capturing = false;
181
+ let currentLevel = 0;
182
+
183
+ for (const line of lines) {
184
+ const headingMatch = line.match(/^(#{1,3})\s+(.+)/);
185
+
186
+ if (headingMatch) {
187
+ const level = headingMatch[1].length;
188
+ const name = headingMatch[2].trim();
189
+
190
+ if (sectionNames.some(s => name.includes(s))) {
191
+ capturing = true;
192
+ currentLevel = level;
193
+ extracted.push(line);
194
+ } else if (capturing && level <= currentLevel) {
195
+ capturing = false;
196
+ } else if (capturing) {
197
+ extracted.push(line);
198
+ }
199
+ } else if (capturing) {
200
+ extracted.push(line);
201
+ }
202
+ }
203
+
204
+ return extracted.join('\n');
205
+ }
206
+
207
+ module.exports = {
208
+ filterProject,
209
+ getStructureContext,
210
+ getSectionContext,
211
+ getPriorSummaries,
212
+ primeContext,
213
+ SECTION_CONTEXT_MAP,
214
+ };