@compilr-dev/factory 0.1.13 → 0.1.15

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.
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Source Operations
3
+ *
4
+ * source_add, source_update, source_remove
5
+ */
6
+ import { generateId } from './defaults.js';
7
+ // ─── Operations ──────────────────────────────────────────────────────────────
8
+ export function sourceAdd(model, op) {
9
+ if (model.sources.some((s) => s.citeKey === op.source.citeKey)) {
10
+ throw new Error(`Source with citeKey "${op.source.citeKey}" already exists`);
11
+ }
12
+ const source = {
13
+ id: op.source.id ?? generateId('src'),
14
+ citeKey: op.source.citeKey,
15
+ citation: op.source.citation,
16
+ kbPath: op.source.kbPath,
17
+ keyFindings: op.source.keyFindings,
18
+ tags: op.source.tags,
19
+ relevance: op.source.relevance,
20
+ analyzed: op.source.analyzed ?? false,
21
+ };
22
+ return { ...model, sources: [...model.sources, source] };
23
+ }
24
+ export function sourceUpdate(model, op) {
25
+ if (!model.sources.some((s) => s.id === op.sourceId)) {
26
+ throw new Error(`Source "${op.sourceId}" not found`);
27
+ }
28
+ // Check citeKey uniqueness if being changed
29
+ if (op.updates.citeKey) {
30
+ const existing = model.sources.find((s) => s.citeKey === op.updates.citeKey && s.id !== op.sourceId);
31
+ if (existing) {
32
+ throw new Error(`Source with citeKey "${op.updates.citeKey}" already exists`);
33
+ }
34
+ }
35
+ return {
36
+ ...model,
37
+ sources: model.sources.map((s) => {
38
+ if (s.id !== op.sourceId)
39
+ return s;
40
+ return { ...s, ...op.updates, id: s.id };
41
+ }),
42
+ };
43
+ }
44
+ export function sourceRemove(model, op) {
45
+ if (!model.sources.some((s) => s.id === op.sourceId)) {
46
+ throw new Error(`Source "${op.sourceId}" not found`);
47
+ }
48
+ // Check if source is referenced by any claims
49
+ const referencingClaims = [];
50
+ for (const section of model.sections) {
51
+ for (const claim of section.claims) {
52
+ const refs = [...claim.supportingSources, ...claim.contradictingSources];
53
+ if (refs.some((r) => r.sourceId === op.sourceId)) {
54
+ referencingClaims.push(claim.id);
55
+ }
56
+ }
57
+ }
58
+ if (referencingClaims.length > 0 && !op.force) {
59
+ throw new Error(`Cannot remove source "${op.sourceId}": referenced by ${String(referencingClaims.length)} claim(s). Use force: true to cascade.`);
60
+ }
61
+ let sections = model.sections;
62
+ if (op.force) {
63
+ // Remove source references from all claims
64
+ sections = sections.map((s) => ({
65
+ ...s,
66
+ claims: s.claims.map((c) => ({
67
+ ...c,
68
+ supportingSources: c.supportingSources.filter((r) => r.sourceId !== op.sourceId),
69
+ contradictingSources: c.contradictingSources.filter((r) => r.sourceId !== op.sourceId),
70
+ })),
71
+ }));
72
+ }
73
+ return {
74
+ ...model,
75
+ sections,
76
+ sources: model.sources.filter((s) => s.id !== op.sourceId),
77
+ };
78
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Research Model Operation Dispatcher
3
+ *
4
+ * Routes op → handler, manages revision + updatedAt.
5
+ */
6
+ import type { ResearchModel } from './types.js';
7
+ import { type SectionOperation } from './operations-section.js';
8
+ import { type ClaimOperation } from './operations-claim.js';
9
+ import { type SourceOperation } from './operations-source.js';
10
+ import { type QuestionOperation } from './operations-question.js';
11
+ import { type MetadataOperation } from './operations-metadata.js';
12
+ export type ResearchModelOperation = SectionOperation | ClaimOperation | SourceOperation | QuestionOperation | MetadataOperation;
13
+ export declare function applyResearchOperation(model: ResearchModel, operation: ResearchModelOperation): ResearchModel;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Research Model Operation Dispatcher
3
+ *
4
+ * Routes op → handler, manages revision + updatedAt.
5
+ */
6
+ import { sectionAdd, sectionUpdate, sectionRemove, sectionReorder, sectionMove, } from './operations-section.js';
7
+ import { claimAdd, claimUpdate, claimRemove, claimLinkSource, claimUnlinkSource, } from './operations-claim.js';
8
+ import { sourceAdd, sourceUpdate, sourceRemove, } from './operations-source.js';
9
+ import { questionAdd, questionUpdate, questionRemove, questionLinkSection, } from './operations-question.js';
10
+ import { setTitle, setAbstract, setCitationStyle, setKeywords, setMethodology, setAuthors, setTargetJournal, } from './operations-metadata.js';
11
+ function dispatchOperation(model, operation) {
12
+ switch (operation.op) {
13
+ // Section operations
14
+ case 'section_add':
15
+ return sectionAdd(model, operation);
16
+ case 'section_update':
17
+ return sectionUpdate(model, operation);
18
+ case 'section_remove':
19
+ return sectionRemove(model, operation);
20
+ case 'section_reorder':
21
+ return sectionReorder(model, operation);
22
+ case 'section_move':
23
+ return sectionMove(model, operation);
24
+ // Claim operations
25
+ case 'claim_add':
26
+ return claimAdd(model, operation);
27
+ case 'claim_update':
28
+ return claimUpdate(model, operation);
29
+ case 'claim_remove':
30
+ return claimRemove(model, operation);
31
+ case 'claim_link_source':
32
+ return claimLinkSource(model, operation);
33
+ case 'claim_unlink_source':
34
+ return claimUnlinkSource(model, operation);
35
+ // Source operations
36
+ case 'source_add':
37
+ return sourceAdd(model, operation);
38
+ case 'source_update':
39
+ return sourceUpdate(model, operation);
40
+ case 'source_remove':
41
+ return sourceRemove(model, operation);
42
+ // Question operations
43
+ case 'question_add':
44
+ return questionAdd(model, operation);
45
+ case 'question_update':
46
+ return questionUpdate(model, operation);
47
+ case 'question_remove':
48
+ return questionRemove(model, operation);
49
+ case 'question_link_section':
50
+ return questionLinkSection(model, operation);
51
+ // Metadata operations
52
+ case 'set_title':
53
+ return setTitle(model, operation);
54
+ case 'set_abstract':
55
+ return setAbstract(model, operation);
56
+ case 'set_citation_style':
57
+ return setCitationStyle(model, operation);
58
+ case 'set_keywords':
59
+ return setKeywords(model, operation);
60
+ case 'set_methodology':
61
+ return setMethodology(model, operation);
62
+ case 'set_authors':
63
+ return setAuthors(model, operation);
64
+ case 'set_target_journal':
65
+ return setTargetJournal(model, operation);
66
+ default:
67
+ throw new Error(`Unknown operation: ${operation.op}`);
68
+ }
69
+ }
70
+ export function applyResearchOperation(model, operation) {
71
+ const result = dispatchOperation(model, operation);
72
+ return {
73
+ ...result,
74
+ meta: {
75
+ ...result.meta,
76
+ revision: result.meta.revision + 1,
77
+ updatedAt: new Date().toISOString(),
78
+ },
79
+ };
80
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Research Model Store — Persistence Layer
3
+ *
4
+ * Reads and writes the ResearchModel as a project document
5
+ * with doc_type: 'research-model' via IDocumentRepository.
6
+ */
7
+ import type { IDocumentRepository } from '@compilr-dev/sdk';
8
+ import type { ResearchModel } from './types.js';
9
+ export declare class ResearchModelStore {
10
+ private readonly documents;
11
+ private readonly projectId;
12
+ constructor(config: {
13
+ documents: IDocumentRepository;
14
+ projectId: number;
15
+ });
16
+ get(): Promise<ResearchModel | null>;
17
+ save(model: ResearchModel): Promise<void>;
18
+ exists(): Promise<boolean>;
19
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Research Model Store — Persistence Layer
3
+ *
4
+ * Reads and writes the ResearchModel as a project document
5
+ * with doc_type: 'research-model' via IDocumentRepository.
6
+ */
7
+ const RESEARCH_MODEL_DOC_TYPE = 'research-model';
8
+ const RESEARCH_MODEL_TITLE = 'Research Model';
9
+ export class ResearchModelStore {
10
+ documents;
11
+ projectId;
12
+ constructor(config) {
13
+ this.documents = config.documents;
14
+ this.projectId = config.projectId;
15
+ }
16
+ async get() {
17
+ const doc = await this.documents.getByType(this.projectId, RESEARCH_MODEL_DOC_TYPE);
18
+ if (!doc)
19
+ return null;
20
+ try {
21
+ return JSON.parse(doc.content);
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ async save(model) {
28
+ const content = JSON.stringify(model, null, 2);
29
+ await this.documents.upsert({
30
+ project_id: this.projectId,
31
+ doc_type: RESEARCH_MODEL_DOC_TYPE,
32
+ title: RESEARCH_MODEL_TITLE,
33
+ content,
34
+ });
35
+ }
36
+ async exists() {
37
+ const doc = await this.documents.getByType(this.projectId, RESEARCH_MODEL_DOC_TYPE);
38
+ return doc !== null;
39
+ }
40
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Research Model Validation
3
+ *
4
+ * Structural validation + research-specific checks:
5
+ * - Unsupported claims (no source refs)
6
+ * - Orphan sources (not linked to any claim)
7
+ * - Unanswered questions (no sections address them)
8
+ * - Empty sections (outlined but no claims)
9
+ * - Unanalyzed sources
10
+ * - Referential integrity (source refs point to existing sources, etc.)
11
+ */
12
+ import type { ResearchModel } from './types.js';
13
+ export interface ResearchValidationError {
14
+ readonly path: string;
15
+ readonly message: string;
16
+ readonly severity: 'error' | 'warning';
17
+ }
18
+ export interface ResearchValidationResult {
19
+ readonly valid: boolean;
20
+ readonly errors: readonly ResearchValidationError[];
21
+ readonly warnings: readonly ResearchValidationError[];
22
+ }
23
+ export declare function validateResearchModel(model: ResearchModel): ResearchValidationResult;
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Research Model Validation
3
+ *
4
+ * Structural validation + research-specific checks:
5
+ * - Unsupported claims (no source refs)
6
+ * - Orphan sources (not linked to any claim)
7
+ * - Unanswered questions (no sections address them)
8
+ * - Empty sections (outlined but no claims)
9
+ * - Unanalyzed sources
10
+ * - Referential integrity (source refs point to existing sources, etc.)
11
+ */
12
+ // =============================================================================
13
+ // Structural Validation
14
+ // =============================================================================
15
+ function validateTitle(model) {
16
+ const errors = [];
17
+ if (!model.title || model.title.trim().length === 0) {
18
+ errors.push({ path: 'title', message: 'Title is required', severity: 'error' });
19
+ }
20
+ return errors;
21
+ }
22
+ function validateCitationStyle(model) {
23
+ const errors = [];
24
+ const valid = ['apa', 'mla', 'chicago', 'ieee', 'harvard'];
25
+ if (!valid.includes(model.citationStyle)) {
26
+ errors.push({
27
+ path: 'citationStyle',
28
+ message: `Invalid citation style "${model.citationStyle}"`,
29
+ severity: 'error',
30
+ });
31
+ }
32
+ return errors;
33
+ }
34
+ function validateSections(model) {
35
+ const errors = [];
36
+ const sectionIds = new Set();
37
+ for (const section of model.sections) {
38
+ if (!section.id) {
39
+ errors.push({ path: 'sections', message: 'Section missing id', severity: 'error' });
40
+ continue;
41
+ }
42
+ if (sectionIds.has(section.id)) {
43
+ errors.push({
44
+ path: `sections[${section.id}]`,
45
+ message: `Duplicate section id "${section.id}"`,
46
+ severity: 'error',
47
+ });
48
+ }
49
+ sectionIds.add(section.id);
50
+ if (!section.title || section.title.trim().length === 0) {
51
+ errors.push({
52
+ path: `sections[${section.id}].title`,
53
+ message: 'Section title is required',
54
+ severity: 'error',
55
+ });
56
+ }
57
+ // Validate parentId references
58
+ if (section.parentId && !model.sections.some((s) => s.id === section.parentId)) {
59
+ errors.push({
60
+ path: `sections[${section.id}].parentId`,
61
+ message: `Parent section "${section.parentId}" not found`,
62
+ severity: 'error',
63
+ });
64
+ }
65
+ // Validate claims within section
66
+ errors.push(...validateClaims(section, model));
67
+ }
68
+ return errors;
69
+ }
70
+ function validateClaims(section, model) {
71
+ const errors = [];
72
+ const claimIds = new Set();
73
+ const sourceIds = new Set(model.sources.map((s) => s.id));
74
+ for (const claim of section.claims) {
75
+ if (!claim.id) {
76
+ errors.push({
77
+ path: `sections[${section.id}].claims`,
78
+ message: 'Claim missing id',
79
+ severity: 'error',
80
+ });
81
+ continue;
82
+ }
83
+ if (claimIds.has(claim.id)) {
84
+ errors.push({
85
+ path: `sections[${section.id}].claims[${claim.id}]`,
86
+ message: `Duplicate claim id "${claim.id}"`,
87
+ severity: 'error',
88
+ });
89
+ }
90
+ claimIds.add(claim.id);
91
+ if (!claim.statement || claim.statement.trim().length === 0) {
92
+ errors.push({
93
+ path: `sections[${section.id}].claims[${claim.id}].statement`,
94
+ message: 'Claim statement is required',
95
+ severity: 'error',
96
+ });
97
+ }
98
+ // Validate source refs point to existing sources
99
+ for (const ref of claim.supportingSources) {
100
+ if (!sourceIds.has(ref.sourceId)) {
101
+ errors.push({
102
+ path: `sections[${section.id}].claims[${claim.id}].supportingSources`,
103
+ message: `Source "${ref.sourceId}" not found`,
104
+ severity: 'error',
105
+ });
106
+ }
107
+ }
108
+ for (const ref of claim.contradictingSources) {
109
+ if (!sourceIds.has(ref.sourceId)) {
110
+ errors.push({
111
+ path: `sections[${section.id}].claims[${claim.id}].contradictingSources`,
112
+ message: `Source "${ref.sourceId}" not found`,
113
+ severity: 'error',
114
+ });
115
+ }
116
+ }
117
+ }
118
+ return errors;
119
+ }
120
+ function validateSources(model) {
121
+ const errors = [];
122
+ const citeKeys = new Set();
123
+ for (const source of model.sources) {
124
+ if (!source.id) {
125
+ errors.push({ path: 'sources', message: 'Source missing id', severity: 'error' });
126
+ continue;
127
+ }
128
+ if (!source.citeKey || source.citeKey.trim().length === 0) {
129
+ errors.push({
130
+ path: `sources[${source.id}].citeKey`,
131
+ message: 'Source citeKey is required',
132
+ severity: 'error',
133
+ });
134
+ }
135
+ else if (citeKeys.has(source.citeKey)) {
136
+ errors.push({
137
+ path: `sources[${source.id}].citeKey`,
138
+ message: `Duplicate citeKey "${source.citeKey}"`,
139
+ severity: 'error',
140
+ });
141
+ }
142
+ citeKeys.add(source.citeKey);
143
+ if (!source.citation.title) {
144
+ errors.push({
145
+ path: `sources[${source.id}].citation.title`,
146
+ message: 'Source citation title is required',
147
+ severity: 'error',
148
+ });
149
+ }
150
+ }
151
+ return errors;
152
+ }
153
+ function validateQuestions(model) {
154
+ const errors = [];
155
+ const sectionIds = new Set(model.sections.map((s) => s.id));
156
+ for (const q of model.researchQuestions) {
157
+ if (!q.id) {
158
+ errors.push({ path: 'researchQuestions', message: 'Question missing id', severity: 'error' });
159
+ continue;
160
+ }
161
+ if (!q.question || q.question.trim().length === 0) {
162
+ errors.push({
163
+ path: `researchQuestions[${q.id}].question`,
164
+ message: 'Question text is required',
165
+ severity: 'error',
166
+ });
167
+ }
168
+ // Validate section refs
169
+ for (const ref of q.sectionRefs) {
170
+ if (!sectionIds.has(ref)) {
171
+ errors.push({
172
+ path: `researchQuestions[${q.id}].sectionRefs`,
173
+ message: `Section "${ref}" not found`,
174
+ severity: 'error',
175
+ });
176
+ }
177
+ }
178
+ }
179
+ return errors;
180
+ }
181
+ // =============================================================================
182
+ // Research-Specific Warnings
183
+ // =============================================================================
184
+ function checkUnsupportedClaims(model) {
185
+ const warnings = [];
186
+ for (const section of model.sections) {
187
+ for (const claim of section.claims) {
188
+ if (claim.supportingSources.length === 0 &&
189
+ claim.contradictingSources.length === 0 &&
190
+ claim.type !== 'original') {
191
+ warnings.push({
192
+ path: `sections[${section.id}].claims[${claim.id}]`,
193
+ message: `Claim "${truncate(claim.statement)}" has no source references`,
194
+ severity: 'warning',
195
+ });
196
+ }
197
+ }
198
+ }
199
+ return warnings;
200
+ }
201
+ function checkOrphanSources(model) {
202
+ const warnings = [];
203
+ const referencedSourceIds = new Set();
204
+ for (const section of model.sections) {
205
+ for (const claim of section.claims) {
206
+ for (const ref of claim.supportingSources)
207
+ referencedSourceIds.add(ref.sourceId);
208
+ for (const ref of claim.contradictingSources)
209
+ referencedSourceIds.add(ref.sourceId);
210
+ }
211
+ }
212
+ for (const source of model.sources) {
213
+ if (!referencedSourceIds.has(source.id)) {
214
+ warnings.push({
215
+ path: `sources[${source.id}]`,
216
+ message: `Source "${source.citeKey}" is not linked to any claim`,
217
+ severity: 'warning',
218
+ });
219
+ }
220
+ }
221
+ return warnings;
222
+ }
223
+ function checkUnansweredQuestions(model) {
224
+ const warnings = [];
225
+ for (const q of model.researchQuestions) {
226
+ if (q.sectionRefs.length === 0 && q.status === 'open') {
227
+ warnings.push({
228
+ path: `researchQuestions[${q.id}]`,
229
+ message: `Question "${truncate(q.question)}" has no sections addressing it`,
230
+ severity: 'warning',
231
+ });
232
+ }
233
+ }
234
+ return warnings;
235
+ }
236
+ function checkEmptySections(model) {
237
+ const warnings = [];
238
+ for (const section of model.sections) {
239
+ if (section.claims.length === 0 && section.status === 'outlined') {
240
+ warnings.push({
241
+ path: `sections[${section.id}]`,
242
+ message: `Section "${section.title}" has no claims yet`,
243
+ severity: 'warning',
244
+ });
245
+ }
246
+ }
247
+ return warnings;
248
+ }
249
+ function checkUnanalyzedSources(model) {
250
+ const warnings = [];
251
+ for (const source of model.sources) {
252
+ if (!source.analyzed) {
253
+ warnings.push({
254
+ path: `sources[${source.id}]`,
255
+ message: `Source "${source.citeKey}" has not been analyzed yet`,
256
+ severity: 'warning',
257
+ });
258
+ }
259
+ }
260
+ return warnings;
261
+ }
262
+ // =============================================================================
263
+ // Helpers
264
+ // =============================================================================
265
+ function truncate(str, max = 60) {
266
+ if (str.length <= max)
267
+ return str;
268
+ return str.slice(0, max - 3) + '...';
269
+ }
270
+ // =============================================================================
271
+ // Main Validator
272
+ // =============================================================================
273
+ export function validateResearchModel(model) {
274
+ const errors = [
275
+ ...validateTitle(model),
276
+ ...validateCitationStyle(model),
277
+ ...validateSections(model),
278
+ ...validateSources(model),
279
+ ...validateQuestions(model),
280
+ ];
281
+ const warnings = [
282
+ ...checkUnsupportedClaims(model),
283
+ ...checkOrphanSources(model),
284
+ ...checkUnansweredQuestions(model),
285
+ ...checkEmptySections(model),
286
+ ...checkUnanalyzedSources(model),
287
+ ];
288
+ return {
289
+ valid: errors.length === 0,
290
+ errors,
291
+ warnings,
292
+ };
293
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Research Model Tools
3
+ *
4
+ * research_model_get — scoped reads of the Research Model
5
+ * research_model_update — semantic operations on the model
6
+ * research_model_validate — check model health
7
+ */
8
+ import type { Tool } from '@compilr-dev/agents';
9
+ import type { PlatformContext } from '@compilr-dev/sdk';
10
+ export interface ResearchModelToolsConfig {
11
+ readonly context: PlatformContext;
12
+ }
13
+ export declare function createResearchModelTools(config: ResearchModelToolsConfig): Tool<never>[];