From f7cb02089544e4faf31589d5f38c007ef8362891 Mon Sep 17 00:00:00 2001 From: ravendevhub Date: Sat, 29 Aug 2026 13:12:32 +0630 Subject: [PATCH] ci(docs): add contract event documentation drift validator (#714) - Create scripts/validate-contract-events.py extracting contractevent structs and cross-referencing docs - Catalog all 34 public Soroban contract events in docs/CONTRACT_EVENTS.md - Add GitHub Actions CI workflow to detect event documentation drift on PRs --- .../workflows/validate-contract-events.yml | 38 ++++ docs/CONTRACT_EVENTS.md | 196 ++++++++++++++++++ scripts/validate-contract-events.py | 97 +++++++++ 3 files changed, 331 insertions(+) create mode 100644 .github/workflows/validate-contract-events.yml create mode 100644 docs/CONTRACT_EVENTS.md create mode 100755 scripts/validate-contract-events.py diff --git a/.github/workflows/validate-contract-events.yml b/.github/workflows/validate-contract-events.yml new file mode 100644 index 00000000..7f1d91be --- /dev/null +++ b/.github/workflows/validate-contract-events.yml @@ -0,0 +1,38 @@ +name: Contract Event Documentation Drift Check + +on: + pull_request: + paths: + - "contract/**/events.rs" + - "docs/CONTRACT_EVENTS.md" + - "scripts/validate-contract-events.py" + push: + branches: [main] + paths: + - "contract/**/events.rs" + - "docs/CONTRACT_EVENTS.md" + +concurrency: + group: event-docs-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + validate-events: + name: check-event-documentation-drift + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Validate Contract Event Documentation Drift + run: | + python3 scripts/validate-contract-events.py diff --git a/docs/CONTRACT_EVENTS.md b/docs/CONTRACT_EVENTS.md new file mode 100644 index 00000000..3f2c3a5c --- /dev/null +++ b/docs/CONTRACT_EVENTS.md @@ -0,0 +1,196 @@ +# šŸ“” Soroban Smart Contract Event Catalog + +This document defines the comprehensive event taxonomy emitted by the NotifyChain Soroban smart contracts (Issue #714). + +--- + +## 1. Event Category & Priority Topics + +All events publish indexed `NotificationCategory` and `NotificationPriority` topics for off-chain listener filtering: + +* **Category**: `Group (0)`, `Admin (1)`, `Financial (2)`, `Notification (3)`, `System (4)` +* **Priority**: `Low (0)`, `Medium (1)`, `High (2)`, `Critical (3)` + +--- + +## 2. Event Taxonomy + +### `AutoshareCreated` +Emitted when a new AutoShare group is created. +* **Topics**: `creator: Address`, `category: NotificationCategory`, `priority: NotificationPriority` +* **Data**: `id: BytesN<32>` + +### `AutoshareUpdated` +Emitted when an AutoShare group's member list or parameters are modified. +* **Topics**: `updater: Address`, `category: NotificationCategory`, `priority: NotificationPriority` +* **Data**: `id: BytesN<32>` + +### `GroupDeactivated` +Emitted when an AutoShare group is deactivated. +* **Topics**: `creator: Address`, `category: NotificationCategory`, `priority: NotificationPriority` +* **Data**: `id: BytesN<32>` + +### `GroupActivated` +Emitted when an AutoShare group is re-activated. +* **Topics**: `creator: Address`, `category: NotificationCategory`, `priority: NotificationPriority` +* **Data**: `id: BytesN<32>` + +### `CategoryRegistered` +Emitted when a new notification category is registered. +* **Topics**: `admin: Address`, `category: NotificationCategory`, `priority: NotificationPriority` +* **Data**: `category_id: u32` + +### `ChannelPreferenceUpdated` +Emitted when user notification channel preferences are updated. +* **Topics**: `user: Address`, `category: NotificationCategory`, `priority: NotificationPriority` +* **Data**: `channel_id: BytesN<32>` + +### `ContractPaused` +Emitted when an administrator pauses the contract. +* **Topics**: `admin: Address`, `category: NotificationCategory`, `priority: NotificationPriority` +* **Data**: `paused_at: u64` + +### `ContractUnpaused` +Emitted when an administrator unpauses the contract. +* **Topics**: `admin: Address`, `category: NotificationCategory`, `priority: NotificationPriority` +* **Data**: `unpaused_at: u64` + +### `AdminTransferred` +Emitted when administrative ownership is transferred. +* **Topics**: `old_admin: Address`, `new_admin: Address`, `category: NotificationCategory`, `priority: NotificationPriority` +* **Data**: `timestamp: u64` + +### `AuthorizationFailure` +Emitted upon unauthorized invocation attempts. +* **Topics**: `caller: Address`, `category: NotificationCategory`, `priority: NotificationPriority` +* **Data**: `action: String` + +### `Withdrawal` +Emitted when protocol funds are withdrawn. +* **Topics**: `recipient: Address`, `category: NotificationCategory`, `priority: NotificationPriority` +* **Data**: `amount: i128` + +### `NotificationScheduled` +Emitted when a notification is queued for delivery. +* **Topics**: `sender: Address`, `category: NotificationCategory`, `priority: NotificationPriority` +* **Data**: `notification_id: BytesN<32>` + +### `NotificationExpired` +Emitted when a notification exceeds its TTL without acknowledgment. +* **Topics**: `category: NotificationCategory`, `priority: NotificationPriority` +* **Data**: `notification_id: BytesN<32>` + +### `ScheduledNotificationCancelled` +Emitted when a scheduled notification is cancelled prior to delivery. +* **Topics**: `sender: Address`, `category: NotificationCategory`, `priority: NotificationPriority` +* **Data**: `notification_id: BytesN<32>` + +### `NotificationDelivered` +Emitted when a notification is successfully received by off-chain endpoints. +* **Topics**: `recipient: Address`, `category: NotificationCategory`, `priority: NotificationPriority` +* **Data**: `notification_id: BytesN<32>` + +### `NotificationRecalled` +Emitted when a pending notification is recalled by sender. +* **Topics**: `sender: Address`, `category: NotificationCategory`, `priority: NotificationPriority` +* **Data**: `notification_id: BytesN<32>` + +### `NotificationRevoked` +Emitted when a delivered or pending notification is revoked. +* **Topics**: `revoker: Address`, `category: NotificationCategory`, `priority: NotificationPriority` +* **Data**: `notification_id: BytesN<32>` + +### `NotificationExtended` +Emitted when a notification lifetime is extended. +* **Topics**: `updater: Address`, `category: NotificationCategory`, `priority: NotificationPriority` +* **Data**: `notification_id: BytesN<32>` + +### `NotificationAcknowledged` +Emitted when a recipient acknowledges receipt. +* **Topics**: `recipient: Address`, `category: NotificationCategory`, `priority: NotificationPriority` +* **Data**: `notification_id: BytesN<32>` + +### `SubscriptionCancelled` +Emitted when a channel subscription is cancelled. +* **Topics**: `subscriber: Address`, `category: NotificationCategory`, `priority: NotificationPriority` +* **Data**: `channel_id: BytesN<32>` + +### `BatchNotificationsCreated` +Emitted when a batch of notifications is dispatched. +* **Topics**: `sender: Address`, `category: NotificationCategory`, `priority: NotificationPriority` +* **Data**: `count: u32` + +### `BatchProcessingCompleted` +Emitted when batch event processing finishes. +* **Topics**: `category: NotificationCategory`, `priority: NotificationPriority` +* **Data**: `processed_count: u32` + +### `AuditRecordAppended` +Emitted when an immutable audit trail entry is logged. +* **Topics**: `category: NotificationCategory`, `priority: NotificationPriority` +* **Data**: `record_id: u64` + +### `OwnershipTransferInitiated` +Emitted when contract ownership transfer is queued. +* **Topics**: `current_owner: Address`, `pending_owner: Address` +* **Data**: `timestamp: u64` + +### `OwnershipTransferred` +Emitted when contract ownership transfer is completed. +* **Topics**: `new_owner: Address` +* **Data**: `timestamp: u64` + +### `ReputationUpdated` +Emitted when an account reputation score is updated. +* **Topics**: `account: Address` +* **Data**: `new_score: i128` + +### `NotificationLimitsConfigured` +Emitted when global rate limits are configured. +* **Topics**: `admin: Address` +* **Data**: `limit: u32` + +### `ReputationTierChanged` +Emitted when user reputation tier advances. +* **Topics**: `account: Address` +* **Data**: `tier: u32` + +### `SchemaVersionSet` +Emitted when protocol schema version is updated. +* **Topics**: `version: u32` +* **Data**: `timestamp: u64` + +### `NotificationAccessed` +Emitted upon reading secured notification content. +* **Topics**: `reader: Address` +* **Data**: `notification_id: BytesN<32>` + +### `ChannelMetadataUpdated` +Emitted when channel metadata is modified. +* **Topics**: `channel_id: BytesN<32>` +* **Data**: `metadata_uri: String` + +### `NotificationArchived` +Emitted when historical notifications are archived. +* **Topics**: `category: NotificationCategory` +* **Data**: `count: u32` + +### `TemplateRegistered` +Emitted when a notification template is created. +* **Topics**: `template_id: BytesN<32>` +* **Data**: `name: String` + +### `TemplateUpdated` +Emitted when a notification template is updated. +* **Topics**: `template_id: BytesN<32>` +* **Data**: `name: String` + +--- + +## 3. Drift Validation + +Run the documentation drift validation script: + +```bash +python3 scripts/validate-contract-events.py +``` diff --git a/scripts/validate-contract-events.py b/scripts/validate-contract-events.py new file mode 100755 index 00000000..ee20308b --- /dev/null +++ b/scripts/validate-contract-events.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +""" +Contract Event Documentation Drift Validator (Issue #714) + +Extracts #[contractevent] structs from Soroban Rust sources and validates them +against the documented event catalog in docs/CONTRACT_EVENTS.md. +""" + +import os +import re +import sys +import argparse +from pathlib import Path + +def extract_code_events(events_rs_path): + events = {} + if not events_rs_path.exists(): + return events + + content = events_rs_path.read_text(encoding='utf-8') + + # Regex matching #[contractevent...] pub struct EventName { ... } + struct_matches = re.finditer(r'(?:#\[contractevent[^\]]*\]\s*)+(?:#\[derive[^\]]*\]\s*)*pub struct\s+([A-Za-z0-9_]+)\s*\{([^}]+)\}', content) + + for match in struct_matches: + event_name = match.group(1) + body = match.group(2) + fields = [] + topics = [] + + for line in body.splitlines(): + line = line.strip() + if line.startswith('pub '): + # pub field_name: Type, + field_part = line.replace('pub ', '').split(':')[0].strip() + fields.append(field_part) + + events[event_name] = { + 'fields': fields, + } + + return events + +def extract_documented_events(docs_path): + documented = set() + if not docs_path.exists(): + return documented + + content = docs_path.read_text(encoding='utf-8') + # Match headers like `### AutoshareCreated` or `## AutoshareCreated` + matches = re.findall(r'###?\s+`?([A-Z][a-zA-Z0-9_]+)`?', content) + for m in matches: + documented.add(m) + + return documented + +def main(): + parser = argparse.ArgumentParser(description="Validate contract events against documentation.") + parser.add_argument("--warn-only", action="store_true", help="Advisory mode") + args = parser.parse_args() + + repo_root = Path(__file__).resolve().parent.parent + events_rs = repo_root / 'contract/contracts/hello-world/src/base/events.rs' + docs_md = repo_root / 'docs/CONTRACT_EVENTS.md' + + print(f"šŸ” Validating Contract Event Documentation Drift...\n") + print(f" Source: {events_rs}") + print(f" Docs: {docs_md}\n") + + code_events = extract_code_events(events_rs) + doc_events = extract_documented_events(docs_md) + + print(f"Found {len(code_events)} #[contractevent] structs in Rust implementation.") + print(f"Found {len(doc_events)} documented events in {docs_md.name}.\n") + + missing_in_docs = [] + for event_name in sorted(code_events.keys()): + if event_name not in doc_events: + missing_in_docs.append(event_name) + + if missing_in_docs: + print(f"āŒ Documentation Drift Detected! The following {len(missing_in_docs)} events are missing from docs:") + for name in missing_in_docs: + print(f" • {name} (Fields: {', '.join(code_events[name]['fields'])})") + + if args.warn_only: + print("\nāš ļø Exiting 0 (warn-only mode enabled).") + sys.exit(0) + else: + print("\nāŒ Please update docs/CONTRACT_EVENTS.md to resolve documentation drift.") + sys.exit(1) + else: + print("āœ… All contract events are accurately documented with zero drift!") + sys.exit(0) + +if __name__ == '__main__': + main()