aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
225 lines • 7.88 kB
JavaScript
/**
* Archival service for OAIS-compliant packaging
*
* @module research/services/archival
*/
import { promises as fs } from 'fs';
import { join, dirname } from 'path';
import { createHash } from 'crypto';
/**
* Archival service for OAIS package management
*/
export class ArchivalService {
archiveDir;
formatVersion;
constructor(config = {}) {
this.archiveDir = config.archiveDir || '.aiwg/research/archives';
this.formatVersion = config.formatVersion || '1.0';
}
/**
* Create OAIS archive package
*/
async createPackage(sources, type) {
const id = this.generatePackageId(type);
const packagePath = join(this.archiveDir, type.toLowerCase(), id);
// Create package directory
await this.ensureDir(packagePath);
// Create subdirectories
await this.ensureDir(join(packagePath, 'data'));
await this.ensureDir(join(packagePath, 'metadata'));
// Copy source files and metadata
const refIds = [];
const manifestEntries = [];
for (const source of sources) {
refIds.push(source.refId);
// Copy PDF to package
const destPath = join(packagePath, 'data', `${source.refId}.pdf`);
await fs.copyFile(source.filePath, destPath);
// Create metadata file
const metadataPath = join(packagePath, 'metadata', `${source.refId}.json`);
await fs.writeFile(metadataPath, JSON.stringify({
paper: source.paper,
refId: source.refId,
acquiredAt: source.acquiredAt,
originalChecksum: source.checksum,
}, null, 2), 'utf-8');
manifestEntries.push({
refId: source.refId,
originalPath: source.filePath,
packagePath: destPath,
checksum: source.checksum,
sizeBytes: source.sizeBytes,
});
}
// Create manifest
const manifestPath = join(packagePath, 'manifest.json');
const manifest = {
packageId: id,
packageType: type,
formatVersion: this.formatVersion,
createdAt: new Date().toISOString(),
sourceCount: sources.length,
entries: manifestEntries,
};
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
// Calculate total size
let totalSize = 0;
for (const entry of manifestEntries) {
totalSize += entry.sizeBytes;
}
// Compute package checksum
const packageChecksum = await this.computeDirectoryChecksum(packagePath);
const archivePackage = {
type,
id,
path: packagePath,
createdAt: new Date().toISOString(),
sources: refIds,
manifestPath,
sizeBytes: totalSize,
packageChecksum,
};
return archivePackage;
}
/**
* Verify integrity of archive package
*/
async verifyIntegrity(packagePath) {
const manifestPath = join(packagePath, 'manifest.json');
// Load manifest
let manifest;
try {
const data = await fs.readFile(manifestPath, 'utf-8');
manifest = JSON.parse(data);
}
catch (error) {
return {
valid: false,
verifiedAt: new Date().toISOString(),
files: [],
missingFiles: [],
extraFiles: [],
summary: 'Failed to load manifest',
};
}
const fileResults = [];
const missingFiles = [];
const expectedFiles = new Set();
// Verify each file in manifest
for (const entry of manifest.entries) {
expectedFiles.add(entry.packagePath);
try {
const actualChecksum = await this.computeChecksum(entry.packagePath);
const valid = actualChecksum === entry.checksum;
fileResults.push({
path: entry.packagePath,
expectedChecksum: entry.checksum,
actualChecksum,
valid,
});
}
catch (error) {
missingFiles.push(entry.packagePath);
fileResults.push({
path: entry.packagePath,
expectedChecksum: entry.checksum,
actualChecksum: '',
valid: false,
});
}
}
// Check for extra files
const actualFiles = await this.listFilesRecursive(join(packagePath, 'data'));
const extraFiles = actualFiles.filter((f) => !expectedFiles.has(f));
const allValid = fileResults.every((f) => f.valid) &&
missingFiles.length === 0 &&
extraFiles.length === 0;
const summary = allValid
? 'Package integrity verified'
: `Integrity check failed: ${fileResults.filter((f) => !f.valid).length} invalid, ${missingFiles.length} missing, ${extraFiles.length} extra`;
return {
valid: allValid,
verifiedAt: new Date().toISOString(),
files: fileResults,
missingFiles,
extraFiles,
summary,
};
}
/**
* Export reproducibility package for workflow
*/
async exportReproducibilityPackage(workflowId) {
const exportPath = join(this.archiveDir, 'reproducibility', `${workflowId}-${Date.now()}.zip`);
await this.ensureDir(dirname(exportPath));
// Create a simple marker file (full implementation would create ZIP)
await fs.writeFile(exportPath.replace('.zip', '.txt'), `Reproducibility package for workflow ${workflowId}\nCreated: ${new Date().toISOString()}`, 'utf-8');
return exportPath;
}
/**
* Compute checksum of file
*/
async computeChecksum(filePath) {
const content = await fs.readFile(filePath);
const hash = createHash('sha256');
hash.update(content);
return hash.digest('hex');
}
/**
* Compute checksum of directory contents
*/
async computeDirectoryChecksum(dirPath) {
const files = await this.listFilesRecursive(dirPath);
files.sort(); // Ensure consistent ordering
const hash = createHash('sha256');
for (const file of files) {
const content = await fs.readFile(file);
hash.update(content);
}
return hash.digest('hex');
}
/**
* List all files recursively
*/
async listFilesRecursive(dirPath) {
const files = [];
try {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dirPath, entry.name);
if (entry.isDirectory()) {
const subFiles = await this.listFilesRecursive(fullPath);
files.push(...subFiles);
}
else {
files.push(fullPath);
}
}
}
catch (error) {
// Directory doesn't exist
}
return files;
}
/**
* Generate package ID
*/
generatePackageId(type) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
return `${type.toLowerCase()}-${timestamp}`;
}
/**
* Ensure directory exists
*/
async ensureDir(dir) {
try {
await fs.mkdir(dir, { recursive: true });
}
catch (error) {
if (error.code !== 'EEXIST') {
throw error;
}
}
}
}
//# sourceMappingURL=archival.js.map