Files
daily-notification-plugin/scripts/generate-contract-schema.js
Matthew Raymer 114843edd5 feat(contract): Add Contract v1 freeze with schema and validation
- Add contracts/NotificationContract.v1.ts (frozen TypeScript interface)
- Add contracts/notification.v1.schema.json (JSON Schema)
- Add contracts/notification.v1.hash (SHA-256 hash for integrity)
- Add scripts/generate-contract-schema.js (schema generator)
- Add scripts/check-contract.sh (validation script)

Contract v1 is now frozen as single source of truth for cross-platform
notification content. All platforms must conform to this contract.

See docs/0017-Daily-Notification-Contract-Freeze.md for details.
2025-11-12 11:23:43 +00:00

164 lines
4.3 KiB
JavaScript

#!/usr/bin/env node
/**
* generate-contract-schema.js
*
* Generates JSON Schema and SHA-256 hash from NotificationContract.v1.ts
*
* This script:
* 1. Parses the TypeScript contract file
* 2. Generates a JSON Schema
* 3. Computes SHA-256 hash of normalized schema
* 4. Writes schema.json and hash files
*
* Run: node scripts/generate-contract-schema.js
*
* @author Matthew Raymer
* @version 1.0.0
*/
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const CONTRACT_FILE = path.join(__dirname, '../contracts/NotificationContract.v1.ts');
const SCHEMA_FILE = path.join(__dirname, '../contracts/notification.v1.schema.json');
const HASH_FILE = path.join(__dirname, '../contracts/notification.v1.hash');
/**
* Generate JSON Schema from TypeScript interface
*
* This is a simplified schema generator. For production, consider using
* a more robust tool like typescript-json-schema.
*/
function generateSchema() {
// Core NotificationContent schema
const schema = {
$schema: 'http://json-schema.org/draft-07/schema#',
title: 'NotificationContent v1',
description: 'Contract v1 for notification content (FROZEN)',
type: 'object',
required: ['id', 'title', 'fetchTime'],
properties: {
id: {
type: 'string',
description: 'Unique identifier for this notification'
},
title: {
type: 'string',
description: 'Notification title (required)'
},
body: {
type: 'string',
description: 'Notification body text (optional)'
},
scheduledTime: {
type: 'number',
description: 'When this notification should be displayed (epoch ms, optional)',
minimum: 0
},
fetchTime: {
type: 'number',
description: 'When this content was fetched (epoch ms, required)',
minimum: 0
},
mediaUrl: {
type: 'string',
description: 'Optional image URL for rich notifications',
format: 'uri'
},
ttlSeconds: {
type: 'number',
description: 'Cache TTL in seconds (how long this content is valid)',
minimum: 0
},
dedupeKey: {
type: 'string',
description: 'Deduplication key (for idempotency)'
},
priority: {
type: 'string',
enum: ['min', 'low', 'default', 'high', 'max'],
description: 'Notification priority level'
},
metadata: {
type: 'object',
description: 'Additional metadata (opaque to plugin)',
additionalProperties: true
}
},
additionalProperties: false
};
return schema;
}
/**
* Normalize schema for hashing
*
* Removes formatting differences to ensure consistent hashing
* Sorts keys recursively for deterministic output
*/
function normalizeSchema(schema) {
// Deep sort all keys recursively
function sortKeys(obj) {
if (Array.isArray(obj)) {
return obj.map(sortKeys);
} else if (obj !== null && typeof obj === 'object') {
const sorted = {};
Object.keys(obj).sort().forEach(key => {
sorted[key] = sortKeys(obj[key]);
});
return sorted;
}
return obj;
}
return JSON.stringify(sortKeys(schema), null, 2);
}
/**
* Compute SHA-256 hash of normalized schema
*/
function computeHash(schema) {
const normalized = normalizeSchema(schema);
return crypto.createHash('sha256').update(normalized).digest('hex');
}
/**
* Main execution
*/
function main() {
console.log('📋 Generating contract schema and hash...');
// Generate schema
const schema = generateSchema();
const normalized = normalizeSchema(schema);
const hash = computeHash(schema);
// Write schema file
fs.writeFileSync(SCHEMA_FILE, normalized, 'utf8');
console.log(`✅ Schema written to ${SCHEMA_FILE}`);
// Write hash file
fs.writeFileSync(HASH_FILE, hash, 'utf8');
console.log(`✅ Hash written to ${HASH_FILE}`);
console.log(` Hash: ${hash}`);
// Verify contract file exists
if (!fs.existsSync(CONTRACT_FILE)) {
console.warn(`⚠️ Warning: Contract file not found: ${CONTRACT_FILE}`);
} else {
console.log(`✅ Contract file exists: ${CONTRACT_FILE}`);
}
console.log('\n📝 Contract v1 schema and hash generated successfully!');
}
if (require.main === module) {
main();
}
module.exports = { generateSchema, computeHash, normalizeSchema };