papergod 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +244 -0
  3. package/ROADMAP.md +171 -0
  4. package/example/main.tex +360 -0
  5. package/frontend/src/components/ui/badge.jsx +5 -0
  6. package/frontend/src/components/ui/button.jsx +24 -0
  7. package/frontend/src/components/workbench.jsx +182 -0
  8. package/frontend/src/lib/utils.js +6 -0
  9. package/frontend/src/main.jsx +19 -0
  10. package/frontend/src/theme.css +256 -0
  11. package/frontend/vite.config.js +23 -0
  12. package/package.json +73 -0
  13. package/papergod-demo.png +0 -0
  14. package/public/app.js +5480 -0
  15. package/public/brand/papergod-logo.png +0 -0
  16. package/public/i18n.js +95 -0
  17. package/public/index.html +480 -0
  18. package/public/pdf-sentence-mapping.js +142 -0
  19. package/public/react/app.js +209 -0
  20. package/public/react/assets/addon-fit-YJmn1quW.js +12 -0
  21. package/public/react/assets/addon-web-links-BWjmmSgS.js +12 -0
  22. package/public/react/assets/main.css +32 -0
  23. package/public/react/assets/xterm-BqvuqXEL.js +27 -0
  24. package/public/style.css +1462 -0
  25. package/src/cli.js +128 -0
  26. package/src/server/agent-adapters.js +1240 -0
  27. package/src/server/agent-errors.js +105 -0
  28. package/src/server/agent-runtime.js +81 -0
  29. package/src/server/agent.js +173 -0
  30. package/src/server/app-version.js +86 -0
  31. package/src/server/change-history.js +114 -0
  32. package/src/server/document-structure.js +174 -0
  33. package/src/server/index.js +1442 -0
  34. package/src/server/latex-structure.js +344 -0
  35. package/src/server/latex.js +67 -0
  36. package/src/server/library-engine.js +193 -0
  37. package/src/server/library-files.js +134 -0
  38. package/src/server/literature-review.js +122 -0
  39. package/src/server/orchestration-engine.js +662 -0
  40. package/src/server/paragraph-analysis.js +300 -0
  41. package/src/server/project-resources.js +290 -0
  42. package/src/server/project-store.js +808 -0
  43. package/src/server/prompt-manifest.js +300 -0
  44. package/src/server/references.js +425 -0
  45. package/src/server/review-panel.js +263 -0
  46. package/src/server/revise-workflow.js +278 -0
  47. package/src/server/revision-engine.js +607 -0
  48. package/src/server/security.js +16 -0
  49. package/src/server/text-extraction.js +149 -0
  50. package/src/server/workspace-browser.js +49 -0
  51. package/src/server/workspace-registry.js +143 -0
  52. package/src/server/workspace-terminal.js +99 -0
  53. package/src/server/workspace.js +223 -0
  54. package/src/server/zotero.js +98 -0
