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:
parent
b6d972d000
commit
f9ab3db284
2 changed files with 313 additions and 1 deletions
142
scripts/load-lemonde-package.js
Executable file
142
scripts/load-lemonde-package.js
Executable 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 };
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue