@compilr-dev/factory 0.1.15 → 0.1.17
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/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/research-model/bibliography.d.ts +26 -0
- package/dist/research-model/bibliography.js +299 -0
- package/dist/research-model/bibtex-parser.d.ts +19 -0
- package/dist/research-model/bibtex-parser.js +147 -0
- package/dist/research-model/index.d.ts +4 -0
- package/dist/research-model/index.js +4 -0
- package/dist/research-model/tools.js +162 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -24,5 +24,5 @@ export { staticLandingToolkit } from './toolkits/static-landing/index.js';
|
|
|
24
24
|
export { reactFastapiToolkit } from './toolkits/react-fastapi/index.js';
|
|
25
25
|
export { reactGoToolkit } from './toolkits/react-go/index.js';
|
|
26
26
|
export { factoryScaffoldSkill, factorySkills } from './factory/skill.js';
|
|
27
|
-
export type { ResearchModel, Section, SectionStatus, Claim, ClaimType, EvidenceStrength, SourceRef, Source, SourceRelevance, CitationInfo, CitationType, CitationStyle, ResearchQuestion, QuestionStatus, ResearchMeta, ResearchValidationError, ResearchValidationResult, ResearchModelOperation, ResearchModelToolsConfig, } from './research-model/index.js';
|
|
28
|
-
export { createDefaultResearchModel, createDefaultSection, createDefaultClaim, createDefaultSource, createDefaultQuestion, ResearchModelStore, applyResearchOperation, validateResearchModel, createResearchModelTools, } from './research-model/index.js';
|
|
27
|
+
export type { ResearchModel, Section, SectionStatus, Claim, ClaimType, EvidenceStrength, SourceRef, Source, SourceRelevance, CitationInfo, CitationType, CitationStyle, ResearchQuestion, QuestionStatus, ResearchMeta, ResearchValidationError, ResearchValidationResult, ResearchModelOperation, ResearchModelToolsConfig, BibliographyOptions, BibTeXEntry, } from './research-model/index.js';
|
|
28
|
+
export { createDefaultResearchModel, createDefaultSection, createDefaultClaim, createDefaultSource, createDefaultQuestion, ResearchModelStore, applyResearchOperation, validateResearchModel, createResearchModelTools, generateBibliography, parseBibTeX, } from './research-model/index.js';
|
package/dist/index.js
CHANGED
|
@@ -31,4 +31,4 @@ export { reactFastapiToolkit } from './toolkits/react-fastapi/index.js';
|
|
|
31
31
|
export { reactGoToolkit } from './toolkits/react-go/index.js';
|
|
32
32
|
// Factory skill (Phase 5)
|
|
33
33
|
export { factoryScaffoldSkill, factorySkills } from './factory/skill.js';
|
|
34
|
-
export { createDefaultResearchModel, createDefaultSection, createDefaultClaim, createDefaultSource, createDefaultQuestion, ResearchModelStore, applyResearchOperation, validateResearchModel, createResearchModelTools, } from './research-model/index.js';
|
|
34
|
+
export { createDefaultResearchModel, createDefaultSection, createDefaultClaim, createDefaultSource, createDefaultQuestion, ResearchModelStore, applyResearchOperation, validateResearchModel, createResearchModelTools, generateBibliography, parseBibTeX, } from './research-model/index.js';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bibliography Generator
|
|
3
|
+
*
|
|
4
|
+
* Formats source citations from the Research Model into a complete
|
|
5
|
+
* bibliography in the project's citation style.
|
|
6
|
+
*
|
|
7
|
+
* Supported styles: APA, MLA, Chicago, IEEE, Harvard
|
|
8
|
+
* Output formats: Markdown, LaTeX/BibTeX
|
|
9
|
+
*/
|
|
10
|
+
import type { Source, CitationStyle } from './types.js';
|
|
11
|
+
export interface BibliographyOptions {
|
|
12
|
+
/** Citation style (from Research Model) */
|
|
13
|
+
style: CitationStyle;
|
|
14
|
+
/** Output format */
|
|
15
|
+
format: 'markdown' | 'latex' | 'bibtex';
|
|
16
|
+
/** Filter: only referenced sources or all */
|
|
17
|
+
filter: 'referenced' | 'all';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Generate a formatted bibliography from a list of sources.
|
|
21
|
+
*
|
|
22
|
+
* @param sources - Sources to include
|
|
23
|
+
* @param referencedIds - Set of source IDs actually cited in the paper (for filter: 'referenced')
|
|
24
|
+
* @param options - Style, format, and filter options
|
|
25
|
+
*/
|
|
26
|
+
export declare function generateBibliography(sources: readonly Source[], referencedIds: Set<string>, options: BibliographyOptions): string;
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bibliography Generator
|
|
3
|
+
*
|
|
4
|
+
* Formats source citations from the Research Model into a complete
|
|
5
|
+
* bibliography in the project's citation style.
|
|
6
|
+
*
|
|
7
|
+
* Supported styles: APA, MLA, Chicago, IEEE, Harvard
|
|
8
|
+
* Output formats: Markdown, LaTeX/BibTeX
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Generate a formatted bibliography from a list of sources.
|
|
12
|
+
*
|
|
13
|
+
* @param sources - Sources to include
|
|
14
|
+
* @param referencedIds - Set of source IDs actually cited in the paper (for filter: 'referenced')
|
|
15
|
+
* @param options - Style, format, and filter options
|
|
16
|
+
*/
|
|
17
|
+
export function generateBibliography(sources, referencedIds, options) {
|
|
18
|
+
let filtered = [...sources];
|
|
19
|
+
if (options.filter === 'referenced') {
|
|
20
|
+
filtered = filtered.filter((s) => referencedIds.has(s.id));
|
|
21
|
+
}
|
|
22
|
+
// Sort alphabetically by first author last name, then year
|
|
23
|
+
filtered.sort((a, b) => {
|
|
24
|
+
const authorA = getLastName(a.citation.authors[0] ?? '');
|
|
25
|
+
const authorB = getLastName(b.citation.authors[0] ?? '');
|
|
26
|
+
const cmp = authorA.localeCompare(authorB);
|
|
27
|
+
if (cmp !== 0)
|
|
28
|
+
return cmp;
|
|
29
|
+
return Number(a.citation.year) - Number(b.citation.year);
|
|
30
|
+
});
|
|
31
|
+
if (options.format === 'bibtex') {
|
|
32
|
+
return filtered.map((s) => formatBibTeX(s)).join('\n\n');
|
|
33
|
+
}
|
|
34
|
+
const formatter = getFormatter(options.style);
|
|
35
|
+
const entries = filtered.map((s) => formatter(s.citation, s.citeKey));
|
|
36
|
+
if (options.format === 'latex') {
|
|
37
|
+
return entries.join('\n\n');
|
|
38
|
+
}
|
|
39
|
+
// Markdown
|
|
40
|
+
return entries.join('\n\n');
|
|
41
|
+
}
|
|
42
|
+
function getFormatter(style) {
|
|
43
|
+
switch (style) {
|
|
44
|
+
case 'apa':
|
|
45
|
+
return formatAPA;
|
|
46
|
+
case 'mla':
|
|
47
|
+
return formatMLA;
|
|
48
|
+
case 'chicago':
|
|
49
|
+
return formatChicago;
|
|
50
|
+
case 'ieee':
|
|
51
|
+
return formatIEEE;
|
|
52
|
+
case 'harvard':
|
|
53
|
+
return formatHarvard;
|
|
54
|
+
default:
|
|
55
|
+
return formatAPA;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* APA 7th Edition
|
|
60
|
+
* Author, A. A., & Author, B. B. (Year). Title of work. *Venue*, Volume(Issue), Pages. DOI
|
|
61
|
+
*/
|
|
62
|
+
function formatAPA(info, _citeKey) {
|
|
63
|
+
const authors = formatAuthorsAPA(info.authors);
|
|
64
|
+
const year = `(${String(info.year)})`;
|
|
65
|
+
const title = info.type === 'article' || info.type === 'chapter' ? info.title : `*${info.title}*`;
|
|
66
|
+
const parts = [authors, year, `${title}.`];
|
|
67
|
+
if (info.venue) {
|
|
68
|
+
parts.push(info.type === 'article' ? `*${info.venue}*` : info.venue);
|
|
69
|
+
if (info.volume) {
|
|
70
|
+
const vol = info.issue ? `${info.volume}(${info.issue})` : info.volume;
|
|
71
|
+
parts[parts.length - 1] += `, ${vol}`;
|
|
72
|
+
}
|
|
73
|
+
if (info.pages) {
|
|
74
|
+
parts[parts.length - 1] += `, ${info.pages}`;
|
|
75
|
+
}
|
|
76
|
+
parts[parts.length - 1] += '.';
|
|
77
|
+
}
|
|
78
|
+
if (info.doi)
|
|
79
|
+
parts.push(`https://doi.org/${info.doi}`);
|
|
80
|
+
else if (info.url)
|
|
81
|
+
parts.push(info.url);
|
|
82
|
+
return parts.join(' ');
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* MLA 9th Edition
|
|
86
|
+
* Author. "Title." *Venue*, vol. Volume, no. Issue, Year, pp. Pages.
|
|
87
|
+
*/
|
|
88
|
+
function formatMLA(info, _citeKey) {
|
|
89
|
+
const authors = formatAuthorsMLA(info.authors);
|
|
90
|
+
const title = info.type === 'article' || info.type === 'chapter' ? `"${info.title}."` : `*${info.title}*.`;
|
|
91
|
+
const parts = [authors, title];
|
|
92
|
+
if (info.venue) {
|
|
93
|
+
let venue = `*${info.venue}*`;
|
|
94
|
+
if (info.volume)
|
|
95
|
+
venue += `, vol. ${info.volume}`;
|
|
96
|
+
if (info.issue)
|
|
97
|
+
venue += `, no. ${info.issue}`;
|
|
98
|
+
venue += `, ${String(info.year)}`;
|
|
99
|
+
if (info.pages)
|
|
100
|
+
venue += `, pp. ${info.pages}`;
|
|
101
|
+
venue += '.';
|
|
102
|
+
parts.push(venue);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
parts.push(`${String(info.year)}.`);
|
|
106
|
+
}
|
|
107
|
+
if (info.doi)
|
|
108
|
+
parts.push(`https://doi.org/${info.doi}`);
|
|
109
|
+
return parts.join(' ');
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Chicago (Author-Date)
|
|
113
|
+
* Author, First. Year. "Title." *Venue* Volume (Issue): Pages. DOI.
|
|
114
|
+
*/
|
|
115
|
+
function formatChicago(info, _citeKey) {
|
|
116
|
+
const authors = formatAuthorsChicago(info.authors);
|
|
117
|
+
const year = `${String(info.year)}.`;
|
|
118
|
+
const title = info.type === 'article' || info.type === 'chapter' ? `"${info.title}."` : `*${info.title}*.`;
|
|
119
|
+
const parts = [authors, year, title];
|
|
120
|
+
if (info.venue) {
|
|
121
|
+
let venue = `*${info.venue}*`;
|
|
122
|
+
if (info.volume) {
|
|
123
|
+
venue += ` ${info.volume}`;
|
|
124
|
+
if (info.issue)
|
|
125
|
+
venue += ` (${info.issue})`;
|
|
126
|
+
}
|
|
127
|
+
if (info.pages)
|
|
128
|
+
venue += `: ${info.pages}`;
|
|
129
|
+
venue += '.';
|
|
130
|
+
parts.push(venue);
|
|
131
|
+
}
|
|
132
|
+
if (info.doi)
|
|
133
|
+
parts.push(`https://doi.org/${info.doi}.`);
|
|
134
|
+
return parts.join(' ');
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* IEEE
|
|
138
|
+
* [N] A. Author and B. Author, "Title," *Venue*, vol. V, no. N, pp. P, Year.
|
|
139
|
+
*/
|
|
140
|
+
function formatIEEE(info, citeKey) {
|
|
141
|
+
const authors = formatAuthorsIEEE(info.authors);
|
|
142
|
+
const title = `"${info.title},"`;
|
|
143
|
+
const parts = [`[${citeKey}]`, `${authors},`, title];
|
|
144
|
+
if (info.venue) {
|
|
145
|
+
let venue = `*${info.venue}*`;
|
|
146
|
+
if (info.volume)
|
|
147
|
+
venue += `, vol. ${info.volume}`;
|
|
148
|
+
if (info.issue)
|
|
149
|
+
venue += `, no. ${info.issue}`;
|
|
150
|
+
if (info.pages)
|
|
151
|
+
venue += `, pp. ${info.pages}`;
|
|
152
|
+
venue += `, ${String(info.year)}.`;
|
|
153
|
+
parts.push(venue);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
parts.push(`${String(info.year)}.`);
|
|
157
|
+
}
|
|
158
|
+
if (info.doi)
|
|
159
|
+
parts.push(`doi: ${info.doi}.`);
|
|
160
|
+
return parts.join(' ');
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Harvard
|
|
164
|
+
* Author, A. (Year) Title. *Venue*, Volume(Issue), pp. Pages.
|
|
165
|
+
*/
|
|
166
|
+
function formatHarvard(info, _citeKey) {
|
|
167
|
+
const authors = formatAuthorsHarvard(info.authors);
|
|
168
|
+
const year = `(${String(info.year)})`;
|
|
169
|
+
const title = info.type === 'article' || info.type === 'chapter' ? `'${info.title}'.` : `*${info.title}*.`;
|
|
170
|
+
const parts = [authors, year, title];
|
|
171
|
+
if (info.venue) {
|
|
172
|
+
let venue = `*${info.venue}*`;
|
|
173
|
+
if (info.volume) {
|
|
174
|
+
venue += `, ${info.volume}`;
|
|
175
|
+
if (info.issue)
|
|
176
|
+
venue += `(${info.issue})`;
|
|
177
|
+
}
|
|
178
|
+
if (info.pages)
|
|
179
|
+
venue += `, pp. ${info.pages}`;
|
|
180
|
+
venue += '.';
|
|
181
|
+
parts.push(venue);
|
|
182
|
+
}
|
|
183
|
+
if (info.doi)
|
|
184
|
+
parts.push(`doi: ${info.doi}.`);
|
|
185
|
+
return parts.join(' ');
|
|
186
|
+
}
|
|
187
|
+
// =============================================================================
|
|
188
|
+
// BibTeX Format
|
|
189
|
+
// =============================================================================
|
|
190
|
+
function formatBibTeX(source) {
|
|
191
|
+
const info = source.citation;
|
|
192
|
+
const entryType = bibtexType(info.type);
|
|
193
|
+
const lines = [`@${entryType}{${source.citeKey},`];
|
|
194
|
+
lines.push(` author = {${info.authors.join(' and ')}},`);
|
|
195
|
+
lines.push(` title = {${info.title}},`);
|
|
196
|
+
lines.push(` year = {${String(info.year)}},`);
|
|
197
|
+
if (info.venue) {
|
|
198
|
+
const field = info.type === 'book' ? 'publisher' : 'journal';
|
|
199
|
+
lines.push(` ${field} = {${info.venue}},`);
|
|
200
|
+
}
|
|
201
|
+
if (info.volume)
|
|
202
|
+
lines.push(` volume = {${info.volume}},`);
|
|
203
|
+
if (info.issue)
|
|
204
|
+
lines.push(` number = {${info.issue}},`);
|
|
205
|
+
if (info.pages)
|
|
206
|
+
lines.push(` pages = {${info.pages}},`);
|
|
207
|
+
if (info.doi)
|
|
208
|
+
lines.push(` doi = {${info.doi}},`);
|
|
209
|
+
if (info.url)
|
|
210
|
+
lines.push(` url = {${info.url}},`);
|
|
211
|
+
lines.push('}');
|
|
212
|
+
return lines.join('\n');
|
|
213
|
+
}
|
|
214
|
+
function bibtexType(citationType) {
|
|
215
|
+
switch (citationType) {
|
|
216
|
+
case 'article':
|
|
217
|
+
return 'article';
|
|
218
|
+
case 'book':
|
|
219
|
+
return 'book';
|
|
220
|
+
case 'chapter':
|
|
221
|
+
return 'incollection';
|
|
222
|
+
case 'conference':
|
|
223
|
+
return 'inproceedings';
|
|
224
|
+
case 'thesis':
|
|
225
|
+
return 'phdthesis';
|
|
226
|
+
case 'report':
|
|
227
|
+
return 'techreport';
|
|
228
|
+
case 'web':
|
|
229
|
+
return 'misc';
|
|
230
|
+
default:
|
|
231
|
+
return 'misc';
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// =============================================================================
|
|
235
|
+
// Author Formatting Helpers
|
|
236
|
+
// =============================================================================
|
|
237
|
+
function getLastName(author) {
|
|
238
|
+
// Handles "Last, First" and "First Last" formats
|
|
239
|
+
if (author.includes(','))
|
|
240
|
+
return author.split(',')[0].trim();
|
|
241
|
+
const parts = author.trim().split(/\s+/);
|
|
242
|
+
return parts[parts.length - 1];
|
|
243
|
+
}
|
|
244
|
+
function getInitials(author) {
|
|
245
|
+
if (author.includes(',')) {
|
|
246
|
+
const parts = author.split(',');
|
|
247
|
+
const firstNames = parts.slice(1).join(',').trim();
|
|
248
|
+
return firstNames
|
|
249
|
+
.split(/\s+/)
|
|
250
|
+
.map((n) => n.charAt(0).toUpperCase() + '.')
|
|
251
|
+
.join(' ');
|
|
252
|
+
}
|
|
253
|
+
const parts = author.trim().split(/\s+/);
|
|
254
|
+
return parts
|
|
255
|
+
.slice(0, -1)
|
|
256
|
+
.map((n) => n.charAt(0).toUpperCase() + '.')
|
|
257
|
+
.join(' ');
|
|
258
|
+
}
|
|
259
|
+
/** APA: Last, F. M., & Last, F. M. */
|
|
260
|
+
function formatAuthorsAPA(authors) {
|
|
261
|
+
if (authors.length === 0)
|
|
262
|
+
return '';
|
|
263
|
+
const formatted = authors.map((a) => `${getLastName(a)}, ${getInitials(a)}`);
|
|
264
|
+
if (formatted.length === 1)
|
|
265
|
+
return formatted[0];
|
|
266
|
+
if (formatted.length === 2)
|
|
267
|
+
return `${formatted[0]}, & ${formatted[1]}`;
|
|
268
|
+
return `${formatted.slice(0, -1).join(', ')}, & ${formatted[formatted.length - 1]}`;
|
|
269
|
+
}
|
|
270
|
+
/** MLA: Last, First, and First Last. */
|
|
271
|
+
function formatAuthorsMLA(authors) {
|
|
272
|
+
if (authors.length === 0)
|
|
273
|
+
return '';
|
|
274
|
+
if (authors.length === 1)
|
|
275
|
+
return `${authors[0]}.`;
|
|
276
|
+
if (authors.length === 2)
|
|
277
|
+
return `${authors[0]}, and ${authors[1]}.`;
|
|
278
|
+
return `${authors[0]}, et al.`;
|
|
279
|
+
}
|
|
280
|
+
/** Chicago: Last, First, and First Last. */
|
|
281
|
+
function formatAuthorsChicago(authors) {
|
|
282
|
+
return formatAuthorsMLA(authors);
|
|
283
|
+
}
|
|
284
|
+
/** IEEE: F. Last and F. Last */
|
|
285
|
+
function formatAuthorsIEEE(authors) {
|
|
286
|
+
const formatted = authors.map((a) => `${getInitials(a)} ${getLastName(a)}`);
|
|
287
|
+
if (formatted.length <= 2)
|
|
288
|
+
return formatted.join(' and ');
|
|
289
|
+
return `${formatted.slice(0, -1).join(', ')}, and ${formatted[formatted.length - 1]}`;
|
|
290
|
+
}
|
|
291
|
+
/** Harvard: Last, F. and Last, F. */
|
|
292
|
+
function formatAuthorsHarvard(authors) {
|
|
293
|
+
const formatted = authors.map((a) => `${getLastName(a)}, ${getInitials(a)}`);
|
|
294
|
+
if (formatted.length === 1)
|
|
295
|
+
return formatted[0];
|
|
296
|
+
if (formatted.length === 2)
|
|
297
|
+
return `${formatted[0]} and ${formatted[1]}`;
|
|
298
|
+
return `${formatted.slice(0, -1).join(', ')} and ${formatted[formatted.length - 1]}`;
|
|
299
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BibTeX Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses .bib files into structured source entries that can be
|
|
5
|
+
* bulk-imported into the Research Model's source registry.
|
|
6
|
+
*/
|
|
7
|
+
import type { CitationInfo } from './types.js';
|
|
8
|
+
export interface BibTeXEntry {
|
|
9
|
+
/** BibTeX citation key (e.g., "smith2024") */
|
|
10
|
+
citeKey: string;
|
|
11
|
+
/** Structured citation info */
|
|
12
|
+
citation: CitationInfo;
|
|
13
|
+
/** Raw BibTeX entry type (article, book, inproceedings, etc.) */
|
|
14
|
+
rawType: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Parse a BibTeX string into structured entries.
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseBibTeX(bibtex: string): BibTeXEntry[];
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BibTeX Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses .bib files into structured source entries that can be
|
|
5
|
+
* bulk-imported into the Research Model's source registry.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Parse a BibTeX string into structured entries.
|
|
9
|
+
*/
|
|
10
|
+
export function parseBibTeX(bibtex) {
|
|
11
|
+
const entries = [];
|
|
12
|
+
// Match @type{key, ... } — handles nested braces via counting
|
|
13
|
+
const entryRegex = /@(\w+)\s*\{([^,]+),/g;
|
|
14
|
+
let match;
|
|
15
|
+
while ((match = entryRegex.exec(bibtex)) !== null) {
|
|
16
|
+
const rawType = match[1].toLowerCase();
|
|
17
|
+
const citeKey = match[2].trim();
|
|
18
|
+
// Skip @string, @preamble, @comment
|
|
19
|
+
if (rawType === 'string' || rawType === 'preamble' || rawType === 'comment')
|
|
20
|
+
continue;
|
|
21
|
+
// Extract the body of this entry (from after the key comma to the matching closing brace)
|
|
22
|
+
const bodyStart = match.index + match[0].length;
|
|
23
|
+
const body = extractBody(bibtex, bodyStart);
|
|
24
|
+
if (!body)
|
|
25
|
+
continue;
|
|
26
|
+
const fields = parseFields(body);
|
|
27
|
+
const citation = fieldsToCitation(fields, rawType);
|
|
28
|
+
entries.push({ citeKey, citation, rawType });
|
|
29
|
+
}
|
|
30
|
+
return entries;
|
|
31
|
+
}
|
|
32
|
+
// =============================================================================
|
|
33
|
+
// Internals
|
|
34
|
+
// =============================================================================
|
|
35
|
+
/**
|
|
36
|
+
* Extract the body of a BibTeX entry by counting braces.
|
|
37
|
+
*/
|
|
38
|
+
function extractBody(source, startIndex) {
|
|
39
|
+
let depth = 1; // We're already inside the opening brace
|
|
40
|
+
let i = startIndex;
|
|
41
|
+
const start = startIndex;
|
|
42
|
+
while (i < source.length && depth > 0) {
|
|
43
|
+
if (source[i] === '{')
|
|
44
|
+
depth++;
|
|
45
|
+
else if (source[i] === '}')
|
|
46
|
+
depth--;
|
|
47
|
+
i++;
|
|
48
|
+
}
|
|
49
|
+
if (depth !== 0)
|
|
50
|
+
return null;
|
|
51
|
+
return source.slice(start, i - 1); // Exclude the final closing brace
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Parse field = {value} or field = "value" or field = number pairs.
|
|
55
|
+
*/
|
|
56
|
+
function parseFields(body) {
|
|
57
|
+
const fields = {};
|
|
58
|
+
// Match: fieldname = {value} or fieldname = "value" or fieldname = 123
|
|
59
|
+
const fieldRegex = /(\w+)\s*=\s*(?:\{([^}]*(?:\{[^}]*\}[^}]*)*)\}|"([^"]*)"|(\d+))/g;
|
|
60
|
+
let m;
|
|
61
|
+
while ((m = fieldRegex.exec(body)) !== null) {
|
|
62
|
+
const key = m[1].toLowerCase();
|
|
63
|
+
const value = m[2] || m[3] || m[4] || '';
|
|
64
|
+
fields[key] = cleanValue(value);
|
|
65
|
+
}
|
|
66
|
+
return fields;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Clean LaTeX artifacts from a value.
|
|
70
|
+
*/
|
|
71
|
+
function cleanValue(value) {
|
|
72
|
+
return value
|
|
73
|
+
.replace(/\{/g, '')
|
|
74
|
+
.replace(/\}/g, '')
|
|
75
|
+
.replace(/\\&/g, '&')
|
|
76
|
+
.replace(/\\textit/g, '')
|
|
77
|
+
.replace(/\\textbf/g, '')
|
|
78
|
+
.replace(/\\emph/g, '')
|
|
79
|
+
.replace(/~/g, ' ')
|
|
80
|
+
.replace(/\s+/g, ' ')
|
|
81
|
+
.trim();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Map BibTeX entry type to our CitationType.
|
|
85
|
+
*/
|
|
86
|
+
function mapType(rawType) {
|
|
87
|
+
switch (rawType) {
|
|
88
|
+
case 'article':
|
|
89
|
+
return 'article';
|
|
90
|
+
case 'book':
|
|
91
|
+
return 'book';
|
|
92
|
+
case 'incollection':
|
|
93
|
+
case 'inbook':
|
|
94
|
+
return 'chapter';
|
|
95
|
+
case 'inproceedings':
|
|
96
|
+
case 'conference':
|
|
97
|
+
return 'conference';
|
|
98
|
+
case 'phdthesis':
|
|
99
|
+
case 'mastersthesis':
|
|
100
|
+
return 'thesis';
|
|
101
|
+
case 'techreport':
|
|
102
|
+
return 'report';
|
|
103
|
+
case 'misc':
|
|
104
|
+
case 'online':
|
|
105
|
+
return 'web';
|
|
106
|
+
default:
|
|
107
|
+
return 'other';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Parse BibTeX author string ("Last, First and Last, First" or "First Last and First Last").
|
|
112
|
+
*/
|
|
113
|
+
function parseAuthors(authorStr) {
|
|
114
|
+
if (!authorStr)
|
|
115
|
+
return [];
|
|
116
|
+
return authorStr
|
|
117
|
+
.split(/\s+and\s+/i)
|
|
118
|
+
.map((a) => a.trim())
|
|
119
|
+
.filter((a) => a.length > 0);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Convert parsed fields to CitationInfo.
|
|
123
|
+
*/
|
|
124
|
+
function fieldsToCitation(fields, rawType) {
|
|
125
|
+
// Helper to access fields safely (Record<string,string> always returns string, but fields may be absent)
|
|
126
|
+
const get = (key) => {
|
|
127
|
+
return Object.prototype.hasOwnProperty.call(fields, key) ? fields[key] : undefined;
|
|
128
|
+
};
|
|
129
|
+
const venue = get('journal') ||
|
|
130
|
+
get('booktitle') ||
|
|
131
|
+
get('publisher') ||
|
|
132
|
+
get('school') ||
|
|
133
|
+
get('institution') ||
|
|
134
|
+
get('howpublished');
|
|
135
|
+
return {
|
|
136
|
+
type: mapType(rawType),
|
|
137
|
+
title: get('title') || 'Untitled',
|
|
138
|
+
authors: parseAuthors(get('author') || ''),
|
|
139
|
+
year: get('year') || 'n.d.',
|
|
140
|
+
venue,
|
|
141
|
+
volume: get('volume'),
|
|
142
|
+
issue: get('number'),
|
|
143
|
+
pages: get('pages')?.replace('--', '–'),
|
|
144
|
+
doi: get('doi'),
|
|
145
|
+
url: get('url'),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -10,3 +10,7 @@ export { validateResearchModel } from './schema.js';
|
|
|
10
10
|
export type { ResearchValidationError, ResearchValidationResult } from './schema.js';
|
|
11
11
|
export { createResearchModelTools } from './tools.js';
|
|
12
12
|
export type { ResearchModelToolsConfig } from './tools.js';
|
|
13
|
+
export { generateBibliography } from './bibliography.js';
|
|
14
|
+
export type { BibliographyOptions } from './bibliography.js';
|
|
15
|
+
export { parseBibTeX } from './bibtex-parser.js';
|
|
16
|
+
export type { BibTeXEntry } from './bibtex-parser.js';
|
|
@@ -11,3 +11,7 @@ export { applyResearchOperation } from './operations.js';
|
|
|
11
11
|
export { validateResearchModel } from './schema.js';
|
|
12
12
|
// Tools
|
|
13
13
|
export { createResearchModelTools } from './tools.js';
|
|
14
|
+
// Bibliography
|
|
15
|
+
export { generateBibliography } from './bibliography.js';
|
|
16
|
+
// BibTeX Parser
|
|
17
|
+
export { parseBibTeX } from './bibtex-parser.js';
|
|
@@ -10,6 +10,9 @@ import { ResearchModelStore } from './persistence.js';
|
|
|
10
10
|
import { applyResearchOperation } from './operations.js';
|
|
11
11
|
import { validateResearchModel } from './schema.js';
|
|
12
12
|
import { createDefaultResearchModel } from './defaults.js';
|
|
13
|
+
import { generateBibliography } from './bibliography.js';
|
|
14
|
+
import { parseBibTeX } from './bibtex-parser.js';
|
|
15
|
+
import { generateId } from './defaults.js';
|
|
13
16
|
// =============================================================================
|
|
14
17
|
// Helpers
|
|
15
18
|
// =============================================================================
|
|
@@ -572,6 +575,163 @@ function createResearchModelValidateTool(config) {
|
|
|
572
575
|
readonly: true,
|
|
573
576
|
});
|
|
574
577
|
}
|
|
578
|
+
function createBibliographyGenerateTool(config) {
|
|
579
|
+
return defineTool({
|
|
580
|
+
name: 'bibliography_generate',
|
|
581
|
+
description: 'Generate a formatted bibliography from the Research Model sources. Supports APA, MLA, Chicago, IEEE, Harvard styles and Markdown, LaTeX, BibTeX output.',
|
|
582
|
+
inputSchema: {
|
|
583
|
+
type: 'object',
|
|
584
|
+
properties: {
|
|
585
|
+
format: {
|
|
586
|
+
type: 'string',
|
|
587
|
+
enum: ['markdown', 'latex', 'bibtex'],
|
|
588
|
+
description: 'Output format. Defaults to "markdown".',
|
|
589
|
+
},
|
|
590
|
+
filter: {
|
|
591
|
+
type: 'string',
|
|
592
|
+
enum: ['referenced', 'all'],
|
|
593
|
+
description: 'Which sources to include. "referenced" = only sources cited in claims. "all" = entire registry. Defaults to "all".',
|
|
594
|
+
},
|
|
595
|
+
project_id: {
|
|
596
|
+
type: 'number',
|
|
597
|
+
description: 'Project ID. Uses active project if omitted.',
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
required: [],
|
|
601
|
+
},
|
|
602
|
+
execute: async (input) => {
|
|
603
|
+
try {
|
|
604
|
+
const projectId = getProjectId(config, input.project_id);
|
|
605
|
+
const store = createStore(config, projectId);
|
|
606
|
+
const model = await store.get();
|
|
607
|
+
if (!model) {
|
|
608
|
+
return createErrorResult('No Research Model found. Create one first.');
|
|
609
|
+
}
|
|
610
|
+
if (model.sources.length === 0) {
|
|
611
|
+
return createSuccessResult({
|
|
612
|
+
bibliography: '',
|
|
613
|
+
message: 'No sources in the model. Add sources first.',
|
|
614
|
+
count: 0,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
// Collect referenced source IDs (from all claims)
|
|
618
|
+
const referencedIds = new Set();
|
|
619
|
+
for (const section of model.sections) {
|
|
620
|
+
for (const claim of section.claims) {
|
|
621
|
+
for (const ref of claim.supportingSources)
|
|
622
|
+
referencedIds.add(ref.sourceId);
|
|
623
|
+
for (const ref of claim.contradictingSources)
|
|
624
|
+
referencedIds.add(ref.sourceId);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const options = {
|
|
628
|
+
style: model.citationStyle,
|
|
629
|
+
format: input.format ?? 'markdown',
|
|
630
|
+
filter: input.filter ?? 'all',
|
|
631
|
+
};
|
|
632
|
+
const bibliography = generateBibliography(model.sources, referencedIds, options);
|
|
633
|
+
const count = options.filter === 'referenced'
|
|
634
|
+
? model.sources.filter((s) => referencedIds.has(s.id)).length
|
|
635
|
+
: model.sources.length;
|
|
636
|
+
return createSuccessResult({
|
|
637
|
+
bibliography,
|
|
638
|
+
style: model.citationStyle,
|
|
639
|
+
format: options.format,
|
|
640
|
+
filter: options.filter,
|
|
641
|
+
count,
|
|
642
|
+
totalSources: model.sources.length,
|
|
643
|
+
referencedSources: referencedIds.size,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
return createErrorResult(error instanceof Error ? error.message : String(error));
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
readonly: true,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
function createBibTeXImportTool(config) {
|
|
654
|
+
return defineTool({
|
|
655
|
+
name: 'bibtex_import',
|
|
656
|
+
description: 'Parse a BibTeX string and bulk-import entries as sources into the Research Model. Skips duplicates by citeKey.',
|
|
657
|
+
inputSchema: {
|
|
658
|
+
type: 'object',
|
|
659
|
+
properties: {
|
|
660
|
+
content: {
|
|
661
|
+
type: 'string',
|
|
662
|
+
description: 'BibTeX content to parse (the full .bib file content).',
|
|
663
|
+
},
|
|
664
|
+
project_id: {
|
|
665
|
+
type: 'number',
|
|
666
|
+
description: 'Project ID. Uses active project if omitted.',
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
required: ['content'],
|
|
670
|
+
},
|
|
671
|
+
execute: async (input) => {
|
|
672
|
+
try {
|
|
673
|
+
const projectId = getProjectId(config, input.project_id);
|
|
674
|
+
const store = createStore(config, projectId);
|
|
675
|
+
let model = await store.get();
|
|
676
|
+
if (!model) {
|
|
677
|
+
model = createDefaultResearchModel();
|
|
678
|
+
}
|
|
679
|
+
const entries = parseBibTeX(input.content);
|
|
680
|
+
if (entries.length === 0) {
|
|
681
|
+
return createSuccessResult({
|
|
682
|
+
imported: 0,
|
|
683
|
+
skipped: 0,
|
|
684
|
+
message: 'No valid BibTeX entries found in the input.',
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
const existingKeys = new Set(model.sources.map((s) => s.citeKey));
|
|
688
|
+
let imported = 0;
|
|
689
|
+
let skipped = 0;
|
|
690
|
+
const importedKeys = [];
|
|
691
|
+
for (const entry of entries) {
|
|
692
|
+
if (existingKeys.has(entry.citeKey)) {
|
|
693
|
+
skipped++;
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
model = {
|
|
697
|
+
...model,
|
|
698
|
+
sources: [
|
|
699
|
+
...model.sources,
|
|
700
|
+
{
|
|
701
|
+
id: generateId('src'),
|
|
702
|
+
citeKey: entry.citeKey,
|
|
703
|
+
citation: entry.citation,
|
|
704
|
+
analyzed: false,
|
|
705
|
+
},
|
|
706
|
+
],
|
|
707
|
+
meta: {
|
|
708
|
+
...model.meta,
|
|
709
|
+
revision: model.meta.revision + 1,
|
|
710
|
+
updatedAt: new Date().toISOString(),
|
|
711
|
+
},
|
|
712
|
+
};
|
|
713
|
+
existingKeys.add(entry.citeKey);
|
|
714
|
+
imported++;
|
|
715
|
+
importedKeys.push(entry.citeKey);
|
|
716
|
+
}
|
|
717
|
+
if (imported > 0) {
|
|
718
|
+
await store.save(model);
|
|
719
|
+
}
|
|
720
|
+
return createSuccessResult({
|
|
721
|
+
imported,
|
|
722
|
+
skipped,
|
|
723
|
+
total: entries.length,
|
|
724
|
+
importedKeys,
|
|
725
|
+
revision: model.meta.revision,
|
|
726
|
+
message: `Imported ${String(imported)} source(s)${skipped > 0 ? `, skipped ${String(skipped)} duplicate(s)` : ''}.`,
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
catch (error) {
|
|
730
|
+
return createErrorResult(error instanceof Error ? error.message : String(error));
|
|
731
|
+
}
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
}
|
|
575
735
|
// =============================================================================
|
|
576
736
|
// Public API
|
|
577
737
|
// =============================================================================
|
|
@@ -580,5 +740,7 @@ export function createResearchModelTools(config) {
|
|
|
580
740
|
createResearchModelGetTool(config),
|
|
581
741
|
createResearchModelUpdateTool(config),
|
|
582
742
|
createResearchModelValidateTool(config),
|
|
743
|
+
createBibliographyGenerateTool(config),
|
|
744
|
+
createBibTeXImportTool(config),
|
|
583
745
|
];
|
|
584
746
|
}
|