feat(submissions): add multilingual document storage for publication packages

Extends SubmissionTracking model to support complete bilingual submission
packages with version control for multiple languages.

Schema additions:
- documents.coverLetter.versions[] - Language-versioned content
- documents.mainArticle.versions[] - With translation metadata
- documents.authorBio.versions[]
- documents.technicalBrief.versions[]

Helper methods:
- getDocument(docType, language, fallbackToDefault)
- setDocumentVersion(docType, language, content, metadata)
- getAvailableLanguages(docType)
- isPackageComplete(language)
- exportPackage(language)

Scripts:
- load-lemonde-package.js - Loads complete Le Monde submission package

Le Monde Package:
- Publication target: Rank 10, high-value French intellectual publication
- Theme: Post-Weberian organizational theory for AI age
- Content: Wittgenstein + Weber critique + indigenous data sovereignty
- Format: 187-word letter (within 150-200 requirement)
- Languages: English (original) + French (translated)
- Database ID: 68fa2abd2e6acd5691932150
- Status: Ready for submission

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
TheFlow 2025-10-24 02:18:18 +13:00
parent b6d972d000
commit f9ab3db284
2 changed files with 313 additions and 1 deletions

142
scripts/load-lemonde-package.js Executable file
View file

@ -0,0 +1,142 @@
#!/usr/bin/env node
/**
* Load Le Monde Submission Package
* Stores the complete bilingual submission package in the database
*/
const fs = require('fs');
const path = require('path');
const mongoose = require('mongoose');
require('dotenv').config();
const SubmissionTracking = require('../src/models/SubmissionTracking.model');
const PACKAGE_DIR = '/tmp/le-monde-package';
// Document file mappings
const DOCUMENTS = {
coverLetter: {
en: '1-cover-letter.txt',
fr: '1-cover-letter-FR.txt'
},
mainArticle: {
en: '2-letter-to-editor.txt',
fr: '2-letter-to-editor-FR.txt'
},
authorBio: {
en: '3-author-bio.txt',
fr: '3-author-bio-FR.txt'
},
technicalBrief: {
en: '4-technical-brief.txt',
fr: '4-technical-brief-FR.txt'
}
};
async function loadPackage() {
try {
// Connect to database
const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/tractatus_dev';
await mongoose.connect(mongoUri);
console.log('✓ Connected to MongoDB');
// Check if package already exists
let submission = await SubmissionTracking.findOne({
publicationId: 'le-monde-lettre',
title: 'Beyond Weber: AI and the Limits of Bureaucratic Rationality'
});
if (!submission) {
// Create new submission tracking entry
submission = new SubmissionTracking({
publicationId: 'le-monde-lettre',
publicationName: 'Le Monde',
title: 'Beyond Weber: AI and the Limits of Bureaucratic Rationality',
wordCount: 187,
contentType: 'letter',
status: 'ready',
createdBy: new mongoose.Types.ObjectId('000000000000000000000000'), // System user
lastUpdatedBy: new mongoose.Types.ObjectId('000000000000000000000000')
});
console.log('✓ Created new submission tracking entry');
} else {
console.log('✓ Found existing submission tracking entry');
}
// Load and store each document type in both languages
for (const [docType, files] of Object.entries(DOCUMENTS)) {
console.log(`\nProcessing ${docType}...`);
for (const [language, filename] of Object.entries(files)) {
const filepath = path.join(PACKAGE_DIR, filename);
if (!fs.existsSync(filepath)) {
console.warn(` ⚠ File not found: ${filepath}`);
continue;
}
const content = fs.readFileSync(filepath, 'utf8');
const wordCount = content.split(/\s+/).length;
await submission.setDocumentVersion(docType, language, content, {
translatedBy: language === 'en' ? 'manual' : 'claude',
approved: true
});
console.log(` ✓ Loaded ${language}: ${wordCount} words`);
}
// Mark document type as completed
if (submission.documents[docType]) {
submission.documents[docType].completed = true;
}
}
// Save final state
await submission.save();
// Display summary
console.log('\n' + '='.repeat(60));
console.log('PACKAGE SUMMARY');
console.log('='.repeat(60));
console.log(`Publication: ${submission.publicationName} (${submission.publicationId})`);
console.log(`Title: ${submission.title}`);
console.log(`Status: ${submission.status}`);
console.log(`\nDocuments:`);
for (const docType of ['coverLetter', 'mainArticle', 'authorBio', 'technicalBrief']) {
const languages = submission.getAvailableLanguages(docType);
const completed = submission.documents[docType]?.completed ? '✓' : '✗';
console.log(` ${completed} ${docType}: ${languages.join(', ')}`);
}
console.log(`\nPackage complete (EN): ${submission.isPackageComplete('en')}`);
console.log(`Package complete (FR): ${submission.isPackageComplete('fr')}`);
// Export sample
console.log('\n' + '='.repeat(60));
console.log('FRENCH PACKAGE EXPORT (SAMPLE)');
console.log('='.repeat(60));
const frPackage = submission.exportPackage('fr');
console.log(`\nCover Letter (${frPackage.documents.coverLetter.wordCount} words):`);
console.log(frPackage.documents.coverLetter.content.substring(0, 200) + '...\n');
console.log(`Main Article (${frPackage.documents.mainArticle.wordCount} words):`);
console.log(frPackage.documents.mainArticle.content.substring(0, 200) + '...\n');
console.log(`\n✅ Le Monde submission package loaded successfully!`);
console.log(`\nSubmission ID: ${submission._id}`);
await mongoose.connection.close();
} catch (error) {
console.error('❌ Error loading package:', error);
process.exit(1);
}
}
// Run if called directly
if (require.main === module) {
loadPackage();
}
module.exports = { loadPackage };

