wtf-p 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -213,6 +213,24 @@ The mapping phase analyzes:
213
213
 
214
214
  ---
215
215
 
216
+ ## Citation Expert (v0.4.0)
217
+
218
+ WTF-P includes a specialized tiered pipeline for bibliography management.
219
+
220
+ ```bash
221
+ /wtfp:analyze-bib # Deep analysis of impact (seminal vs rising)
222
+ /wtfp:research-gap # Intent-aware search (seminal/recent)
223
+ /wtfp:check-refs # Auto-suggest missing citations
224
+ ```
225
+
226
+ Capabilities:
227
+ - **Tiered Search:** Integrated Semantic Scholar, SerpAPI (Google Scholar), and CrossRef.
228
+ - **Impact Ranking:** Automatically scores papers by citations, velocity, and venue prestige.
229
+ - **Deduplication:** universal anchoring via DOI and Scholar Cluster IDs.
230
+ - **Provenance:** Entries track their metadata source and verification status.
231
+
232
+ ---
233
+
216
234
  ## Command Reference
217
235
 
218
236
  ### Setup
@@ -187,16 +187,12 @@ async function install(isGlobal, isUpdate, options, pkg) {
187
187
  vendorConfig.components.forEach(component => {
188
188
  // Check --only filters
189
189
  if (onlyInstall !== 'all') {
190
- // Map 'workflows' flag to 'workflows' component ID
191
- // Map 'commands' flag to 'commands' component ID
192
- // Map 'skills' flag to 'skills' component ID
193
- if (onlyInstall !== component.id) {
194
- // Special case: 'commands' usually implies 'skills' too in legacy logic
195
- if (onlyInstall === 'commands' && component.id === 'skills') {
196
- // Allow
197
- } else {
198
- return;
199
- }
190
+ const allowedIds = [onlyInstall];
191
+ // Legacy mapping: 'commands' includes 'skills'
192
+ if (onlyInstall === 'commands') allowedIds.push('skills');
193
+
194
+ if (!allowedIds.includes(component.id)) {
195
+ return;
200
196
  }
201
197
  }
202
198
 
@@ -0,0 +1,105 @@
1
+ const fs = require('fs');
2
+ const s2 = require('./semantic-scholar');
3
+ const ranker = require('./citation-ranker');
4
+ const bibIndex = require('./bib-index'); // Assuming this exists or I need to use the one in bin/lib if available
5
+
6
+ // Check if bib-index exists in bin/lib or I need to implement basic parsing
7
+ // The command says `node ~/.claude/bin/bib-index.js`.
8
+ // In this project structure, it is likely `bin/lib/bib-index.js`.
9
+
10
+ const BIB_INDEX_PATH = './bib-index.js';
11
+
12
+ /**
13
+ * Impact Analyzer
14
+ *
15
+ * Analyzes a BibTeX file or JSON index:
16
+ * 1. Fetches metrics for each paper (via S2)
17
+ * 2. Categorizes them (Seminal, Rising, Outdated)
18
+ * 3. Returns summary
19
+ */
20
+
21
+ async function analyze(filePath) {
22
+ // 1. Index/Parse
23
+ let entries = [];
24
+ try {
25
+ // We'll require bib-index locally if possible
26
+ const indexer = require(BIB_INDEX_PATH);
27
+ const json = indexer.index(fs.readFileSync(filePath, 'utf8'));
28
+ entries = JSON.parse(json);
29
+ } catch (e) {
30
+ // Fallback: simple regex parsing if bib-index lib isn't easily require-able
31
+ console.error("Could not use bib-index, falling back to simple parse", e);
32
+ const content = fs.readFileSync(filePath, 'utf8');
33
+ const regex = /@\w+\s*{\s*([^,]+),[\s\S]*?title\s*=\s*[{"'](.+?)[}"'][\s\S]*?year\s*=\s*[{"'](\d{4})[}"']/gi;
34
+ let match;
35
+ while ((match = regex.exec(content)) !== null) {
36
+ entries.push({ key: match[1], title: match[2], year: parseInt(match[3]) });
37
+ }
38
+ }
39
+
40
+ const results = {
41
+ seminal: [], // > 1000 citations
42
+ rising: [], // Velocity > 50/mo, < 1000 total
43
+ outdated: [], // > 10 years, < 50 citations
44
+ unknown: []
45
+ };
46
+
47
+ // Process in batches to avoid rate limits
48
+ const BATCH_SIZE = 5;
49
+ for (let i = 0; i < entries.length; i += BATCH_SIZE) {
50
+ const batch = entries.slice(i, i + BATCH_SIZE);
51
+ await Promise.all(batch.map(async (entry) => {
52
+ try {
53
+ // Search by title to get metrics (most reliable if DOI missing)
54
+ // Ideally we'd use DOI if we parsed it.
55
+ const papers = await s2.search(entry.title, { limit: 1 });
56
+ if (papers && papers.length > 0) {
57
+ const p = papers[0];
58
+ const velocity = ranker.calculateVelocity(p.citationCount, p.year);
59
+ const age = new Date().getFullYear() - (p.year || new Date().getFullYear());
60
+
61
+ const enriched = {
62
+ key: entry.key,
63
+ title: p.title,
64
+ year: p.year,
65
+ citations: p.citationCount,
66
+ velocity: velocity.toFixed(1)
67
+ };
68
+
69
+ if (p.citationCount > 1000) {
70
+ results.seminal.push(enriched);
71
+ } else if (velocity > 10 && p.citationCount < 1000) { // Threshold adjusted: 50/mo is very high. 10/mo is solid.
72
+ results.rising.push(enriched);
73
+ } else if (age > 15 && p.citationCount < 50) {
74
+ results.outdated.push({ ...enriched, concern: `${age} years old, low citations` });
75
+ } else {
76
+ results.unknown.push(enriched);
77
+ }
78
+ } else {
79
+ results.unknown.push({ key: entry.key, title: entry.title, error: "Not found" });
80
+ }
81
+ } catch (e) {
82
+ results.unknown.push({ key: entry.key, title: entry.title, error: e.message });
83
+ }
84
+ }));
85
+ // Small delay between batches
86
+ await new Promise(r => setTimeout(r, 1000));
87
+ }
88
+
89
+ // Sort
90
+ results.seminal.sort((a,b) => b.citations - a.citations);
91
+ results.rising.sort((a,b) => b.velocity - a.velocity);
92
+
93
+ return results;
94
+ }
95
+
96
+ if (require.main === module) {
97
+ const file = process.argv[2];
98
+ if (!file) {
99
+ console.error("Usage: node analyze-impact.js <bibfile>");
100
+ process.exit(1);
101
+ }
102
+ analyze(file).then(r => console.log(JSON.stringify(r, null, 2)));
103
+ }
104
+
105
+ module.exports = { analyze };
@@ -0,0 +1,161 @@
1
+ const fs = require('fs');
2
+
3
+ /**
4
+ * WTF-P BibTeX Formatter
5
+ * Standardizes BibTeX entries to the project's strict template.
6
+ * Handles missing fields and assigns provenance metadata.
7
+ */
8
+
9
+ // --- Parsing Logic ---
10
+
11
+ function parseField(text, field) {
12
+ // Regex to find field="value" or field={value}
13
+ const regex = new RegExp(`\\b${field}\\s*=\\s*[{"']([\\s\\S]*?)[}"']\\s*[,}]`, 'i');
14
+ const match = text.match(regex);
15
+ return match ? match[1].trim() : null;
16
+ }
17
+
18
+ function parseEntryType(text) {
19
+ const match = text.match(/@(\w+)\s*{/);
20
+ return match ? match[1].toLowerCase() : 'misc';
21
+ }
22
+
23
+ function parseKey(text) {
24
+ const match = text.match(/@\w+\s*{\s*([^,]+),/);
25
+ return match ? match[1].trim() : 'unknown_key';
26
+ }
27
+
28
+ function parse(rawEntry) {
29
+ const entryType = parseEntryType(rawEntry);
30
+ const key = parseKey(rawEntry);
31
+
32
+ return {
33
+ key,
34
+ entryType,
35
+ author: parseField(rawEntry, 'author'),
36
+ title: parseField(rawEntry, 'title'),
37
+ booktitle: parseField(rawEntry, 'booktitle') || parseField(rawEntry, 'journal'),
38
+ year: parseField(rawEntry, 'year'),
39
+ month: parseField(rawEntry, 'month'),
40
+ abstract: parseField(rawEntry, 'abstract'),
41
+ publisher: parseField(rawEntry, 'publisher'),
42
+ volume: parseField(rawEntry, 'volume'),
43
+ number: parseField(rawEntry, 'number'),
44
+ pages: parseField(rawEntry, 'pages'),
45
+ doi: parseField(rawEntry, 'doi'),
46
+ url: parseField(rawEntry, 'url'),
47
+ keywords: parseField(rawEntry, 'keywords'),
48
+ abbr: parseField(rawEntry, 'abbr'),
49
+ bibtex_show: parseField(rawEntry, 'bibtex_show'),
50
+ selected: parseField(rawEntry, 'selected'),
51
+ projects: parseField(rawEntry, 'projects')
52
+ };
53
+ }
54
+
55
+ // --- Formatting Logic ---
56
+
57
+ function format(data, provenance = {}) {
58
+ // Determine Status
59
+ let status = provenance.wtfp_status || 'official';
60
+ const missingFields = [];
61
+
62
+ if (!data.doi) {
63
+ status = 'incomplete';
64
+ missingFields.push('doi');
65
+ }
66
+ if (!data.author || data.author === '{MISSING_AUTHOR}') {
67
+ status = 'incomplete';
68
+ missingFields.push('author');
69
+ }
70
+ if (!data.booktitle || data.booktitle === '{MISSING_VENUE}') {
71
+ missingFields.push('venue');
72
+ }
73
+ if (!data.abstract) {
74
+ if (status !== 'incomplete') status = 'partial';
75
+ missingFields.push('abstract');
76
+ }
77
+
78
+ // Use provided provenance or defaults
79
+ const wtfp_source = provenance.wtfp_source || '';
80
+ const wtfp_citations = provenance.wtfp_citations || '';
81
+ const wtfp_velocity = provenance.wtfp_velocity || '';
82
+ const wtfp_s2_id = provenance.wtfp_s2_id || '';
83
+ const wtfp_scholar_id = provenance.wtfp_scholar_id || '';
84
+ const wtfp_fetched = provenance.wtfp_fetched || new Date().toISOString().split('T')[0];
85
+
86
+ const entryType = data.entryType === 'inproceedings' ? 'conference' : (data.entryType === 'article' ? 'journal' : data.entryType || 'misc');
87
+
88
+ // Clean fields
89
+ const clean = (val, fallback = "") => val || fallback;
90
+
91
+ return `@${entryType}{${data.key || 'unknown'},
92
+ abbr="${clean(data.abbr)}",
93
+ entry_type="${entryType}",
94
+ author="${clean(data.author, "{MISSING_AUTHOR}")}",
95
+ abstract="${clean(data.abstract)}",
96
+ booktitle="${clean(data.booktitle, "{MISSING_VENUE}")}",
97
+ title="${clean(data.title, "{MISSING_TITLE}")}",
98
+ year="${clean(data.year, "{????}")}",
99
+ month="${clean(data.month)}",
100
+ publisher="${clean(data.publisher)}",
101
+ volume="${clean(data.volume)}",
102
+ number="${clean(data.number)}",
103
+ pages="${clean(data.pages)}",
104
+ keywords="${clean(data.keywords)}",
105
+ doi="${clean(data.doi)}",
106
+ url="${clean(data.url, data.doi ? `https://doi.org/${data.doi}` : "")}",
107
+ html="${clean(data.html, data.doi ? `https://doi.org/${data.doi}` : "")}",
108
+ pdf="${clean(data.pdf, "paper.pdf")}",
109
+ google_scholar_id="${clean(data.google_scholar_id)}",
110
+ additional_info="${clean(data.additional_info)}",
111
+ bibtex_show="${clean(data.bibtex_show, "true")}",
112
+ selected="${clean(data.selected, "false")}",
113
+ projects="${clean(data.projects)}",
114
+ wtfp_status="${status}",
115
+ wtfp_source="${wtfp_source}",
116
+ wtfp_citations="${wtfp_citations}",
117
+ wtfp_velocity="${wtfp_velocity}",
118
+ wtfp_s2_id="${wtfp_s2_id}",
119
+ wtfp_scholar_id="${wtfp_scholar_id}",
120
+ wtfp_fetched="${wtfp_fetched}",
121
+ wtfp_missing="${missingFields.join(',')}"
122
+ }`;
123
+ }
124
+
125
+ // --- CLI Handling ---
126
+
127
+ if (require.main === module) {
128
+ const MODE = process.argv[2];
129
+ const INPUT = process.argv[3];
130
+ const KEY_ARG = process.argv[4];
131
+
132
+ if (!MODE) {
133
+ console.error('Usage: node bib-format.js <raw_string|--file> <content|filepath> [key]');
134
+ process.exit(1);
135
+ }
136
+
137
+ let rawEntry = '';
138
+
139
+ if (MODE === '--file') {
140
+ if (!fs.existsSync(INPUT)) {
141
+ console.error(`File not found: ${INPUT}`);
142
+ process.exit(1);
143
+ }
144
+ const content = fs.readFileSync(INPUT, 'utf8');
145
+ const regex = new RegExp(`@\w+\s*{\s*${KEY_ARG}\s*,[\s\S]*?\n}`, 'm');
146
+ const match = content.match(regex);
147
+ if (match) {
148
+ rawEntry = match[0];
149
+ } else {
150
+ console.error(`Key ${KEY_ARG} not found in ${INPUT}`);
151
+ process.exit(1);
152
+ }
153
+ } else {
154
+ rawEntry = INPUT;
155
+ }
156
+
157
+ const parsed = parse(rawEntry);
158
+ console.log(format(parsed));
159
+ }
160
+
161
+ module.exports = { parse, format };
@@ -0,0 +1,104 @@
1
+ const fs = require('fs');
2
+
3
+ /**
4
+ * WTF-P Bibliography Indexer
5
+ * Indexes and retrieves BibTeX entries without loading the entire file into context.
6
+ */
7
+
8
+ const ENTRY_REGEX = /@(\w+)\s*\{\s*([^,]+),([^@]*)\}/g;
9
+
10
+ function parseEntries(text) {
11
+ const entries = [];
12
+ let match;
13
+ // Reset regex index just in case
14
+ ENTRY_REGEX.lastIndex = 0;
15
+
16
+ while ((match = ENTRY_REGEX.exec(text)) !== null) {
17
+ const fullText = match[0];
18
+ const type = match[1];
19
+ const key = match[2].trim();
20
+ const body = match[3];
21
+
22
+ // Extract title specifically for indexing
23
+ const titleMatch = body.match(/title\s*=\s*[{"'](.+?)[}"']/i);
24
+ const title = titleMatch ? titleMatch[1] : 'Unknown Title';
25
+
26
+ // Extract year
27
+ const yearMatch = body.match(/year\s*=\s*[{"']?(\d+)[}"']?/i);
28
+ const year = yearMatch ? yearMatch[1] : '????';
29
+
30
+ entries.push({ key, type, title, year, fullText: `@${type}{${key},${body}}` });
31
+ }
32
+ return entries;
33
+ }
34
+
35
+ function index(content) {
36
+ const entries = parseEntries(content);
37
+ return JSON.stringify(entries.map(e => ({ key: e.key, title: e.title, year: e.year })), null, 2);
38
+ }
39
+
40
+ function getEntry(content, key) {
41
+ const entries = parseEntries(content);
42
+ const entry = entries.find(e => e.key === key);
43
+ return entry ? entry.fullText : null;
44
+ }
45
+
46
+ function search(content, query) {
47
+ const entries = parseEntries(content);
48
+ const q = query.toLowerCase();
49
+ const results = entries.filter(e =>
50
+ e.title.toLowerCase().includes(q) ||
51
+ e.key.toLowerCase().includes(q) ||
52
+ e.fullText.toLowerCase().includes(q)
53
+ );
54
+ return JSON.stringify(results.map(e => ({ key: e.key, title: e.title, year: e.year })), null, 2);
55
+ }
56
+
57
+ // --- CLI Handling ---
58
+
59
+ if (require.main === module) {
60
+ const COMMAND = process.argv[2];
61
+ const BIB_FILE = process.argv[3];
62
+ const ARG = process.argv[4];
63
+
64
+ if (!COMMAND || !BIB_FILE) {
65
+ console.error('Usage: node bib-index.js <command> <bib_file> [arg]');
66
+ process.exit(1);
67
+ }
68
+
69
+ if (!fs.existsSync(BIB_FILE)) {
70
+ console.error(`Error: Bibliography file not found: ${BIB_FILE}`);
71
+ process.exit(1);
72
+ }
73
+
74
+ const content = fs.readFileSync(BIB_FILE, 'utf8');
75
+
76
+ if (COMMAND === 'index') {
77
+ console.log(index(content));
78
+ }
79
+ else if (COMMAND === 'get') {
80
+ if (!ARG) {
81
+ console.error('Error: Missing citation key');
82
+ process.exit(1);
83
+ }
84
+ const result = getEntry(content, ARG);
85
+ if (result) console.log(result);
86
+ else {
87
+ console.error(`Error: Entry '${ARG}' not found.`);
88
+ process.exit(1);
89
+ }
90
+ }
91
+ else if (COMMAND === 'search') {
92
+ if (!ARG) {
93
+ console.error('Error: Missing search query');
94
+ process.exit(1);
95
+ }
96
+ console.log(search(content, ARG));
97
+ }
98
+ else {
99
+ console.error(`Unknown command: ${COMMAND}`);
100
+ process.exit(1);
101
+ }
102
+ }
103
+
104
+ module.exports = { index, getEntry, search, parseEntries };