@@ -0,0 +1,300 @@
1
+ import { createHash, randomUUID } from 'crypto';
2
+ import { mkdir, readFile, writeFile } from 'fs/promises';
3
+ import { basename, join } from 'path';
4
+ import { sanitizePath } from './security.js';
5
+ import { loadProject } from './project-store.js';
6
+ import { findStructureNode } from './latex-structure.js';
7
+ import { materializeLibraries } from './library-files.js';
8
+ import { loadReferenceState } from './references.js';
9
+
10
+ const CONTEXT_DIRECTORY = '.papergod/context';
11
+ const MANIFEST_VERSION = 2;
12
+
13
+ function text(value) { return typeof value === 'string' ? value.trim() : ''; }
14
+ function tokenEstimate(value) { return Math.ceil(String(value || '').length / 4); }
15
+ function sha256(value) { return createHash('sha256').update(String(value || '')).digest('hex'); }
16
+
17
+ function compactNode(node, position = {}) {
18
+ return {
19
+ id: node.id,
20
+ type: node.type,
21
+ title: text(node.title),
22
+ parentId: node.parentId || '',
23
+ position,
24
+ sourceRange: node.sourceRange || null,
25
+ summary: text(node.summary),
26
+ hasPrompt: Boolean(text(node.prompt)),
27
+ hasIntent: Boolean(text(node.intent)),
28
+ children: (node.children || []).map((child, index) => compactNode(child, {
29
+ section: position.section,
30
+ paragraph: child.type === 'paragraph' ? index + 1 : position.paragraph,
31
+ sentence: child.type === 'sentence' ? index + 1 : undefined,
32
+ })),
33
+ };
34
+ }
35
+
36
+ function structureFor(document) {
37
+ return {
38
+ documentId: document.id,
39
+ file: document.file,
40
+ title: text(document.title),
41
+ sourceHash: document.sourceHash || '',
42
+ sections: (document.sections || []).map((section, index) => compactNode(section, { section: index + 1 })),
43
+ };
44
+ }
45
+
46
+ function locateNode(document, nodeId) {
47
+ for (const [sectionIndex, section] of (document.sections || []).entries()) {
48
+ if (section.id === nodeId) return { node: section, section, sectionIndex: sectionIndex + 1 };
49
+ for (const [paragraphIndex, paragraph] of (section.children || []).entries()) {
50
+ if (paragraph.id === nodeId) return { node: paragraph, section, paragraph, sectionIndex: sectionIndex + 1, paragraphIndex: paragraphIndex + 1 };
51
+ for (const [sentenceIndex, sentence] of (paragraph.children || []).entries()) {
52
+ if (sentence.id === nodeId) return { node: sentence, section, paragraph, sentence, sectionIndex: sectionIndex + 1, paragraphIndex: paragraphIndex + 1, sentenceIndex: sentenceIndex + 1 };
53
+ }
54
+ }
55
+ }
56
+ const node = findStructureNode(document, nodeId);
57
+ return node ? { node } : null;
58
+ }
59
+
60
+ function taskFromTarget({ id, document, target, instruction, fallbackQuote = '', resourceIds = [], citekeys = [], sourceHash = '' }) {
61
+ const location = locateNode(document, target?.id);
62
+ const range = Number.isInteger(target?.start) && Number.isInteger(target?.end)
63
+ ? { start: target.start, end: target.end }
64
+ : location?.node?.sourceRange || null;
65
+ const exactQuote = String(target?.quote || fallbackQuote || '');
66
+ return {
67
+ taskId: id,
68
+ humanLocation: {
69
+ section: location?.sectionIndex || null,
70
+ sectionTitle: text(location?.section?.title),
71
+ paragraph: location?.paragraphIndex || null,
72
+ sentence: location?.sentenceIndex || null,
73
+ },
74
+ target: {
75
+ file: document.file,
76
+ nodeId: target?.id || location?.node?.id || document.id,
77
+ type: target?.type || location?.node?.type || 'document',
78
+ sourceRange: range,
79
+ offsetEncoding: 'utf16-code-units',
80
+ sourceHash: sourceHash || document.sourceHash || '',
81
+ matchMode: exactQuote ? 'exact' : 'substring-within-range',
82
+ exactQuote,
83
+ exactQuoteHash: sha256(exactQuote),
84
+ },
85
+ instruction: text(instruction) || 'Improve this target according to the supplied academic-writing context.',
86
+ templateResourceIds: [...new Set(resourceIds.filter(Boolean))],
87
+ citekeys: [...new Set(citekeys.filter(Boolean))],
88
+ };
89
+ }
90
+
91
+ function projectMarkdown(project) {
92
+ return `# Project context\n\n- name: ${project.project.name}\n- id: ${project.project.id}\n\n## Core prompt\n\n${text(project.project.corePrompt) || 'No project-level prompt.'}\n`;
93
+ }
94
+
95
+ function documentMarkdown(document) {
96
+ const lines = [`# Document context`, '', `- title: ${text(document.title)}`, `- file: ${document.file}`, '', '## Summary', '', text(document.summary) || 'No document summary.', '', '## Core prompt', '', text(document.corePrompt) || 'No document-level prompt.', '', '## Element guidance'];
97
+ const visit = (nodes) => {
98
+ for (const node of nodes || []) {
99
+ if (text(node.prompt) || text(node.summary) || text(node.intent)) {
100
+ lines.push('', `### ${node.type} ${node.id}`, `- sourceRange: ${node.sourceRange ? `${node.sourceRange.start}-${node.sourceRange.end}` : 'unknown'}`);
101
+ if (text(node.summary)) lines.push(`- summary: ${text(node.summary)}`);
102
+ if (text(node.prompt)) lines.push(`- prompt: ${text(node.prompt)}`);
103
+ if (text(node.intent)) lines.push(`- intent: ${text(node.intent)}`);
104
+ }
105
+ visit(node.children);
106
+ }
107
+ };
108
+ visit(document.sections);
109
+ return `${lines.join('\n')}\n`;
110
+ }
111
+
112
+ export function alignSuggestionsToManifest(suggestions, manifest = null, unresolvedTasks = [], sourceContent = '') {
113
+ const list = Array.isArray(suggestions) ? suggestions : [];
114
+ if (!manifest?.tasks?.length) return list.map((suggestion) => ({
115
+ ...suggestion,
116
+ taskId: suggestion.taskId || 'task_1', nodeId: suggestion.nodeId || '',
117
+ usedTemplateIds: Array.isArray(suggestion.usedTemplateIds) ? suggestion.usedTemplateIds : [],
118
+ usedCitekeys: Array.isArray(suggestion.usedCitekeys) ? suggestion.usedCitekeys : [],
119
+ }));
120
+ const tasks = new Map(manifest.tasks.map((task) => [task.taskId, task]));
121
+ const seen = new Set();
122
+ const aligned = list.map((suggestion, index) => {
123
+ const task = tasks.get(suggestion.taskId);
124
+ if (!task) throw Object.assign(new Error(`suggestions[${index}].taskId does not reference a manifest task`), { status: 422, code: 'AGENT_TASK_MISMATCH' });
125
+ if (seen.has(task.taskId)) throw Object.assign(new Error(`Agent returned more than one result for task ${task.taskId}`), { status: 422, code: 'AGENT_TASK_MISMATCH' });
126
+ seen.add(task.taskId);
127
+ let resolvedTarget = task.target;
128
+ if (task.target.matchMode === 'exact' && suggestion.originalText !== task.target.exactQuote) {
129
+ throw Object.assign(new Error(`Agent originalText did not exactly match manifest task ${task.taskId}`), { status: 422, code: 'AGENT_STALE_TARGET' });
130
+ }
131
+ if (task.target.matchMode === 'substring-within-range') {
132
+ const range = task.target.sourceRange;
133
+ const scope = sourceContent.slice(range?.start || 0, range?.end ?? sourceContent.length);
134
+ const matches = [];
135
+ let offset = suggestion.originalText ? scope.indexOf(suggestion.originalText) : -1;
136
+ while (offset !== -1) { matches.push(offset); offset = scope.indexOf(suggestion.originalText, offset + Math.max(1, suggestion.originalText.length)); }
137
+ if (matches.length !== 1) throw Object.assign(new Error(`Agent originalText for manifest task ${task.taskId} was not a unique substring of its source range`), { status: 422, code: 'AGENT_STALE_TARGET' });
138
+ const start = (range?.start || 0) + matches[0];
139
+ resolvedTarget = { ...task.target, matchMode: 'exact', sourceRange: { start, end: start + suggestion.originalText.length }, exactQuote: suggestion.originalText, exactQuoteHash: sha256(suggestion.originalText) };
140
+ }
141
+ if (suggestion.nodeId && suggestion.nodeId !== task.target.nodeId) {
142
+ throw Object.assign(new Error(`Agent nodeId did not match manifest task ${task.taskId}`), { status: 422, code: 'AGENT_TASK_MISMATCH' });
143
+ }
144
+ const allowedTemplates = new Set(task.templateResourceIds || []);
145
+ const usedTemplateIds = Array.isArray(suggestion.usedTemplateIds) ? suggestion.usedTemplateIds : [];
146
+ if (usedTemplateIds.some((id) => !allowedTemplates.has(id))) throw Object.assign(new Error(`Agent reported an unlisted template for task ${task.taskId}`), { status: 422, code: 'UNKNOWN_AGENT_RESOURCE' });
147
+ const allowedCitekeys = new Set(task.citekeys || []);
148
+ const usedCitekeys = Array.isArray(suggestion.usedCitekeys) ? suggestion.usedCitekeys : [];
149
+ if (usedCitekeys.some((id) => !allowedCitekeys.has(id))) throw Object.assign(new Error(`Agent reported an unlisted citekey for task ${task.taskId}`), { status: 422, code: 'UNKNOWN_CITATION_KEY' });
150
+ return { ...suggestion, taskId: task.taskId, nodeId: task.target.nodeId, usedTemplateIds, usedCitekeys, targetAnchor: resolvedTarget };
151
+ });
152
+ for (const [index, unresolved] of (Array.isArray(unresolvedTasks) ? unresolvedTasks : []).entries()) {
153
+ if (!unresolved || typeof unresolved.taskId !== 'string' || !tasks.has(unresolved.taskId)) throw Object.assign(new Error(`unresolvedTasks[${index}].taskId does not reference a manifest task`), { status: 422, code: 'AGENT_TASK_MISMATCH' });
154
+ if (seen.has(unresolved.taskId)) throw Object.assign(new Error(`Agent returned more than one result for task ${unresolved.taskId}`), { status: 422, code: 'AGENT_TASK_MISMATCH' });
155
+ if (typeof unresolved.reason !== 'string' || !unresolved.reason.trim()) throw Object.assign(new Error(`unresolvedTasks[${index}].reason must be non-empty`), { status: 422, code: 'AGENT_TASK_MISMATCH' });
156
+ seen.add(unresolved.taskId);
157
+ }
158
+ const missing = [...tasks.keys()].filter((taskId) => !seen.has(taskId));
159
+ if (missing.length) throw Object.assign(new Error(`Agent omitted manifest task results: ${missing.join(', ')}`), { status: 422, code: 'AGENT_TASK_INCOMPLETE', details: { missingTaskIds: missing } });
160
+ return aligned;
161
+ }
162
+
163
+ export async function loadPromptManifest(workspaceRoot, manifestPath) {
164
+ if (typeof manifestPath !== 'string' || !/^\.papergod\/context\/manifests\/manifest_[a-zA-Z0-9-]+\/manifest\.json$/.test(manifestPath)) {
165
+ throw Object.assign(new Error('Invalid Prompt Manifest path'), { status: 400, code: 'INVALID_PROMPT_MANIFEST' });
166
+ }
167
+ const path = sanitizePath(manifestPath, workspaceRoot);
168
+ if (!path) throw Object.assign(new Error('Prompt Manifest path is outside the workspace'), { status: 403, code: 'INVALID_PROMPT_MANIFEST' });
169
+ let serialized;
170
+ try { serialized = await readFile(path, 'utf-8'); }
171
+ catch (cause) { throw Object.assign(new Error('Prompt Manifest could not be loaded'), { status: cause.code === 'ENOENT' ? 409 : 500, code: 'PROMPT_MANIFEST_MISSING' }); }
172
+ let manifest;
173
+ try { manifest = JSON.parse(serialized); }
174
+ catch { throw Object.assign(new Error('Prompt Manifest is invalid JSON'), { status: 409, code: 'INVALID_PROMPT_MANIFEST' }); }
175
+ if (manifest?.version !== MANIFEST_VERSION || !Array.isArray(manifest.tasks) || !manifest.tasks.length) throw Object.assign(new Error('Prompt Manifest has an unsupported shape'), { status: 409, code: 'INVALID_PROMPT_MANIFEST' });
176
+ const taskIds = manifest.tasks.map((task) => task?.taskId);
177
+ if (taskIds.some((id) => typeof id !== 'string' || !id) || new Set(taskIds).size !== taskIds.length) throw Object.assign(new Error('Prompt Manifest task ids must be non-empty and unique'), { status: 409, code: 'INVALID_PROMPT_MANIFEST' });
178
+ const snapshotPrefix = manifestPath.slice(0, manifestPath.lastIndexOf('/') + 1);
179
+ for (const [resourcePath, expectedHash] of Object.entries(manifest.resources?.integrity || {})) {
180
+ if (!resourcePath.startsWith(snapshotPrefix)) throw Object.assign(new Error('Prompt Manifest references a resource outside its immutable snapshot'), { status: 409, code: 'INVALID_PROMPT_MANIFEST' });
181
+ const resource = sanitizePath(resourcePath, workspaceRoot);
182
+ if (!resource) throw Object.assign(new Error('Prompt Manifest resource path is invalid'), { status: 409, code: 'INVALID_PROMPT_MANIFEST' });
183
+ let content;
184
+ try { content = await readFile(resource, 'utf-8'); }
185
+ catch { throw Object.assign(new Error(`Prompt Manifest resource is missing: ${resourcePath}`), { status: 409, code: 'PROMPT_MANIFEST_RESOURCE_CHANGED' }); }
186
+ if (sha256(content) !== expectedHash) throw Object.assign(new Error(`Prompt Manifest resource changed after preview: ${resourcePath}`), { status: 409, code: 'PROMPT_MANIFEST_RESOURCE_CHANGED' });
187
+ }
188
+ return { manifest, serialized };
189
+ }
190
+
191
+ export async function materializePromptManifest(workspaceRoot, input = {}) {
192
+ const project = await loadProject(workspaceRoot);
193
+ const document = project.documents.find((item) => item.id === input.documentId)
194
+ || project.documents.find((item) => item.file === input.file)
195
+ || project.documents[0];
196
+ if (!document) throw Object.assign(new Error('Document not found for prompt manifest'), { status: 404 });
197
+ const documentPath = sanitizePath(document.file, workspaceRoot);
198
+ if (!documentPath) throw Object.assign(new Error('Document path is outside the workspace'), { status: 403 });
199
+ const sourceContent = typeof input.sourceContent === 'string' ? input.sourceContent : await readFile(documentPath, 'utf-8');
200
+ const sourceHash = sha256(sourceContent);
201
+ const library = await materializeLibraries(workspaceRoot);
202
+ const references = await loadReferenceState(workspaceRoot);
203
+ const selectedResourceIds = new Set(Array.isArray(input.resourceIds) ? input.resourceIds : []);
204
+ const unknownResourceIds = [...selectedResourceIds].filter((id) => !library.index.entries.some((entry) => entry.id === id));
205
+ if (unknownResourceIds.length) throw Object.assign(new Error(`Unknown writing resource ids: ${unknownResourceIds.join(', ')}`), { status: 400, code: 'UNKNOWN_AGENT_RESOURCE' });
206
+ const selectedWritingResources = library.index.entries.filter((entry) => selectedResourceIds.has(entry.id));
207
+ const templateIds = selectedWritingResources.filter((entry) => entry.kind === 'sentence-patterns').map((entry) => entry.id);
208
+ const intentIds = Array.isArray(input.intentIds) ? input.intentIds : [];
209
+ if (new Set(intentIds).size !== intentIds.length) throw Object.assign(new Error('Modification intent ids must be unique'), { status: 400, code: 'DUPLICATE_MODIFICATION_INTENT' });
210
+ const annotations = intentIds.map((id) => {
211
+ const annotation = project.annotations.find((item) => item.id === id);
212
+ if (!annotation) throw Object.assign(new Error(`Modification intent not found: ${id}`), { status: 404, code: 'MODIFICATION_INTENT_NOT_FOUND' });
213
+ if (annotation.documentId !== document.id) throw Object.assign(new Error(`Modification intent ${id} belongs to another document`), { status: 409, code: 'MODIFICATION_INTENT_DOCUMENT_MISMATCH' });
214
+ if (annotation.status !== 'open' || !annotation.source?.actor?.startsWith('pdf-intent:')) throw Object.assign(new Error(`Modification intent ${id} is not an open PDF intent`), { status: 409, code: 'MODIFICATION_INTENT_NOT_EXECUTABLE' });
215
+ if (annotation.target?.type === 'document' && /^PDF page\b/i.test(String(annotation.target.quote || ''))) throw Object.assign(new Error(`Modification intent ${id} has only a PDF position and must be mapped to an exact source sentence before execution`), { status: 409, code: 'MODIFICATION_INTENT_UNRESOLVED' });
216
+ return annotation;
217
+ });
218
+ const ensureTargetMatches = (task) => {
219
+ const range = task.target.sourceRange;
220
+ if (!range || !Number.isInteger(range.start) || !Number.isInteger(range.end) || range.start < 0 || range.end < range.start || range.end > sourceContent.length) {
221
+ throw Object.assign(new Error(`Manifest task ${task.taskId} has an invalid source range`), { status: 409, code: 'PROMPT_TARGET_STALE' });
222
+ }
223
+ if (task.target.exactQuote && sourceContent.slice(range.start, range.end) !== task.target.exactQuote) {
224
+ throw Object.assign(new Error(`Manifest task ${task.taskId} no longer matches its exact source quote`), { status: 409, code: 'PROMPT_TARGET_STALE' });
225
+ }
226
+ };
227
+ const tasks = annotations.length
228
+ ? annotations.map((annotation, index) => taskFromTarget({ id: annotation.id || `task_${index + 1}`, document, target: annotation.target, instruction: annotation.body, resourceIds: templateIds, citekeys: input.citekeys || [], sourceHash }))
229
+ : [taskFromTarget({ id: input.taskId || `task_${randomUUID()}`, document, target: input.target || { type: input.nodeId ? 'sentence' : 'document', id: input.nodeId || document.id, start: input.start, end: input.end, quote: input.quote }, instruction: input.instruction, fallbackQuote: input.quote, resourceIds: templateIds, citekeys: input.citekeys || [], sourceHash })];
230
+ tasks.forEach(ensureTargetMatches);
231
+
232
+ const manifestId = input.manifestId || `manifest_${randomUUID()}`;
233
+ const relativeSnapshotDirectory = `${CONTEXT_DIRECTORY}/manifests/${manifestId}`;
234
+ const snapshotDirectory = join(workspaceRoot, relativeSnapshotDirectory);
235
+ const librarySnapshotDirectory = join(snapshotDirectory, 'library');
236
+ await mkdir(librarySnapshotDirectory, { recursive: true });
237
+ const structure = structureFor(document);
238
+ structure.sourceHash = sourceHash;
239
+ const snapshotFiles = new Map([
240
+ [`${relativeSnapshotDirectory}/project.md`, projectMarkdown(project)],
241
+ [`${relativeSnapshotDirectory}/document.md`, documentMarkdown(document)],
242
+ [`${relativeSnapshotDirectory}/document-structure.json`, `${JSON.stringify(structure, null, 2)}\n`],
243
+ [`${relativeSnapshotDirectory}/references.json`, `${JSON.stringify(references, null, 2)}\n`],
244
+ ]);
245
+ const libraryPathMap = new Map();
246
+ for (const relativePath of library.paths) {
247
+ const sourcePath = sanitizePath(relativePath, workspaceRoot);
248
+ if (!sourcePath) continue;
249
+ const snapshotPath = `${relativeSnapshotDirectory}/library/${basename(relativePath)}`;
250
+ snapshotFiles.set(snapshotPath, await readFile(sourcePath, 'utf-8'));
251
+ libraryPathMap.set(relativePath, snapshotPath);
252
+ }
253
+ const bibliographySource = sanitizePath(references.bibliographyFile || 'references.bib', workspaceRoot);
254
+ let bibliography = '';
255
+ if (bibliographySource) {
256
+ try { bibliography = await readFile(bibliographySource, 'utf-8'); }
257
+ catch (cause) { if (cause.code !== 'ENOENT') throw cause; }
258
+ }
259
+ const bibliographyPath = `${relativeSnapshotDirectory}/bibliography.bib`;
260
+ snapshotFiles.set(bibliographyPath, bibliography);
261
+ const snapshotWritingResources = selectedWritingResources.map((entry) => ({ ...entry, file: libraryPathMap.get(entry.file) || entry.file }));
262
+ const snapshotLibraryFiles = library.index.files.map((entry) => {
263
+ const originalPath = entry.file.startsWith('.papergod/') ? entry.file : `.papergod/${entry.file}`;
264
+ return { ...entry, file: libraryPathMap.get(originalPath) || originalPath };
265
+ });
266
+ const libraryIndexPath = `${relativeSnapshotDirectory}/library-index.json`;
267
+ snapshotFiles.set(libraryIndexPath, `${JSON.stringify({ files: snapshotLibraryFiles, entries: snapshotWritingResources }, null, 2)}\n`);
268
+ await Promise.all([...snapshotFiles].map(([relativePath, content]) => writeFile(join(workspaceRoot, relativePath), content, 'utf-8')));
269
+ const integrity = Object.fromEntries([...snapshotFiles].map(([relativePath, content]) => [relativePath, sha256(content)]));
270
+ const manifest = {
271
+ version: MANIFEST_VERSION,
272
+ workspace: { root: '.', mainDocument: document.file },
273
+ resources: {
274
+ projectContext: `${relativeSnapshotDirectory}/project.md`,
275
+ documentContext: `${relativeSnapshotDirectory}/document.md`,
276
+ documentStructure: `${relativeSnapshotDirectory}/document-structure.json`,
277
+ bibliography: bibliographyPath,
278
+ referenceState: `${relativeSnapshotDirectory}/references.json`,
279
+ writingLibraryIndex: libraryIndexPath,
280
+ selectedWritingResources: snapshotWritingResources,
281
+ sentencePatterns: `${relativeSnapshotDirectory}/library/patterns.md`,
282
+ vocabulary: [`${relativeSnapshotDirectory}/library/vocabulary-global.md`, `${relativeSnapshotDirectory}/library/vocabulary-session.md`],
283
+ integrity,
284
+ },
285
+ policy: {
286
+ precedence: ['safety-and-output-contract', 'task-instruction', 'additional-requirements', 'document-context', 'template-reference'],
287
+ readOnly: true, readOnlyOnDemand: true, doNotReadEveryResource: true, doNotModifyFiles: true,
288
+ templateSafety: 'Use templates only for structure and style. Never copy factual claims, numbers, citations, or conclusions.',
289
+ targetSafety: 'Modify only listed exact targets. Ranges use JavaScript UTF-16 source-character offsets. If exactQuote or sourceRange is stale, report the task unresolved instead of guessing.',
290
+ },
291
+ additionalRequirements: text(input.additionalRequirements),
292
+ tasks,
293
+ outputContract: { oneResultPerTask: true, allowExplicitUnresolved: true, requireTaskId: true, requireExactOriginalText: true, noUnlistedTargets: true },
294
+ };
295
+ const relativeManifestPath = `${relativeSnapshotDirectory}/manifest.json`;
296
+ const serialized = `${JSON.stringify(manifest, null, 2)}\n`;
297
+ await writeFile(join(workspaceRoot, relativeManifestPath), serialized, 'utf-8');
298
+ const prompt = `You are editing a local academic LaTeX project.\n\nPrompt manifest: ${relativeManifestPath}\n\nRead the manifest first. Read only its immutable local resource snapshots needed for each task. Return exactly one result per manifest task: either one suggestion with the matching taskId and nodeId, or one unresolvedTasks entry with a non-empty reason. Obey the precedence policy, do not modify files, and return structured JSON only. For exact-match tasks, originalText must exactly match exactQuote. For substring-within-range tasks, choose one non-empty unique contiguous source substring inside sourceRange. Never guess or edit unrelated text.`;
299
+ return { manifestId, manifestPath: relativeManifestPath, manifest, serialized, manifestHash: sha256(serialized), prompt, characterCount: prompt.length, tokenEstimate: tokenEstimate(prompt), manifestCharacterCount: serialized.length, manifestTokenEstimate: tokenEstimate(serialized) };
300
+ }