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.
- package/README.md +146 -208
- package/bin/commands/doctor.js +88 -2
- package/bin/commands/install-logic.js +127 -36
- package/bin/commands/status.js +22 -4
- package/bin/commands/update.js +5 -4
- package/bin/install.js +66 -26
- package/bin/lib/analyze-impact.js +105 -0
- package/bin/lib/bib-format.js +161 -0
- package/bin/lib/bib-index.js +104 -0
- package/bin/lib/checkpoint.js +183 -0
- package/bin/lib/citation-fetcher.js +299 -0
- package/bin/lib/citation-ranker.js +133 -0
- package/bin/lib/context-primer.js +214 -0
- package/bin/lib/manifest.js +80 -2
- package/bin/lib/scholar-lookup.js +188 -0
- package/bin/lib/semantic-scholar.js +184 -0
- package/bin/lib/utils.js +22 -1
- package/bin/uninstall.js +270 -111
- package/core/write-the-f-paper/references/agent-model-matrix.md +62 -0
- package/core/write-the-f-paper/references/checkpoints.md +160 -9
- package/core/write-the-f-paper/references/context-fidelity.md +153 -0
- package/core/write-the-f-paper/references/deviation-rules.md +150 -0
- package/core/write-the-f-paper/references/git-integration.md +80 -0
- package/core/write-the-f-paper/references/orchestrator-pattern.md +167 -0
- package/core/write-the-f-paper/references/planning-config.md +135 -0
- package/core/write-the-f-paper/references/ui-brand.md +186 -0
- package/core/write-the-f-paper/templates/UAT.md +80 -0
- package/core/write-the-f-paper/templates/base-prefs.yaml +42 -0
- package/core/write-the-f-paper/templates/config.json +31 -5
- package/core/write-the-f-paper/workflows/create-outline.wcn.md +1 -1
- package/core/write-the-f-paper/workflows/execute-outline.md +163 -0
- package/core/write-the-f-paper/workflows/execute-outline.wcn.md +73 -0
- package/core/write-the-f-paper/workflows/execute-section.wcn.md +141 -169
- package/core/write-the-f-paper/workflows/plan-section.wcn.md +96 -103
- package/core/write-the-f-paper/workflows/research-gap.md +27 -3
- package/core/write-the-f-paper/workflows/research-gap.wcn.md +18 -1
- package/core/write-the-f-paper/workflows/verify-work.md +251 -0
- package/core/write-the-f-paper/workflows/verify-work.wcn.md +74 -0
- package/package.json +19 -7
- package/vendors/claude/.claude-plugin/plugin.json +2 -2
- package/vendors/claude/agents/wtfp/argument-verifier.md +189 -0
- package/vendors/claude/agents/wtfp/citation-expert.md +45 -0
- package/vendors/claude/agents/wtfp/citation-formatter.md +42 -0
- package/vendors/claude/agents/wtfp/coherence-checker.md +167 -0
- package/vendors/claude/agents/wtfp/outliner.md +259 -0
- package/vendors/claude/agents/wtfp/plan-checker.md +249 -0
- package/vendors/claude/agents/wtfp/prose-polisher.md +166 -0
- package/vendors/claude/agents/wtfp/research-synthesizer.md +215 -0
- package/vendors/claude/agents/wtfp/section-planner.md +364 -0
- package/vendors/claude/agents/wtfp/section-reviewer.md +182 -0
- package/vendors/claude/agents/wtfp/section-writer.md +312 -0
- package/vendors/claude/commands/wtfp/add-todo.md +108 -0
- package/vendors/claude/commands/wtfp/analyze-bib.md +60 -249
- package/vendors/claude/commands/wtfp/audit-milestone.md +286 -0
- package/vendors/claude/commands/wtfp/check-refs.md +60 -131
- package/vendors/claude/commands/wtfp/check-todos.md +152 -0
- package/vendors/claude/commands/wtfp/checkpoint.md +105 -0
- package/vendors/claude/commands/wtfp/contribute.md +1 -1
- package/vendors/claude/commands/wtfp/create-outline.md +109 -195
- package/vendors/claude/commands/wtfp/create-poster.md +1 -1
- package/vendors/claude/commands/wtfp/create-slides.md +1 -1
- package/vendors/claude/commands/wtfp/discuss-section.md +1 -1
- package/vendors/claude/commands/wtfp/execute-outline.md +257 -0
- package/vendors/claude/commands/wtfp/export-latex.md +1 -1
- package/vendors/claude/commands/wtfp/help.md +174 -280
- package/vendors/claude/commands/wtfp/insert-section.md +1 -1
- package/vendors/claude/commands/wtfp/list-assumptions.md +1 -1
- package/vendors/claude/commands/wtfp/map-project.md +35 -124
- package/vendors/claude/commands/wtfp/new-paper.md +88 -362
- package/vendors/claude/commands/wtfp/pause-writing.md +1 -1
- package/vendors/claude/commands/wtfp/plan-milestone-gaps.md +255 -0
- package/vendors/claude/commands/wtfp/plan-revision.md +1 -1
- package/vendors/claude/commands/wtfp/plan-section.md +165 -190
- package/vendors/claude/commands/wtfp/polish-prose.md +66 -118
- package/vendors/claude/commands/wtfp/progress.md +82 -254
- package/vendors/claude/commands/wtfp/quick.md +107 -0
- package/vendors/claude/commands/wtfp/remove-section.md +1 -1
- package/vendors/claude/commands/wtfp/report-bug.md +1 -1
- package/vendors/claude/commands/wtfp/request-feature.md +1 -1
- package/vendors/claude/commands/wtfp/research-gap.md +138 -151
- package/vendors/claude/commands/wtfp/resume-writing.md +1 -1
- package/vendors/claude/commands/wtfp/review-section.md +55 -333
- package/vendors/claude/commands/wtfp/settings.md +197 -0
- package/vendors/claude/commands/wtfp/submit-milestone.md +248 -99
- package/vendors/claude/commands/wtfp/update.md +112 -0
- package/vendors/claude/commands/wtfp/verify-work.md +254 -0
- package/vendors/claude/commands/wtfp/write-section.md +144 -180
- package/vendors/claude/mcp/research-server/package.json +13 -0
- package/vendors/claude/mcp/research-server/src/index.js +133 -0
- package/vendors/gemini/agents/wtfp/argument-verifier.md +184 -0
- package/vendors/gemini/agents/wtfp/citation-expert.md +41 -0
- package/vendors/gemini/agents/wtfp/citation-formatter.md +38 -0
- package/vendors/gemini/agents/wtfp/coherence-checker.md +162 -0
- package/vendors/gemini/agents/wtfp/outliner.md +252 -0
- package/vendors/gemini/agents/wtfp/plan-checker.md +244 -0
- package/vendors/gemini/agents/wtfp/prose-polisher.md +158 -0
- package/vendors/gemini/agents/wtfp/research-synthesizer.md +205 -0
- package/vendors/gemini/agents/wtfp/section-planner.md +355 -0
- package/vendors/gemini/agents/wtfp/section-reviewer.md +175 -0
- package/vendors/gemini/agents/wtfp/section-writer.md +304 -0
- package/vendors/gemini/commands/wtfp/add-todo.toml +102 -0
- package/vendors/gemini/commands/wtfp/analyze-bib.toml +126 -0
- package/vendors/gemini/commands/wtfp/audit-milestone.toml +278 -0
- package/vendors/gemini/commands/wtfp/check-refs.toml +123 -0
- package/vendors/gemini/commands/wtfp/check-todos.toml +145 -0
- package/vendors/gemini/commands/wtfp/checkpoint.toml +96 -0
- package/vendors/gemini/commands/wtfp/contribute.toml +269 -0
- package/vendors/gemini/commands/wtfp/create-outline.toml +180 -0
- package/vendors/gemini/commands/wtfp/create-poster.toml +36 -0
- package/vendors/gemini/commands/wtfp/create-slides.toml +35 -0
- package/vendors/gemini/commands/wtfp/discuss-section.toml +62 -0
- package/vendors/gemini/commands/wtfp/execute-outline.toml +247 -0
- package/vendors/gemini/commands/wtfp/export-latex.toml +209 -0
- package/vendors/gemini/commands/wtfp/help.toml +184 -0
- package/vendors/gemini/commands/wtfp/insert-section.toml +123 -0
- package/vendors/gemini/commands/wtfp/list-assumptions.toml +104 -0
- package/vendors/gemini/commands/wtfp/map-project.toml +99 -0
- package/vendors/gemini/commands/wtfp/new-paper.toml +160 -0
- package/vendors/gemini/commands/wtfp/pause-writing.toml +140 -0
- package/vendors/gemini/commands/wtfp/plan-milestone-gaps.toml +249 -0
- package/vendors/gemini/commands/wtfp/plan-revision.toml +171 -0
- package/vendors/gemini/commands/wtfp/plan-section.toml +212 -0
- package/vendors/gemini/commands/wtfp/polish-prose.toml +114 -0
- package/vendors/gemini/commands/wtfp/progress.toml +145 -0
- package/vendors/gemini/commands/wtfp/quick.toml +96 -0
- package/vendors/gemini/commands/wtfp/remove-section.toml +140 -0
- package/vendors/gemini/commands/wtfp/report-bug.toml +126 -0
- package/vendors/gemini/commands/wtfp/request-feature.toml +155 -0
- package/vendors/gemini/commands/wtfp/research-gap.toml +214 -0
- package/vendors/gemini/commands/wtfp/resume-writing.toml +136 -0
- package/vendors/gemini/commands/wtfp/review-section.toml +121 -0
- package/vendors/gemini/commands/wtfp/settings.toml +191 -0
- package/vendors/gemini/commands/wtfp/submit-milestone.toml +351 -0
- package/vendors/gemini/commands/wtfp/update.toml +107 -0
- package/vendors/gemini/commands/wtfp/verify-work.toml +245 -0
- package/vendors/gemini/commands/wtfp/write-section.toml +228 -0
- package/vendors/opencode/agents/wtfp/argument-verifier.md +184 -0
- package/vendors/opencode/agents/wtfp/citation-expert.md +41 -0
- package/vendors/opencode/agents/wtfp/citation-formatter.md +38 -0
- package/vendors/opencode/agents/wtfp/coherence-checker.md +162 -0
- package/vendors/opencode/agents/wtfp/outliner.md +252 -0
- package/vendors/opencode/agents/wtfp/plan-checker.md +244 -0
- package/vendors/opencode/agents/wtfp/prose-polisher.md +158 -0
- package/vendors/opencode/agents/wtfp/research-synthesizer.md +205 -0
- package/vendors/opencode/agents/wtfp/section-planner.md +355 -0
- package/vendors/opencode/agents/wtfp/section-reviewer.md +175 -0
- package/vendors/opencode/agents/wtfp/section-writer.md +304 -0
- package/vendors/opencode/commands/wtfp/add-todo.md +103 -0
- package/vendors/opencode/commands/wtfp/analyze-bib.md +127 -0
- package/vendors/opencode/commands/wtfp/audit-milestone.md +279 -0
- package/vendors/opencode/commands/wtfp/check-refs.md +124 -0
- package/vendors/opencode/commands/wtfp/check-todos.md +146 -0
- package/vendors/opencode/commands/wtfp/checkpoint.md +97 -0
- package/vendors/opencode/commands/wtfp/contribute.md +266 -0
- package/vendors/opencode/commands/wtfp/create-outline.md +181 -0
- package/vendors/opencode/commands/wtfp/create-poster.md +37 -0
- package/vendors/opencode/commands/wtfp/create-slides.md +36 -0
- package/vendors/opencode/commands/wtfp/discuss-section.md +63 -0
- package/vendors/opencode/commands/wtfp/execute-outline.md +248 -0
- package/vendors/opencode/commands/wtfp/export-latex.md +210 -0
- package/vendors/opencode/commands/wtfp/help.md +185 -0
- package/vendors/opencode/commands/wtfp/insert-section.md +124 -0
- package/vendors/opencode/commands/wtfp/list-assumptions.md +105 -0
- package/vendors/opencode/commands/wtfp/map-project.md +100 -0
- package/vendors/opencode/commands/wtfp/new-paper.md +161 -0
- package/vendors/opencode/commands/wtfp/pause-writing.md +141 -0
- package/vendors/opencode/commands/wtfp/plan-milestone-gaps.md +250 -0
- package/vendors/opencode/commands/wtfp/plan-revision.md +174 -0
- package/vendors/opencode/commands/wtfp/plan-section.md +214 -0
- package/vendors/opencode/commands/wtfp/polish-prose.md +115 -0
- package/vendors/opencode/commands/wtfp/progress.md +146 -0
- package/vendors/opencode/commands/wtfp/quick.md +97 -0
- package/vendors/opencode/commands/wtfp/remove-section.md +141 -0
- package/vendors/opencode/commands/wtfp/report-bug.md +127 -0
- package/vendors/opencode/commands/wtfp/request-feature.md +156 -0
- package/vendors/opencode/commands/wtfp/research-gap.md +215 -0
- package/vendors/opencode/commands/wtfp/resume-writing.md +137 -0
- package/vendors/opencode/commands/wtfp/review-section.md +123 -0
- package/vendors/opencode/commands/wtfp/settings.md +192 -0
- package/vendors/opencode/commands/wtfp/submit-milestone.md +352 -0
- package/vendors/opencode/commands/wtfp/update.md +108 -0
- package/vendors/opencode/commands/wtfp/verify-work.md +246 -0
- 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
|
+
};
|