@compilr-dev/factory 0.1.13 → 0.1.14

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,3 +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';
package/dist/index.js CHANGED
@@ -31,3 +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';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Default Research Model Creators
3
+ *
4
+ * Factory functions for creating ResearchModel and sub-structure
5
+ * instances with sensible defaults.
6
+ */
7
+ import type { ResearchModel, Section, Claim, Source, ResearchQuestion, CitationInfo } from './types.js';
8
+ /** Generate a short unique ID. */
9
+ export declare function generateId(prefix?: string): string;
10
+ /** Reset ID counter (for tests). */
11
+ export declare function resetIdCounter(): void;
12
+ export declare function createDefaultResearchModel(overrides?: Partial<ResearchModel>): ResearchModel;
13
+ export declare function createDefaultSection(title: string, order: number, overrides?: Partial<Section>): Section;
14
+ export declare function createDefaultClaim(statement: string, overrides?: Partial<Claim>): Claim;
15
+ export declare function createDefaultSource(citeKey: string, citation: CitationInfo, overrides?: Partial<Source>): Source;
16
+ export declare function createDefaultQuestion(question: string, overrides?: Partial<ResearchQuestion>): ResearchQuestion;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Default Research Model Creators
3
+ *
4
+ * Factory functions for creating ResearchModel and sub-structure
5
+ * instances with sensible defaults.
6
+ */
7
+ let _idCounter = 0;
8
+ /** Generate a short unique ID. */
9
+ export function generateId(prefix = 'r') {
10
+ _idCounter++;
11
+ return `${prefix}_${Date.now().toString(36)}_${String(_idCounter)}`;
12
+ }
13
+ /** Reset ID counter (for tests). */
14
+ export function resetIdCounter() {
15
+ _idCounter = 0;
16
+ }
17
+ export function createDefaultResearchModel(overrides) {
18
+ const now = new Date().toISOString();
19
+ return {
20
+ title: overrides?.title ?? 'Untitled Paper',
21
+ citationStyle: overrides?.citationStyle ?? 'apa',
22
+ researchQuestions: overrides?.researchQuestions ?? [],
23
+ sections: overrides?.sections ?? [],
24
+ sources: overrides?.sources ?? [],
25
+ abstract: overrides?.abstract,
26
+ authors: overrides?.authors,
27
+ targetJournal: overrides?.targetJournal,
28
+ keywords: overrides?.keywords,
29
+ methodology: overrides?.methodology,
30
+ meta: {
31
+ revision: 1,
32
+ createdAt: now,
33
+ updatedAt: now,
34
+ ...overrides?.meta,
35
+ },
36
+ };
37
+ }
38
+ export function createDefaultSection(title, order, overrides) {
39
+ return {
40
+ id: overrides?.id ?? generateId('sec'),
41
+ title,
42
+ order,
43
+ status: 'outlined',
44
+ claims: [],
45
+ ...overrides,
46
+ };
47
+ }
48
+ export function createDefaultClaim(statement, overrides) {
49
+ return {
50
+ id: overrides?.id ?? generateId('clm'),
51
+ statement,
52
+ evidenceStrength: 'unsupported',
53
+ supportingSources: [],
54
+ contradictingSources: [],
55
+ type: 'original',
56
+ ...overrides,
57
+ };
58
+ }
59
+ export function createDefaultSource(citeKey, citation, overrides) {
60
+ return {
61
+ id: overrides?.id ?? generateId('src'),
62
+ citeKey,
63
+ citation,
64
+ analyzed: false,
65
+ ...overrides,
66
+ };
67
+ }
68
+ export function createDefaultQuestion(question, overrides) {
69
+ return {
70
+ id: overrides?.id ?? generateId('rq'),
71
+ question,
72
+ sectionRefs: [],
73
+ status: 'open',
74
+ ...overrides,
75
+ };
76
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Research Model — barrel export
3
+ */
4
+ export type { ResearchModel, Section, SectionStatus, Claim, ClaimType, EvidenceStrength, SourceRef, Source, SourceRelevance, CitationInfo, CitationType, CitationStyle, ResearchQuestion, QuestionStatus, ResearchMeta, } from './types.js';
5
+ export { createDefaultResearchModel, createDefaultSection, createDefaultClaim, createDefaultSource, createDefaultQuestion, generateId, resetIdCounter, } from './defaults.js';
6
+ export { ResearchModelStore } from './persistence.js';
7
+ export { applyResearchOperation } from './operations.js';
8
+ export type { ResearchModelOperation } from './operations.js';
9
+ export { validateResearchModel } from './schema.js';
10
+ export type { ResearchValidationError, ResearchValidationResult } from './schema.js';
11
+ export { createResearchModelTools } from './tools.js';
12
+ export type { ResearchModelToolsConfig } from './tools.js';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Research Model — barrel export
3
+ */
4
+ // Defaults
5
+ export { createDefaultResearchModel, createDefaultSection, createDefaultClaim, createDefaultSource, createDefaultQuestion, generateId, resetIdCounter, } from './defaults.js';
6
+ // Persistence
7
+ export { ResearchModelStore } from './persistence.js';
8
+ // Operations
9
+ export { applyResearchOperation } from './operations.js';
10
+ // Validation
11
+ export { validateResearchModel } from './schema.js';
12
+ // Tools
13
+ export { createResearchModelTools } from './tools.js';
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Claim Operations
3
+ *
4
+ * claim_add, claim_update, claim_remove, claim_link_source, claim_unlink_source
5
+ */
6
+ import type { ResearchModel, Claim, SourceRef } from './types.js';
7
+ export interface ClaimAddOp {
8
+ readonly op: 'claim_add';
9
+ readonly sectionId: string;
10
+ readonly claim: Partial<Claim> & {
11
+ statement: string;
12
+ };
13
+ }
14
+ export interface ClaimUpdateOp {
15
+ readonly op: 'claim_update';
16
+ readonly sectionId: string;
17
+ readonly claimId: string;
18
+ readonly updates: Partial<Omit<Claim, 'id' | 'supportingSources' | 'contradictingSources'>>;
19
+ }
20
+ export interface ClaimRemoveOp {
21
+ readonly op: 'claim_remove';
22
+ readonly sectionId: string;
23
+ readonly claimId: string;
24
+ }
25
+ export interface ClaimLinkSourceOp {
26
+ readonly op: 'claim_link_source';
27
+ readonly sectionId: string;
28
+ readonly claimId: string;
29
+ readonly sourceRef: SourceRef;
30
+ readonly relation: 'supporting' | 'contradicting';
31
+ }
32
+ export interface ClaimUnlinkSourceOp {
33
+ readonly op: 'claim_unlink_source';
34
+ readonly sectionId: string;
35
+ readonly claimId: string;
36
+ readonly sourceId: string;
37
+ }
38
+ export type ClaimOperation = ClaimAddOp | ClaimUpdateOp | ClaimRemoveOp | ClaimLinkSourceOp | ClaimUnlinkSourceOp;
39
+ export declare function claimAdd(model: ResearchModel, op: ClaimAddOp): ResearchModel;
40
+ export declare function claimUpdate(model: ResearchModel, op: ClaimUpdateOp): ResearchModel;
41
+ export declare function claimRemove(model: ResearchModel, op: ClaimRemoveOp): ResearchModel;
42
+ export declare function claimLinkSource(model: ResearchModel, op: ClaimLinkSourceOp): ResearchModel;
43
+ export declare function claimUnlinkSource(model: ResearchModel, op: ClaimUnlinkSourceOp): ResearchModel;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Claim Operations
3
+ *
4
+ * claim_add, claim_update, claim_remove, claim_link_source, claim_unlink_source
5
+ */
6
+ import { generateId } from './defaults.js';
7
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
8
+ function mapSection(model, sectionId, fn) {
9
+ const section = model.sections.find((s) => s.id === sectionId);
10
+ if (!section)
11
+ throw new Error(`Section "${sectionId}" not found`);
12
+ return {
13
+ ...model,
14
+ sections: model.sections.map((s) => {
15
+ if (s.id !== sectionId)
16
+ return s;
17
+ return { ...s, claims: fn(s.claims) };
18
+ }),
19
+ };
20
+ }
21
+ function findClaim(claims, claimId) {
22
+ const claim = claims.find((c) => c.id === claimId);
23
+ if (!claim)
24
+ throw new Error(`Claim "${claimId}" not found`);
25
+ return claim;
26
+ }
27
+ // ─── Operations ──────────────────────────────────────────────────────────────
28
+ export function claimAdd(model, op) {
29
+ return mapSection(model, op.sectionId, (claims) => {
30
+ const claim = {
31
+ id: op.claim.id ?? generateId('clm'),
32
+ statement: op.claim.statement,
33
+ evidenceStrength: op.claim.evidenceStrength ?? 'unsupported',
34
+ supportingSources: op.claim.supportingSources ?? [],
35
+ contradictingSources: op.claim.contradictingSources ?? [],
36
+ type: op.claim.type ?? 'original',
37
+ };
38
+ return [...claims, claim];
39
+ });
40
+ }
41
+ export function claimUpdate(model, op) {
42
+ return mapSection(model, op.sectionId, (claims) => {
43
+ findClaim(claims, op.claimId);
44
+ return claims.map((c) => {
45
+ if (c.id !== op.claimId)
46
+ return c;
47
+ return {
48
+ ...c,
49
+ ...op.updates,
50
+ id: c.id,
51
+ supportingSources: c.supportingSources,
52
+ contradictingSources: c.contradictingSources,
53
+ };
54
+ });
55
+ });
56
+ }
57
+ export function claimRemove(model, op) {
58
+ return mapSection(model, op.sectionId, (claims) => {
59
+ findClaim(claims, op.claimId);
60
+ return claims.filter((c) => c.id !== op.claimId);
61
+ });
62
+ }
63
+ export function claimLinkSource(model, op) {
64
+ // Validate source exists
65
+ if (!model.sources.some((s) => s.id === op.sourceRef.sourceId)) {
66
+ throw new Error(`Source "${op.sourceRef.sourceId}" not found`);
67
+ }
68
+ return mapSection(model, op.sectionId, (claims) => {
69
+ findClaim(claims, op.claimId);
70
+ return claims.map((c) => {
71
+ if (c.id !== op.claimId)
72
+ return c;
73
+ if (op.relation === 'supporting') {
74
+ if (c.supportingSources.some((r) => r.sourceId === op.sourceRef.sourceId))
75
+ return c;
76
+ return { ...c, supportingSources: [...c.supportingSources, op.sourceRef] };
77
+ }
78
+ else {
79
+ if (c.contradictingSources.some((r) => r.sourceId === op.sourceRef.sourceId))
80
+ return c;
81
+ return { ...c, contradictingSources: [...c.contradictingSources, op.sourceRef] };
82
+ }
83
+ });
84
+ });
85
+ }
86
+ export function claimUnlinkSource(model, op) {
87
+ return mapSection(model, op.sectionId, (claims) => {
88
+ findClaim(claims, op.claimId);
89
+ return claims.map((c) => {
90
+ if (c.id !== op.claimId)
91
+ return c;
92
+ return {
93
+ ...c,
94
+ supportingSources: c.supportingSources.filter((r) => r.sourceId !== op.sourceId),
95
+ contradictingSources: c.contradictingSources.filter((r) => r.sourceId !== op.sourceId),
96
+ };
97
+ });
98
+ });
99
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Metadata Operations
3
+ *
4
+ * set_title, set_abstract, set_citation_style, set_keywords, set_methodology,
5
+ * set_authors, set_target_journal
6
+ */
7
+ import type { ResearchModel, CitationStyle } from './types.js';
8
+ export interface SetTitleOp {
9
+ readonly op: 'set_title';
10
+ readonly title: string;
11
+ }
12
+ export interface SetAbstractOp {
13
+ readonly op: 'set_abstract';
14
+ readonly abstract: string;
15
+ }
16
+ export interface SetCitationStyleOp {
17
+ readonly op: 'set_citation_style';
18
+ readonly citationStyle: CitationStyle;
19
+ }
20
+ export interface SetKeywordsOp {
21
+ readonly op: 'set_keywords';
22
+ readonly keywords: readonly string[];
23
+ }
24
+ export interface SetMethodologyOp {
25
+ readonly op: 'set_methodology';
26
+ readonly methodology: string;
27
+ }
28
+ export interface SetAuthorsOp {
29
+ readonly op: 'set_authors';
30
+ readonly authors: readonly string[];
31
+ }
32
+ export interface SetTargetJournalOp {
33
+ readonly op: 'set_target_journal';
34
+ readonly targetJournal: string;
35
+ }
36
+ export type MetadataOperation = SetTitleOp | SetAbstractOp | SetCitationStyleOp | SetKeywordsOp | SetMethodologyOp | SetAuthorsOp | SetTargetJournalOp;
37
+ export declare function setTitle(model: ResearchModel, op: SetTitleOp): ResearchModel;
38
+ export declare function setAbstract(model: ResearchModel, op: SetAbstractOp): ResearchModel;
39
+ export declare function setCitationStyle(model: ResearchModel, op: SetCitationStyleOp): ResearchModel;
40
+ export declare function setKeywords(model: ResearchModel, op: SetKeywordsOp): ResearchModel;
41
+ export declare function setMethodology(model: ResearchModel, op: SetMethodologyOp): ResearchModel;
42
+ export declare function setAuthors(model: ResearchModel, op: SetAuthorsOp): ResearchModel;
43
+ export declare function setTargetJournal(model: ResearchModel, op: SetTargetJournalOp): ResearchModel;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Metadata Operations
3
+ *
4
+ * set_title, set_abstract, set_citation_style, set_keywords, set_methodology,
5
+ * set_authors, set_target_journal
6
+ */
7
+ // ─── Operations ──────────────────────────────────────────────────────────────
8
+ export function setTitle(model, op) {
9
+ if (!op.title.trim())
10
+ throw new Error('Title cannot be empty');
11
+ return { ...model, title: op.title };
12
+ }
13
+ export function setAbstract(model, op) {
14
+ return { ...model, abstract: op.abstract };
15
+ }
16
+ export function setCitationStyle(model, op) {
17
+ const valid = ['apa', 'mla', 'chicago', 'ieee', 'harvard'];
18
+ if (!valid.includes(op.citationStyle)) {
19
+ throw new Error(`Invalid citation style "${op.citationStyle}". Must be one of: ${valid.join(', ')}`);
20
+ }
21
+ return { ...model, citationStyle: op.citationStyle };
22
+ }
23
+ export function setKeywords(model, op) {
24
+ return { ...model, keywords: op.keywords };
25
+ }
26
+ export function setMethodology(model, op) {
27
+ return { ...model, methodology: op.methodology };
28
+ }
29
+ export function setAuthors(model, op) {
30
+ return { ...model, authors: op.authors };
31
+ }
32
+ export function setTargetJournal(model, op) {
33
+ return { ...model, targetJournal: op.targetJournal };
34
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Question Operations
3
+ *
4
+ * question_add, question_update, question_remove, question_link_section
5
+ */
6
+ import type { ResearchModel, ResearchQuestion } from './types.js';
7
+ export interface QuestionAddOp {
8
+ readonly op: 'question_add';
9
+ readonly question: Partial<ResearchQuestion> & {
10
+ question: string;
11
+ };
12
+ }
13
+ export interface QuestionUpdateOp {
14
+ readonly op: 'question_update';
15
+ readonly questionId: string;
16
+ readonly updates: Partial<Omit<ResearchQuestion, 'id'>>;
17
+ }
18
+ export interface QuestionRemoveOp {
19
+ readonly op: 'question_remove';
20
+ readonly questionId: string;
21
+ }
22
+ export interface QuestionLinkSectionOp {
23
+ readonly op: 'question_link_section';
24
+ readonly questionId: string;
25
+ readonly sectionId: string;
26
+ readonly unlink?: boolean;
27
+ }
28
+ export type QuestionOperation = QuestionAddOp | QuestionUpdateOp | QuestionRemoveOp | QuestionLinkSectionOp;
29
+ export declare function questionAdd(model: ResearchModel, op: QuestionAddOp): ResearchModel;
30
+ export declare function questionUpdate(model: ResearchModel, op: QuestionUpdateOp): ResearchModel;
31
+ export declare function questionRemove(model: ResearchModel, op: QuestionRemoveOp): ResearchModel;
32
+ export declare function questionLinkSection(model: ResearchModel, op: QuestionLinkSectionOp): ResearchModel;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Question Operations
3
+ *
4
+ * question_add, question_update, question_remove, question_link_section
5
+ */
6
+ import { generateId } from './defaults.js';
7
+ // ─── Operations ──────────────────────────────────────────────────────────────
8
+ export function questionAdd(model, op) {
9
+ const question = {
10
+ id: op.question.id ?? generateId('rq'),
11
+ question: op.question.question,
12
+ sectionRefs: op.question.sectionRefs ?? [],
13
+ status: op.question.status ?? 'open',
14
+ };
15
+ return { ...model, researchQuestions: [...model.researchQuestions, question] };
16
+ }
17
+ export function questionUpdate(model, op) {
18
+ if (!model.researchQuestions.some((q) => q.id === op.questionId)) {
19
+ throw new Error(`Research question "${op.questionId}" not found`);
20
+ }
21
+ return {
22
+ ...model,
23
+ researchQuestions: model.researchQuestions.map((q) => {
24
+ if (q.id !== op.questionId)
25
+ return q;
26
+ return { ...q, ...op.updates, id: q.id };
27
+ }),
28
+ };
29
+ }
30
+ export function questionRemove(model, op) {
31
+ if (!model.researchQuestions.some((q) => q.id === op.questionId)) {
32
+ throw new Error(`Research question "${op.questionId}" not found`);
33
+ }
34
+ return {
35
+ ...model,
36
+ researchQuestions: model.researchQuestions.filter((q) => q.id !== op.questionId),
37
+ };
38
+ }
39
+ export function questionLinkSection(model, op) {
40
+ if (!model.researchQuestions.some((q) => q.id === op.questionId)) {
41
+ throw new Error(`Research question "${op.questionId}" not found`);
42
+ }
43
+ if (!model.sections.some((s) => s.id === op.sectionId)) {
44
+ throw new Error(`Section "${op.sectionId}" not found`);
45
+ }
46
+ return {
47
+ ...model,
48
+ researchQuestions: model.researchQuestions.map((q) => {
49
+ if (q.id !== op.questionId)
50
+ return q;
51
+ if (op.unlink) {
52
+ return { ...q, sectionRefs: q.sectionRefs.filter((ref) => ref !== op.sectionId) };
53
+ }
54
+ if (q.sectionRefs.includes(op.sectionId))
55
+ return q;
56
+ return { ...q, sectionRefs: [...q.sectionRefs, op.sectionId] };
57
+ }),
58
+ };
59
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Section Operations
3
+ *
4
+ * section_add, section_update, section_remove, section_reorder, section_move
5
+ */
6
+ import type { ResearchModel, Section } from './types.js';
7
+ export interface SectionAddOp {
8
+ readonly op: 'section_add';
9
+ readonly section: Partial<Section> & {
10
+ title: string;
11
+ };
12
+ }
13
+ export interface SectionUpdateOp {
14
+ readonly op: 'section_update';
15
+ readonly sectionId: string;
16
+ readonly updates: Partial<Omit<Section, 'id' | 'claims'>>;
17
+ }
18
+ export interface SectionRemoveOp {
19
+ readonly op: 'section_remove';
20
+ readonly sectionId: string;
21
+ readonly force?: boolean;
22
+ }
23
+ export interface SectionReorderOp {
24
+ readonly op: 'section_reorder';
25
+ readonly order: readonly string[];
26
+ }
27
+ export interface SectionMoveOp {
28
+ readonly op: 'section_move';
29
+ readonly sectionId: string;
30
+ readonly newParentId?: string;
31
+ }
32
+ export type SectionOperation = SectionAddOp | SectionUpdateOp | SectionRemoveOp | SectionReorderOp | SectionMoveOp;
33
+ export declare function sectionAdd(model: ResearchModel, op: SectionAddOp): ResearchModel;
34
+ export declare function sectionUpdate(model: ResearchModel, op: SectionUpdateOp): ResearchModel;
35
+ export declare function sectionRemove(model: ResearchModel, op: SectionRemoveOp): ResearchModel;
36
+ export declare function sectionReorder(model: ResearchModel, op: SectionReorderOp): ResearchModel;
37
+ export declare function sectionMove(model: ResearchModel, op: SectionMoveOp): ResearchModel;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Section Operations
3
+ *
4
+ * section_add, section_update, section_remove, section_reorder, section_move
5
+ */
6
+ import { generateId } from './defaults.js';
7
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
8
+ function findSection(model, id) {
9
+ const section = model.sections.find((s) => s.id === id);
10
+ if (!section)
11
+ throw new Error(`Section "${id}" not found`);
12
+ return section;
13
+ }
14
+ // ─── Operations ──────────────────────────────────────────────────────────────
15
+ export function sectionAdd(model, op) {
16
+ const maxOrder = model.sections.reduce((max, s) => Math.max(max, s.order), 0);
17
+ const section = {
18
+ id: op.section.id ?? generateId('sec'),
19
+ title: op.section.title,
20
+ purpose: op.section.purpose,
21
+ order: op.section.order ?? maxOrder + 1,
22
+ parentId: op.section.parentId,
23
+ status: op.section.status ?? 'outlined',
24
+ targetWordCount: op.section.targetWordCount,
25
+ claims: op.section.claims ?? [],
26
+ notes: op.section.notes,
27
+ };
28
+ if (section.parentId) {
29
+ findSection(model, section.parentId); // validate parent exists
30
+ }
31
+ return { ...model, sections: [...model.sections, section] };
32
+ }
33
+ export function sectionUpdate(model, op) {
34
+ findSection(model, op.sectionId);
35
+ return {
36
+ ...model,
37
+ sections: model.sections.map((s) => {
38
+ if (s.id !== op.sectionId)
39
+ return s;
40
+ return { ...s, ...op.updates, id: s.id, claims: s.claims };
41
+ }),
42
+ };
43
+ }
44
+ export function sectionRemove(model, op) {
45
+ findSection(model, op.sectionId);
46
+ // Check for children
47
+ const children = model.sections.filter((s) => s.parentId === op.sectionId);
48
+ if (children.length > 0 && !op.force) {
49
+ throw new Error(`Cannot remove section "${op.sectionId}": has ${String(children.length)} child section(s). Use force: true to cascade.`);
50
+ }
51
+ let sections = model.sections.filter((s) => s.id !== op.sectionId);
52
+ if (op.force) {
53
+ sections = sections.filter((s) => s.parentId !== op.sectionId);
54
+ }
55
+ // Clean up question sectionRefs
56
+ const questions = model.researchQuestions.map((q) => ({
57
+ ...q,
58
+ sectionRefs: q.sectionRefs.filter((ref) => ref !== op.sectionId),
59
+ }));
60
+ return { ...model, sections, researchQuestions: questions };
61
+ }
62
+ export function sectionReorder(model, op) {
63
+ const ids = new Set(model.sections.map((s) => s.id));
64
+ for (const id of op.order) {
65
+ if (!ids.has(id))
66
+ throw new Error(`Section "${id}" not found`);
67
+ }
68
+ return {
69
+ ...model,
70
+ sections: model.sections.map((s) => {
71
+ const newOrder = op.order.indexOf(s.id);
72
+ if (newOrder === -1)
73
+ return s;
74
+ return { ...s, order: newOrder + 1 };
75
+ }),
76
+ };
77
+ }
78
+ export function sectionMove(model, op) {
79
+ findSection(model, op.sectionId);
80
+ if (op.newParentId) {
81
+ findSection(model, op.newParentId);
82
+ if (op.newParentId === op.sectionId) {
83
+ throw new Error('Cannot move a section under itself');
84
+ }
85
+ }
86
+ return {
87
+ ...model,
88
+ sections: model.sections.map((s) => {
89
+ if (s.id !== op.sectionId)
90
+ return s;
91
+ return { ...s, parentId: op.newParentId };
92
+ }),
93
+ };
94
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Source Operations
3
+ *
4
+ * source_add, source_update, source_remove
5
+ */
6
+ import type { ResearchModel, Source, CitationInfo } from './types.js';
7
+ export interface SourceAddOp {
8
+ readonly op: 'source_add';
9
+ readonly source: Partial<Source> & {
10
+ citeKey: string;
11
+ citation: CitationInfo;
12
+ };
13
+ }
14
+ export interface SourceUpdateOp {
15
+ readonly op: 'source_update';
16
+ readonly sourceId: string;
17
+ readonly updates: Partial<Omit<Source, 'id'>>;
18
+ }
19
+ export interface SourceRemoveOp {
20
+ readonly op: 'source_remove';
21
+ readonly sourceId: string;
22
+ readonly force?: boolean;
23
+ }
24
+ export type SourceOperation = SourceAddOp | SourceUpdateOp | SourceRemoveOp;
25
+ export declare function sourceAdd(model: ResearchModel, op: SourceAddOp): ResearchModel;
26
+ export declare function sourceUpdate(model: ResearchModel, op: SourceUpdateOp): ResearchModel;
27
+ export declare function sourceRemove(model: ResearchModel, op: SourceRemoveOp): ResearchModel;