From f9ab3db284f3af94619b39e9b602f62b69b3b667 Mon Sep 17 00:00:00 2001 From: TheFlow Date: Fri, 24 Oct 2025 02:18:18 +1300 Subject: [PATCH] feat(submissions): add multilingual document storage for publication packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- scripts/load-lemonde-package.js | 142 ++++++++++++++++++++ src/models/SubmissionTracking.model.js | 172 ++++++++++++++++++++++++- 2 files changed, 313 insertions(+), 1 deletion(-) create mode 100755 scripts/load-lemonde-package.js diff --git a/scripts/load-lemonde-package.js b/scripts/load-lemonde-package.js new file mode 100755 index 00000000..666e6053 --- /dev/null +++ b/scripts/load-lemonde-package.js @@ -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 }; diff --git a/src/models/SubmissionTracking.model.js b/src/models/SubmissionTracking.model.js index e6fc690e..75b6d6d2 100644 --- a/src/models/SubmissionTracking.model.js +++ b/src/models/SubmissionTracking.model.js @@ -10,7 +10,7 @@ const SubmissionTrackingSchema = new mongoose.Schema({ blogPostId: { type: mongoose.Schema.Types.ObjectId, ref: 'BlogPost', - required: true, + required: false, // Optional for standalone submission packages index: true }, 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 createdBy: { type: mongoose.Schema.Types.ObjectId, @@ -313,6 +372,117 @@ SubmissionTrackingSchema.methods.addNote = async function(content, authorId) { 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); module.exports = SubmissionTracking;