View file

@ -10,7 +10,7 @@ const SubmissionTrackingSchema = new mongoose.Schema({
blogPostId: { blogPostId: {
type: mongoose.Schema.Types.ObjectId, type: mongoose.Schema.Types.ObjectId,
ref: 'BlogPost', ref: 'BlogPost',
required: true, required: false, // Optional for standalone submission packages
index: true index: true
}, },
publicationId: { publicationId: {
@ -129,6 +129,65 @@ const SubmissionTrackingSchema = new mongoose.Schema({
}] }]
}, },
// Multilingual document storage for complete submission packages
documents: {
coverLetter: {
primaryLanguage: { type: String, default: 'en' },
versions: [{
language: {
type: String,
required: true,
enum: ['en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'ar', 'mi']
},
content: String,
wordCount: Number,
lastUpdated: { type: Date, default: Date.now },
translatedBy: {
type: String,
enum: ['human', 'deepl', 'claude', 'google', 'manual']
},
approved: { type: Boolean, default: false }
}],
completed: { type: Boolean, default: false }
},
mainArticle: {
primaryLanguage: { type: String, default: 'en' },
versions: [{
language: String,
content: String,
wordCount: Number,
lastUpdated: { type: Date, default: Date.now },
translatedBy: String,
approved: { type: Boolean, default: false }
}],
completed: { type: Boolean, default: false }
},
authorBio: {
primaryLanguage: { type: String, default: 'en' },
versions: [{
language: String,
content: String,
wordCount: Number,
lastUpdated: { type: Date, default: Date.now },
translatedBy: String,
approved: { type: Boolean, default: false }
}],
completed: { type: Boolean, default: false }
},
technicalBrief: {
primaryLanguage: { type: String, default: 'en' },
versions: [{
language: String,
content: String,
wordCount: Number,
lastUpdated: { type: Date, default: Date.now },
translatedBy: String,
approved: { type: Boolean, default: false }
}],
completed: { type: Boolean, default: false }
}
},
// Metadata // Metadata
createdBy: { createdBy: {
type: mongoose.Schema.Types.ObjectId, type: mongoose.Schema.Types.ObjectId,
@ -313,6 +372,117 @@ SubmissionTrackingSchema.methods.addNote = async function(content, authorId) {
return await this.save(); return await this.save();
}; };
/**
* Get document in specific language (with fallback)
*/
SubmissionTrackingSchema.methods.getDocument = function(docType, language, fallbackToDefault = true) {
const doc = this.documents?.[docType];
if (!doc) return null;
// Find version in requested language
let version = doc.versions.find(v => v.language === language);
// Fallback to primary language if requested
if (!version && fallbackToDefault) {
version = doc.versions.find(v => v.language === doc.primaryLanguage);
}
return version;
};
/**
* Add or update document version
*/
SubmissionTrackingSchema.methods.setDocumentVersion = async function(docType, language, content, metadata = {}) {
if (!this.documents) {
this.documents = {};
}
if (!this.documents[docType]) {
this.documents[docType] = {
primaryLanguage: language,
versions: [],
completed: false
};
}
// Find existing version or create new
const existingIndex = this.documents[docType].versions.findIndex(v => v.language === language);
const versionData = {
language,
content,
wordCount: content.split(/\s+/).length,
lastUpdated: new Date(),
...metadata
};
if (existingIndex >= 0) {
this.documents[docType].versions[existingIndex] = {
...this.documents[docType].versions[existingIndex],
...versionData
};
} else {
this.documents[docType].versions.push(versionData);
}
return await this.save();
};
/**
* Get all available languages for a document type
*/
SubmissionTrackingSchema.methods.getAvailableLanguages = function(docType) {
const doc = this.documents?.[docType];
if (!doc) return [];
return doc.versions.map(v => v.language);
};
/**
* Check if submission package is complete for a language
*/
SubmissionTrackingSchema.methods.isPackageComplete = function(language) {
const requiredDocs = ['coverLetter', 'mainArticle', 'authorBio'];
return requiredDocs.every(docType => {
const doc = this.documents?.[docType];
if (!doc) return false;
return doc.versions.some(v => v.language === language);
});
};
/**
* Export package for submission
*/
SubmissionTrackingSchema.methods.exportPackage = function(language) {
const packageData = {
publication: {
id: this.publicationId,
name: this.publicationName
},
metadata: {
title: this.title,
wordCount: this.wordCount,
language,
exportedAt: new Date()
},
documents: {}
};
['coverLetter', 'mainArticle', 'authorBio', 'technicalBrief'].forEach(docType => {
const version = this.getDocument(docType, language, true);
if (version) {
packageData.documents[docType] = {
content: version.content,
wordCount: version.wordCount,
language: version.language
};
}
});
return packageData;
};
const SubmissionTracking = mongoose.model('SubmissionTracking', SubmissionTrackingSchema); const SubmissionTracking = mongoose.model('SubmissionTracking', SubmissionTrackingSchema);
module.exports = SubmissionTracking; module.exports = SubmissionTracking;