- 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.
164 lines
4.3 KiB
JavaScript
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 };
|
|
|