@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 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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/factory",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "AI-driven application scaffolder for the compilr-dev ecosystem",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",