diff --git a/docs/agents/task-logs/weekly/2026-02-17_13-05-27_event-schema-agent_completed.md b/docs/agents/task-logs/weekly/2026-02-17_13-05-27_event-schema-agent_completed.md new file mode 100644 index 00000000..eb44aa31 --- /dev/null +++ b/docs/agents/task-logs/weekly/2026-02-17_13-05-27_event-schema-agent_completed.md @@ -0,0 +1,20 @@ +# Event Schema Validation + +**Agent:** event-schema-agent +**Date:** 2026-02-17 +**Status:** Completed + +## Summary +Created a validator harness `scripts/agent/validate-events.mjs` to check Nostr event builders against canonical schemas in `js/nostrEventSchemas.js`. + +## Validation Report +- **Total Tests:** 31 +- **Passed:** 31 +- **Failed:** 0 +- **Artifact:** `artifacts/validate-events-20260217.json` + +## Fixes Applied +- None required (all builders validated successfully). + +## Recommendations +- Run this validator regularly or integrate into CI. diff --git a/scripts/agent/validate-events.mjs b/scripts/agent/validate-events.mjs index bb9f3c61..c7ef8d89 100644 --- a/scripts/agent/validate-events.mjs +++ b/scripts/agent/validate-events.mjs @@ -1,319 +1,422 @@ -import { - getNostrEventSchema, - validateEventStructure, - buildVideoPostEvent, - buildHttpAuthEvent, - buildReportEvent, - buildGiftWrapEvent, - buildSealEvent, - buildChatMessageEvent, - buildVideoMirrorEvent, - buildRepostEvent, - buildShareEvent, - buildRelayListEvent, - buildDmRelayListEvent, - buildProfileMetadataEvent, - buildMuteListEvent, - buildDeletionEvent, - buildLegacyDirectMessageEvent, - buildDmAttachmentEvent, - buildDmReadReceiptEvent, - buildDmTypingIndicatorEvent, - buildViewEvent, - buildZapRequestEvent, - buildReactionEvent, - buildCommentEvent, - buildWatchHistoryEvent, - buildSubscriptionListEvent, - buildBlockListEvent, - buildHashtagPreferenceEvent, - buildAdminListEvent, - NOTE_TYPES, -} from "../../js/nostrEventSchemas.js"; -import { buildNip71VideoEvent } from "../../js/nostr/nip71.js"; -import fs from "fs"; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import * as schemas from '../../js/nostrEventSchemas.js'; +import { buildNip71VideoEvent } from '../../js/nostr/nip71.js'; -const REPORT_FILE = "artifacts/validate-events.json"; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); -// Valid 32-byte hex pubkey for testing -const TEST_PUBKEY = "0000000000000000000000000000000000000000000000000000000000000001"; -const TEST_EVENT_ID = "0000000000000000000000000000000000000000000000000000000000000002"; +// Mock window for compatibility +if (typeof window === 'undefined') { + global.window = {}; +} -function runValidation() { - const results = []; - const failures = []; +const results = []; - const runTest = (name, builder, input, expectedType) => { - try { - const event = builder(input); - const validation = validateEventStructure(expectedType, event); +const builders = { + ...schemas, + buildNip71VideoEvent +}; - const result = { - builder: name, - input: input, - event: event, - validation: { - status: validation.valid ? "PASS" : "FAIL", - failures: validation.errors.map(msg => ({ message: msg })) - } - }; - - results.push(result); - - if (!validation.valid) { - failures.push(result); - console.error(`❌ ${name} FAILED:`); - validation.errors.forEach(e => console.error(` - ${e}`)); - } else { - console.log(`✅ ${name} PASSED`); - } - } catch (e) { - console.error(`❌ ${name} CRASHED:`, e); - failures.push({ - builder: name, - input: input, - error: e.message, - validation: { status: "CRASH", failures: [{ message: e.message }] } - }); +function runTest(builderName, testName, args, expectedValid = true) { + try { + const builder = builders[builderName]; + if (typeof builder !== 'function') { + throw new Error(`Builder ${builderName} not found`); } - }; - console.log("Starting event schema validation..."); - - // 1. Video Post - runTest("buildVideoPostEvent", buildVideoPostEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - dTagValue: "video-id", - content: { - version: 3, - title: "My Video", - videoRootId: "video-id", - url: "https://example.com/video.mp4" + // Handle arguments: if args is an array, apply it. If it's an object, assume single argument (params). + // Exception: buildAdminListEvent takes (listKey, params). + let event; + if (Array.isArray(args)) { + event = builder(...args); + } else { + event = builder(args); } - }, NOTE_TYPES.VIDEO_POST); - // 2. HTTP Auth - runTest("buildHttpAuthEvent", buildHttpAuthEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - url: "https://example.com/login", - method: "GET" - }, NOTE_TYPES.HTTP_AUTH); - - // 3. Report - runTest("buildReportEvent", buildReportEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - eventId: TEST_EVENT_ID, - reportType: "nudity" - }, NOTE_TYPES.REPORT); - - // 4. Gift Wrap - runTest("buildGiftWrapEvent", buildGiftWrapEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - recipientPubkey: TEST_PUBKEY, - ciphertext: "encrypted-content" - }, NOTE_TYPES.GIFT_WRAP); - - // 5. Seal - runTest("buildSealEvent", buildSealEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - ciphertext: "encrypted-rumor" - }, NOTE_TYPES.SEAL); - - // 6. Chat Message - runTest("buildChatMessageEvent", buildChatMessageEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - recipientPubkey: TEST_PUBKEY, - content: "Hello world" - }, NOTE_TYPES.CHAT_MESSAGE); - - // 7. Video Mirror - runTest("buildVideoMirrorEvent", buildVideoMirrorEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - content: "https://example.com/video.mp4" - }, NOTE_TYPES.VIDEO_MIRROR); - - // 8. Repost - runTest("buildRepostEvent", buildRepostEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - eventId: TEST_EVENT_ID, - eventRelay: "wss://relay.example.com" - }, NOTE_TYPES.REPOST); - - // 9. Share - runTest("buildShareEvent", buildShareEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - content: "Check this out!", - video: { id: "video-id", pubkey: TEST_PUBKEY } - }, NOTE_TYPES.SHARE); - - // 10. Relay List - runTest("buildRelayListEvent", buildRelayListEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - relays: ["wss://relay1.com", { url: "wss://relay2.com", mode: "read" }] - }, NOTE_TYPES.RELAY_LIST); - - // 11. DM Relay List - runTest("buildDmRelayListEvent", buildDmRelayListEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - relays: ["wss://dm.relay.com"] - }, NOTE_TYPES.DM_RELAY_LIST); - - // 12. Profile Metadata - runTest("buildProfileMetadataEvent", buildProfileMetadataEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - metadata: { name: "Alice", about: "Bob" } - }, NOTE_TYPES.PROFILE_METADATA); - - // 13. Mute List - runTest("buildMuteListEvent", buildMuteListEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - pTags: [TEST_PUBKEY] - }, NOTE_TYPES.MUTE_LIST); - - // 14. Deletion - runTest("buildDeletionEvent", buildDeletionEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - eventIds: [TEST_EVENT_ID], - reason: "spam" - }, NOTE_TYPES.DELETION); - - // 15. Legacy DM - runTest("buildLegacyDirectMessageEvent", buildLegacyDirectMessageEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - recipientPubkey: TEST_PUBKEY, - ciphertext: "secret message" - }, NOTE_TYPES.LEGACY_DM); - - // 16. DM Attachment - runTest("buildDmAttachmentEvent", buildDmAttachmentEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - recipientPubkey: TEST_PUBKEY, - attachment: { x: "hash", url: "https://example.com/file.jpg" } - }, NOTE_TYPES.DM_ATTACHMENT); - - // 17. DM Read Receipt - runTest("buildDmReadReceiptEvent", buildDmReadReceiptEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - recipientPubkey: TEST_PUBKEY, - eventId: TEST_EVENT_ID - }, NOTE_TYPES.DM_READ_RECEIPT); - - // 18. DM Typing Indicator - runTest("buildDmTypingIndicatorEvent", buildDmTypingIndicatorEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - recipientPubkey: TEST_PUBKEY, - eventId: TEST_EVENT_ID - }, NOTE_TYPES.DM_TYPING); - - // 19. View Event - runTest("buildViewEvent", buildViewEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - pointerValue: "video-id", - dedupeTag: "random-dedupe" - }, NOTE_TYPES.VIEW_EVENT); - - // 20. Zap Request - runTest("buildZapRequestEvent", buildZapRequestEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - recipientPubkey: TEST_PUBKEY, - amountSats: 100, - lnurl: "lnurl1..." - }, NOTE_TYPES.ZAP_REQUEST); - - // 21. Reaction - runTest("buildReactionEvent", buildReactionEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - targetPointer: { type: "e", value: TEST_EVENT_ID }, - content: "+" - }, NOTE_TYPES.VIDEO_REACTION); - - // 22. Comment - runTest("buildCommentEvent", buildCommentEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - videoEventId: TEST_EVENT_ID, - content: "Nice video!" - }, NOTE_TYPES.VIDEO_COMMENT); - - // 23. Watch History - // Testing implicit monthIdentifier via default - runTest("buildWatchHistoryEvent (Default Month)", buildWatchHistoryEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - content: { "video-id": 1234567890 } - }, NOTE_TYPES.WATCH_HISTORY); - - // 24. Subscription List - runTest("buildSubscriptionListEvent", buildSubscriptionListEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - content: [["p", TEST_PUBKEY]] - }, NOTE_TYPES.SUBSCRIPTION_LIST); - - // 25. Block List - runTest("buildBlockListEvent", buildBlockListEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - content: [["p", TEST_PUBKEY]] - }, NOTE_TYPES.USER_BLOCK_LIST); - - // 26. Hashtag Preference - runTest("buildHashtagPreferenceEvent", buildHashtagPreferenceEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - content: { version: 1, interests: ["nostr"], disinterests: [] } - }, NOTE_TYPES.HASHTAG_PREFERENCES); - - // 27. Admin List (Moderation) - runTest("buildAdminListEvent (Moderation)", (input) => buildAdminListEvent("moderation", input), { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - hexPubkeys: [TEST_PUBKEY] - }, NOTE_TYPES.ADMIN_MODERATION_LIST); - - // 28. NIP-71 Video - runTest("buildNip71VideoEvent", buildNip71VideoEvent, { - pubkey: TEST_PUBKEY, - created_at: 1234567890, - title: "NIP-71 Test", - content: "Summary", - metadata: { - duration: 120, - alt: "Alt text" + if (!event) { + throw new Error(`Builder ${builderName} returned null/undefined`); } - }, NOTE_TYPES.NIP71_VIDEO); - // Write Report - fs.writeFileSync(REPORT_FILE, JSON.stringify(results, null, 2)); - console.log(`\nReport written to ${REPORT_FILE}`); + let type; + const builderToType = { + buildVideoPostEvent: schemas.NOTE_TYPES.VIDEO_POST, + buildVideoMirrorEvent: schemas.NOTE_TYPES.VIDEO_MIRROR, + buildRepostEvent: schemas.NOTE_TYPES.REPOST, + buildShareEvent: schemas.NOTE_TYPES.SHARE, + buildRelayListEvent: schemas.NOTE_TYPES.RELAY_LIST, + buildDmRelayListEvent: schemas.NOTE_TYPES.DM_RELAY_LIST, + buildProfileMetadataEvent: schemas.NOTE_TYPES.PROFILE_METADATA, + buildMuteListEvent: schemas.NOTE_TYPES.MUTE_LIST, + buildDeletionEvent: schemas.NOTE_TYPES.DELETION, + buildLegacyDirectMessageEvent: schemas.NOTE_TYPES.LEGACY_DM, + buildDmAttachmentEvent: schemas.NOTE_TYPES.DM_ATTACHMENT, + buildDmReadReceiptEvent: schemas.NOTE_TYPES.DM_READ_RECEIPT, + buildDmTypingIndicatorEvent: schemas.NOTE_TYPES.DM_TYPING, + buildViewEvent: schemas.NOTE_TYPES.VIEW_EVENT, + buildZapRequestEvent: schemas.NOTE_TYPES.ZAP_REQUEST, + buildReactionEvent: schemas.NOTE_TYPES.VIDEO_REACTION, + buildCommentEvent: schemas.NOTE_TYPES.VIDEO_COMMENT, + buildWatchHistoryEvent: schemas.NOTE_TYPES.WATCH_HISTORY, + buildSubscriptionListEvent: schemas.NOTE_TYPES.SUBSCRIPTION_LIST, + buildBlockListEvent: schemas.NOTE_TYPES.USER_BLOCK_LIST, + buildHashtagPreferenceEvent: schemas.NOTE_TYPES.HASHTAG_PREFERENCES, + buildAdminListEvent: schemas.NOTE_TYPES.ADMIN_MODERATION_LIST, // Default + buildGiftWrapEvent: schemas.NOTE_TYPES.GIFT_WRAP, + buildSealEvent: schemas.NOTE_TYPES.SEAL, + buildChatMessageEvent: schemas.NOTE_TYPES.CHAT_MESSAGE, + buildHttpAuthEvent: schemas.NOTE_TYPES.HTTP_AUTH, + buildReportEvent: schemas.NOTE_TYPES.REPORT, + buildNip71VideoEvent: schemas.NOTE_TYPES.NIP71_VIDEO // Default + }; - if (failures.length > 0) { - console.error(`\n⚠️ Found ${failures.length} validation failures.`); - process.exit(1); - } else { - console.log("\n✨ All validations passed!"); + type = builderToType[builderName]; + + // Special cases for type resolution + if (builderName === 'buildRepostEvent' && event.kind === 16) { + type = schemas.NOTE_TYPES.GENERIC_REPOST; + } + if (builderName === 'buildAdminListEvent') { + const listKey = Array.isArray(args) ? args[0] : args; // assuming first arg is listKey + if (listKey === 'moderation' || listKey === 'editors') type = schemas.NOTE_TYPES.ADMIN_MODERATION_LIST; + else if (listKey === 'whitelist') type = schemas.NOTE_TYPES.ADMIN_WHITELIST; + else if (listKey === 'blacklist') type = schemas.NOTE_TYPES.ADMIN_BLACKLIST; + } + if (builderName === 'buildNip71VideoEvent') { + if (event.kind === 22) type = schemas.NOTE_TYPES.NIP71_SHORT_VIDEO; + else type = schemas.NOTE_TYPES.NIP71_VIDEO; + } + + const validation = schemas.validateEventStructure(type, event); + + results.push({ + builder: builderName, + testName, + inputs: args, + event, + valid: validation.valid === expectedValid, + validationResult: validation, + expectedValid + }); + + } catch (error) { + results.push({ + builder: builderName, + testName, + inputs: args, + error: error.message, + stack: error.stack, + valid: false + }); } } -runValidation(); +// --- Test Cases --- + +const PUBKEY = '0000000000000000000000000000000000000000000000000000000000000001'; +const CREATED_AT = 1700000000; + +// Video Post +runTest('buildVideoPostEvent', 'Basic Video Post', { + pubkey: PUBKEY, + created_at: CREATED_AT, + dTagValue: 'video-1', + content: { + version: 3, + title: 'My Video', + videoRootId: 'video-1', + url: 'https://example.com/video.mp4' + } +}); + +runTest('buildVideoPostEvent', 'Video Post with Magnet', { + pubkey: PUBKEY, + created_at: CREATED_AT, + dTagValue: 'video-2', + content: { + version: 3, + title: 'My Magnet Video', + videoRootId: 'video-2', + magnet: 'magnet:?xt=urn:btih:1234567890abcdef1234567890abcdef12345678&dn=video' + } +}); + +// Video Mirror +runTest('buildVideoMirrorEvent', 'Video Mirror', { + pubkey: PUBKEY, + created_at: CREATED_AT, + tags: [['url', 'https://example.com/mirror.mp4'], ['m', 'video/mp4']], + content: 'Alt text' +}); + +// Repost +runTest('buildRepostEvent', 'Repost Event', { + pubkey: PUBKEY, + created_at: CREATED_AT, + eventId: '1111111111111111111111111111111111111111111111111111111111111111', + eventRelay: 'wss://relay.example.com' +}); + +runTest('buildRepostEvent', 'Generic Repost (Kind 16)', { + pubkey: PUBKEY, + created_at: CREATED_AT, + targetKind: 30078, + serializedEvent: JSON.stringify({ kind: 30078, tags: [], content: '{}' }) +}); + +// Share +runTest('buildShareEvent', 'Share Event', { + pubkey: PUBKEY, + created_at: CREATED_AT, + content: 'Check this out!', + video: { id: '2222222222222222222222222222222222222222222222222222222222222222', pubkey: PUBKEY }, + relays: ['wss://relay.example.com'] +}); + +// Relay List +runTest('buildRelayListEvent', 'Relay List', { + pubkey: PUBKEY, + created_at: CREATED_AT, + relays: [ + { url: 'wss://relay1.com', read: true, write: true }, + { url: 'wss://relay2.com', read: true, write: false } + ] +}); + +// DM Relay List +runTest('buildDmRelayListEvent', 'DM Relay List', { + pubkey: PUBKEY, + created_at: CREATED_AT, + relays: ['wss://relay.dm.com'] +}); + +// Profile Metadata +runTest('buildProfileMetadataEvent', 'Profile Metadata', { + pubkey: PUBKEY, + created_at: CREATED_AT, + metadata: { name: 'Alice', about: 'Hello' } +}); + +// Mute List +runTest('buildMuteListEvent', 'Mute List', { + pubkey: PUBKEY, + created_at: CREATED_AT, + pTags: [PUBKEY], + content: '{ "blockedPubkeys": [] }', + encrypted: true, + encryptionTag: 'nip44_v2' +}); + +// Deletion +runTest('buildDeletionEvent', 'Deletion', { + pubkey: PUBKEY, + created_at: CREATED_AT, + eventIds: ['3333333333333333333333333333333333333333333333333333333333333333'], + reason: 'Mistake' +}); + +// Legacy DM +runTest('buildLegacyDirectMessageEvent', 'Legacy DM', { + pubkey: PUBKEY, + created_at: CREATED_AT, + recipientPubkey: PUBKEY, + ciphertext: 'encrypted_content' +}); + +// DM Attachment +runTest('buildDmAttachmentEvent', 'DM Attachment', { + pubkey: PUBKEY, + created_at: CREATED_AT, + recipientPubkey: PUBKEY, + attachment: { url: 'https://example.com/file.jpg', x: 'hash', type: 'image/jpeg' } +}); + +// DM Read Receipt +runTest('buildDmReadReceiptEvent', 'DM Read Receipt', { + pubkey: PUBKEY, + created_at: CREATED_AT, + recipientPubkey: PUBKEY, + eventId: '4444444444444444444444444444444444444444444444444444444444444444' +}); + +// DM Typing +runTest('buildDmTypingIndicatorEvent', 'DM Typing', { + pubkey: PUBKEY, + created_at: CREATED_AT, + recipientPubkey: PUBKEY, + expiresAt: CREATED_AT + 60 +}); + +// View Event +runTest('buildViewEvent', 'View Event', { + pubkey: PUBKEY, + created_at: CREATED_AT, + pointerValue: 'video-1', + dedupeTag: 'session-1', + includeSessionTag: true +}); + +// Zap Request +runTest('buildZapRequestEvent', 'Zap Request', { + pubkey: PUBKEY, + created_at: CREATED_AT, + recipientPubkey: PUBKEY, + amountSats: 100, + relays: ['wss://relay.zap.com'] +}); + +// Reaction +runTest('buildReactionEvent', 'Reaction', { + pubkey: PUBKEY, + created_at: CREATED_AT, + targetPointer: { type: 'e', value: '5555555555555555555555555555555555555555555555555555555555555555' }, + content: '+' +}); + +// Comment +runTest('buildCommentEvent', 'Comment', { + pubkey: PUBKEY, + created_at: CREATED_AT, + content: 'Nice video!', + videoEventId: '6666666666666666666666666666666666666666666666666666666666666666', + rootKind: '30078' +}); + +// Watch History +runTest('buildWatchHistoryEvent', 'Watch History', { + pubkey: PUBKEY, + created_at: CREATED_AT, + monthIdentifier: '2023-11', + content: { version: 2, month: '2023-11', items: [] } +}); + +// Subscription List +runTest('buildSubscriptionListEvent', 'Subscription List', { + pubkey: PUBKEY, + created_at: CREATED_AT, + content: 'encrypted_subscriptions', + encryption: 'nip44_v2' +}); + +// Block List +runTest('buildBlockListEvent', 'Block List', { + pubkey: PUBKEY, + created_at: CREATED_AT, + content: 'encrypted_blocks', + encryption: 'nip44_v2' +}); + +// Hashtag Preferences +runTest('buildHashtagPreferenceEvent', 'Hashtag Preferences', { + pubkey: PUBKEY, + created_at: CREATED_AT, + content: 'encrypted_preferences' +}); + +// Admin Lists +runTest('buildAdminListEvent', 'Admin Moderation List', ['moderation', { + pubkey: PUBKEY, + created_at: CREATED_AT, + hexPubkeys: [PUBKEY] +}]); + +runTest('buildAdminListEvent', 'Admin Whitelist', ['whitelist', { + pubkey: PUBKEY, + created_at: CREATED_AT, + hexPubkeys: [PUBKEY] +}]); + +// Gift Wrap +runTest('buildGiftWrapEvent', 'Gift Wrap', { + pubkey: PUBKEY, + created_at: CREATED_AT, + recipientPubkey: PUBKEY, + ciphertext: 'sealed_message' +}); + +// Seal +runTest('buildSealEvent', 'Seal', { + pubkey: PUBKEY, + created_at: CREATED_AT, + ciphertext: 'rumor_message' +}); + +// Chat Message +runTest('buildChatMessageEvent', 'Chat Message', { + pubkey: PUBKEY, + created_at: CREATED_AT, + recipientPubkey: PUBKEY, + content: 'Hello' +}); + +// HTTP Auth +runTest('buildHttpAuthEvent', 'HTTP Auth', { + pubkey: PUBKEY, + created_at: CREATED_AT, + url: 'https://example.com/auth', + method: 'GET' +}); + +// Report +runTest('buildReportEvent', 'Report', { + pubkey: PUBKEY, + created_at: CREATED_AT, + eventId: '7777777777777777777777777777777777777777777777777777777777777777', + reportType: 'spam' +}); + +// NIP-71 Video +runTest('buildNip71VideoEvent', 'NIP-71 Video', { + metadata: { + title: 'My NIP-71 Video', + summary: 'Summary', + kind: 21, + publishedAt: 1700000000 + }, + pubkey: PUBKEY, + title: 'My NIP-71 Video', + created_at: CREATED_AT +}); + +// --- CLI Handling --- + +const args = process.argv.slice(2); +let outFile = 'artifacts/validate-events-latest.json'; +const dryRun = args.includes('--dry-run'); + +args.forEach(arg => { + if (arg.startsWith('--out=')) { + outFile = arg.split('=')[1]; + } +}); + +// Summary +const total = results.length; +const passed = results.filter(r => r.valid).length; +const failed = total - passed; + +console.log(`\nValidation Summary: ${passed}/${total} passed`); + +if (failed > 0) { + console.log(`⚠️ ${failed} failures found:`); + results.filter(r => !r.valid).forEach(r => { + console.log(`\n[FAIL] ${r.builder}: ${r.testName}`); + if (r.error) { + console.log(` Error: ${r.error}`); + } else if (r.validationResult && r.validationResult.errors) { + r.validationResult.errors.forEach(e => console.log(` Schema Error: ${e}`)); + } + }); +} else { + console.log('✓ All builders validated successfully.'); +} + +if (!dryRun) { + const dir = path.dirname(outFile); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(outFile, JSON.stringify(results, null, 2)); + console.log(`\nReport written to ${outFile}`); +}