DocumentWDP-9c
Version0.1.0-draft
StatusDraft
CategoryINFORMATIVE (Non-Normative)
Updated2025-12-25

WDP Part 9c: Implementation Guide

Practical implementation guidance for the Waddling Diagnostic Protocol.

Abstract#

This document provides practical implementation guidance for the Waddling Diagnostic Protocol (WDP) catalog system. It includes code examples for generating catalogs, loading them on clients, and expanding compact diagnostics. This document is INFORMATIVE and supplements the normative requirements in Part 9a (Catalog Format) and Part 9b (Wire Protocol).

Status of This Memo#

This document specifies an INFORMATIVE implementation guide for the Waddling Diagnostic Protocol (WDP). Distribution of this document is unlimited.

This document provides non-normative guidance. Implementations MAY deviate from the patterns shown here while still conforming to the normative requirements in Parts 9a and 9b.

Requirements Language#

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.

Note: As an INFORMATIVE document, requirements keywords in this specification describe recommended practices rather than mandatory conformance requirements.

1. Introduction#

1.1. Purpose#

This implementation guide provides practical examples and patterns for implementing the WDP catalog system. While Parts 9a and 9b define WHAT implementations must do, this document shows HOW implementations might achieve conformance.

1.2. Scope#

This document covers:

  • Server-side catalog generation
  • Client-side catalog loading and expansion
  • Testing strategies
  • Best practices for production deployments

1.3. Implementation Checklist#

1.3.1. Server-Side#

Implementations SHOULD address the following:

TaskStatus
Create error registry (source of truth)☐
Generate CompactIDs using xxHash3 with seed 0x000031762D706477☐
Build catalog JSON (Full, Compact, or Minimal format)☐
Host catalog on CDN or static server☐
Generate diagnostics in wire protocol format☐

1.3.2. Client-Side#

Implementations SHOULD address the following:

TaskStatus
Download and cache catalog☐
Parse diagnostic responses☐
Expand CompactIDs using catalog☐
Interpolate fields into messages☐
Display with appropriate severity styling☐

1.4. Architecture Overview#

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Error Registry  β”‚  (Source code, config)
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Catalog Generatorβ”‚  (Build-time tool)
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  catalog.json    β”‚  (Artifact)
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   CDN / Server   β”‚  (Distribution)
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Client Cache   β”‚ ◄──► β”‚   Server API     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚                         β”‚
         β”‚   {"xY9Kp":{"f":{...}}} β”‚
         β”‚ β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Expanded Message β”‚
β”‚ "Token expired…" β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

2. Server-Side Implementation#

2.1. Hash Algorithm Requirements#

Per Part 5, all WDP implementations MUST use xxHash3 for CompactID generation:

  • Algorithm: xxHash3 (64-bit variant)
  • Code Hash Seed: 0x000031762D706477 (ASCII: "wdp-v1\0\0")
  • Namespace Hash Seed: 0x31762D736E706477 (ASCII: "wdpns-v1")
  • Output: 5-character Base62 string

2.2. Catalog Generation (Rust)#

Rust
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::HashMap;
use xxhash_rust::xxh3::xxh3_64_with_seed;

/// WDP-mandated seed for code hashing (Part 5)
const WDP_CODE_SEED: u64 = 0x000031762D706477;  // "wdp-v1\0\0"

/// WDP-mandated seed for namespace hashing (Part 7)
const WDP_NAMESPACE_SEED: u64 = 0x31762D736E706477;  // "wdpns-v1"

/// Compute CompactID using xxHash3 with WDP seed
fn compute_compact_id(code: &str) -> String {
    let hash = xxh3_64_with_seed(code.as_bytes(), WDP_CODE_SEED);
    base62_encode(hash, 5)
}

/// Compute namespace hash using xxHash3 with namespace seed
fn compute_namespace_hash(namespace: &str) -> String {
    let hash = xxh3_64_with_seed(namespace.as_bytes(), WDP_NAMESPACE_SEED);
    base62_encode(hash, 5)
}

fn base62_encode(mut num: u64, len: usize) -> String {
    const CHARS: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    let mut result = String::new();
    
    while num > 0 || result.is_empty() {
        result.insert(0, CHARS[(num % 62) as usize] as char);
        num /= 62;
    }
    
    // Pad to required length
    while result.len() < len {
        result.insert(0, '0');
    }
    
    result.chars().take(len).collect()
}

#[derive(Serialize, Deserialize)]
struct ErrorEntry {
    code: String,
    severity: String,
    message: String,
    description: Option<String>,
    hints: Vec<String>,
    tags: Vec<String>,
    fields: Vec<String>,
}

struct CatalogGenerator {
    version: String,
    namespace: Option<String>,
    namespace_hash: Option<String>,
    diags: HashMap<String, ErrorEntry>,
}

impl CatalogGenerator {
    fn new(version: &str) -> Self {
        Self {
            version: version.to_string(),
            namespace: None,
            namespace_hash: None,
            diags: HashMap::new(),
        }
    }
    
    /// Create single-namespace catalog with optional namespace metadata
    fn with_namespace(mut self, namespace: &str) -> Self {
        self.namespace_hash = Some(compute_namespace_hash(namespace));
        self.namespace = Some(namespace.to_string());
        self
    }
    
    fn add_error(&mut self, entry: ErrorEntry) {
        let compact_id = compute_compact_id(&entry.code);
        self.diags.insert(compact_id, entry);
    }
    
    fn generate_full(&self) -> String {
        let mut catalog = json!({
            "version": self.version,
            "diags": self.diags
        });
        
        // Add optional namespace fields
        if let Some(ns) = &self.namespace {
            catalog["namespace"] = json!(ns);
        }
        if let Some(ns_hash) = &self.namespace_hash {
            catalog["namespace_hash"] = json!(ns_hash);
        }
        
        serde_json::to_string_pretty(&catalog).unwrap()
    }
    
    fn generate_compact(&self) -> String {
        let mut compact_diags = serde_json::Map::new();
        
        for (hash, entry) in &self.diags {
            compact_diags.insert(hash.clone(), json!({
                "c": entry.code,
                "s": entry.severity,
                "m": entry.message,
                "d": entry.description,
                "h": entry.hints,
                "t": entry.tags,
                "f": entry.fields,
            }));
        }
        
        let mut catalog = json!({
            "v": self.version,
            "wd": compact_diags
        });
        
        // Add optional namespace fields
        if let Some(ns) = &self.namespace {
            catalog["ns"] = json!(ns);
        }
        if let Some(ns_hash) = &self.namespace_hash {
            catalog["nh"] = json!(ns_hash);
        }
        
        serde_json::to_string(&catalog).unwrap()
    }
}

// Usage
fn main() {
    let mut generator = CatalogGenerator::new("1.0.0")
        .with_namespace("auth_service");
    
    generator.add_error(ErrorEntry {
        code: "E.AUTH.TOKEN.001".to_string(),
        severity: "E".to_string(),
        message: "Token missing from Authorization header".to_string(),
        description: Some("The JWT token was not provided.".to_string()),
        hints: vec!["Include header: Authorization: Bearer <token>".to_string()],
        tags: vec!["auth".to_string(), "security".to_string()],
        fields: vec![],
    });
    
    // Generate single-namespace catalog
    let catalog_json = generator.generate_compact();
    std::fs::write("catalog.json", catalog_json).unwrap();
}

2.3. Catalog Generation (Python)#

Python
import json
from typing import Dict, List, Optional
from dataclasses import dataclass, asdict
import xxhash

# WDP-mandated seeds (Part 5, Part 7)
WDP_CODE_SEED = 0x000031762D706477      # "wdp-v1\0\0"
WDP_NAMESPACE_SEED = 0x31762D736E706477  # "wdpns-v1"

def compute_compact_id(code: str) -> str:
    """Generate 5-character Base62 CompactID using xxHash3."""
    hash_val = xxhash.xxh3_64(code.encode('utf-8'), seed=WDP_CODE_SEED).intdigest()
    return base62_encode(hash_val)[:5].zfill(5)

def compute_namespace_hash(namespace: str) -> str:
    """Generate 5-character Base62 hash for namespace using xxHash3."""
    hash_val = xxhash.xxh3_64(namespace.encode('utf-8'), seed=WDP_NAMESPACE_SEED).intdigest()
    return base62_encode(hash_val)[:5].zfill(5)

def base62_encode(num: int) -> str:
    """Encode number as Base62 string."""
    chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
    if num == 0:
        return "00000"
    
    result = ""
    while num > 0:
        result = chars[num % 62] + result
        num //= 62
    
    return result.zfill(5)[:5]

@dataclass
class ErrorEntry:
    code: str
    severity: str
    message: str
    description: Optional[str] = None
    hints: List[str] = None
    tags: List[str] = None
    fields: List[str] = None
    
    def __post_init__(self):
        self.hints = self.hints or []
        self.tags = self.tags or []
        self.fields = self.fields or []

class CatalogGenerator:
    def __init__(self, version: str, namespace: Optional[str] = None):
        self.version = version
        self.namespace = namespace
        self.namespace_hash = compute_namespace_hash(namespace) if namespace else None
        self.diags: Dict[str, ErrorEntry] = {}
    
    def add_error(self, entry: ErrorEntry):
        compact_id = compute_compact_id(entry.code)
        self.diags[compact_id] = entry
    
    def generate_full(self) -> str:
        catalog = {
            "version": self.version,
            "diags": {
                cid: asdict(entry) 
                for cid, entry in self.diags.items()
            }
        }
        
        if self.namespace:
            catalog["namespace"] = self.namespace
        if self.namespace_hash:
            catalog["namespace_hash"] = self.namespace_hash
        
        return json.dumps(catalog, indent=2)
    
    def generate_compact(self) -> str:
        compact_diags = {}
        for cid, entry in self.diags.items():
            compact_diags[cid] = {
                "c": entry.code,
                "s": entry.severity,
                "m": entry.message,
                "d": entry.description,
                "h": entry.hints,
                "t": entry.tags,
                "f": entry.fields
            }
        
        catalog = {"v": self.version, "wd": compact_diags}
        
        if self.namespace:
            catalog["ns"] = self.namespace
        if self.namespace_hash:
            catalog["nh"] = self.namespace_hash
        
        return json.dumps(catalog, separators=(',', ':'))

# Usage
if __name__ == "__main__":
    generator = CatalogGenerator("1.0.0", namespace="auth_service")
    
    generator.add_error(ErrorEntry(
        code="E.AUTH.TOKEN.001",
        severity="E",
        message="Token missing from Authorization header",
        description="The JWT token was not provided.",
        hints=["Include header: Authorization: Bearer <token>"],
        tags=["auth", "security"],
        fields=[]
    ))
    
    catalog_json = generator.generate_compact()
    with open("catalog.json", "w") as f:
        f.write(catalog_json)

2.4. Generating Diagnostic Responses#

Typescript
// TypeScript - Server-side diagnostic response generation

interface Diagnostic {
  compactId: string;  // CompactID (5 chars) or CombinedID (11 chars)
  fields?: Record<string, any>;
}

// WDP-mandated seeds
const WDP_CODE_SEED = 0x000031762D706477n;
const WDP_NAMESPACE_SEED = 0x31762D736E706477n;

class DiagnosticResponse {
  /**
   * Generate WDP diagnostic response (standalone format)
   * Use when diagnostics are the only content
   */
  static standalone(diagnostics: Diagnostic[]): object {
    const response: Record<string, any> = {};
    
    for (const diag of diagnostics) {
      if (diag.fields && Object.keys(diag.fields).length > 0) {
        response[diag.compactId] = { f: diag.fields };
      } else {
        response[diag.compactId] = {};
      }
    }
    
    return response;
  }
  
  /**
   * Generate WDP diagnostic response with application data
   * Use "wd" wrapper to avoid key conflicts
   */
  static withData(data: object, diagnostics: Diagnostic[]): object {
    const wd: Record<string, any> = {};
    
    for (const diag of diagnostics) {
      if (diag.fields && Object.keys(diag.fields).length > 0) {
        wd[diag.compactId] = { f: diag.fields };
      } else {
        wd[diag.compactId] = {};
      }
    }
    
    return { ...data, wd };
  }
}

// Express.js example
app.post('/api/login', async (req, res) => {
  const errors: Diagnostic[] = [];
  
  if (!req.body.email) {
    errors.push({
      compactId: 'aG8eT',
      fields: { field: 'email' }
    });
  }
  
  if (!req.body.password || req.body.password.length < 8) {
    errors.push({
      compactId: 'mN3Yr',
      fields: { field: 'password', min_length: 8 }
    });
  }
  
  if (errors.length > 0) {
    return res.status(422).json(
      DiagnosticResponse.standalone(errors)
    );
  }
  
  res.json({ token: '...' });
});

2.5. Aggregated Catalog Generation#

Rust
/// Generate aggregated catalog combining multiple namespaces
fn generate_aggregated_catalog() {
    use std::collections::HashMap;
    
    let mut namespaces = HashMap::new();
    namespaces.insert(
        "auth_service".to_string(),
        compute_namespace_hash("auth_service")
    );
    namespaces.insert(
        "payment_service".to_string(),
        compute_namespace_hash("payment_service")
    );
    
    let mut diags = HashMap::new();
    
    // Add auth service errors with CombinedID format
    let auth_hash = &namespaces["auth_service"];
    let auth_compact = compute_compact_id("E.AUTH.TOKEN.001");
    diags.insert(
        format!("{}-{}", auth_hash, auth_compact),
        ErrorEntry {
            code: "E.AUTH.TOKEN.001".to_string(),
            severity: "E".to_string(),
            message: "Token missing".to_string(),
            description: Some("JWT token not provided.".to_string()),
            hints: vec!["Include Authorization header".to_string()],
            tags: vec!["auth".to_string()],
            fields: vec![],
        }
    );
    
    // Add payment service errors
    let payment_hash = &namespaces["payment_service"];
    let payment_compact = compute_compact_id("E.PAYMENT.INVALID.001");
    diags.insert(
        format!("{}-{}", payment_hash, payment_compact),
        ErrorEntry {
            code: "E.PAYMENT.INVALID.001".to_string(),
            severity: "E".to_string(),
            message: "Invalid payment method".to_string(),
            description: Some("Payment method not supported.".to_string()),
            hints: vec!["Use credit card, debit card, or PayPal".to_string()],
            tags: vec!["payment".to_string()],
            fields: vec![],
        }
    );
    
    let catalog = json!({
        "version": "1.0.0",
        "namespaces": namespaces,
        "diags": diags
    });
    
    let catalog_json = serde_json::to_string_pretty(&catalog).unwrap();
    std::fs::write("catalog-aggregated.json", catalog_json).unwrap();
}

3. Client-Side Implementation#

3.1. Catalog Loading (TypeScript)#

Typescript
interface CatalogEntry {
  code: string;
  severity: string;
  message: string;
  description?: string;
  hints?: string[];
  tags?: string[];
  fields?: string[];
}

interface Catalog {
  version: string;
  namespace?: string;
  namespace_hash?: string;
  namespaces?: Record<string, string>;
  diags: Record<string, CatalogEntry>;
}

interface ExpandedDiagnostic {
  compactId: string;
  code: string;
  severity: string;
  message: string;
  description?: string;
  hints?: string[];
}

class WDPCatalog {
  private catalog: Catalog | null = null;
  
  async load(url: string): Promise<void> {
    const response = await fetch(url);
    this.catalog = await response.json();
  }
  
  /**
   * Expand diagnostics from wire protocol format
   * Handles both standalone and "wd" wrapped formats
   * Supports CompactID (5 chars) and CombinedID (11 chars)
   */
  expand(response: any): ExpandedDiagnostic[] {
    if (!this.catalog) {
      throw new Error("Catalog not loaded");
    }
    
    // Extract diagnostics (handle both formats)
    let diagnostics: Record<string, any>;
    
    if (response.wd) {
      diagnostics = response.wd;
    } else {
      diagnostics = {};
      for (const [key, value] of Object.entries(response)) {
        if (/^[0-9A-Za-z]{5}(-[0-9A-Za-z]{5})?$/.test(key)) {
          diagnostics[key] = value;
        }
      }
    }
    
    const results: ExpandedDiagnostic[] = [];
    
    for (const [wireKey, data] of Object.entries(diagnostics)) {
      const entry = this.lookupDiag(wireKey);
      
      if (!entry) {
        results.push({
          compactId: wireKey,
          code: 'UNKNOWN',
          severity: 'E',
          message: `Unknown diagnostic: ${wireKey}`,
          description: 'This diagnostic is not in the catalog.'
        });
        continue;
      }
      
      let message = entry.message;
      
      // Interpolate fields
      const fields = (data as any)?.f;
      if (fields) {
        for (const [field, value] of Object.entries(fields)) {
          message = message.replace(`{{${field}}}`, String(value));
        }
      }
      
      results.push({
        compactId: wireKey,
        code: entry.code,
        severity: entry.severity,
        message,
        description: entry.description,
        hints: entry.hints
      });
    }
    
    return results;
  }
  
  /**
   * Lookup diagnostic with namespace compatibility handling
   * Per Part 9b Section 7
   */
  private lookupDiag(wireKey: string): CatalogEntry | null {
    if (!this.catalog) return null;
    
    // Step 1: Direct lookup
    if (this.catalog.diags[wireKey]) {
      return this.catalog.diags[wireKey];
    }
    
    // Detect catalog type
    const firstKey = Object.keys(this.catalog.diags)[0];
    const isAggregated = firstKey?.includes('-');
    
    // Step 2: Over-qualified (CombinedID β†’ single-namespace)
    if (!isAggregated && wireKey.includes('-')) {
      const [nsHash, compactId] = wireKey.split('-');
      
      if (this.catalog.namespace_hash && 
          this.catalog.namespace_hash !== nsHash) {
        console.warn(`Namespace mismatch: ${nsHash} vs ${this.catalog.namespace_hash}`);
      }
      
      return this.catalog.diags[compactId] || null;
    }
    
    // Step 3: Under-qualified (CompactID β†’ aggregated)
    if (isAggregated && !wireKey.includes('-')) {
      throw new Error(
        `Protocol violation: Aggregated catalog requires CombinedID, got '${wireKey}'`
      );
    }
    
    return null;
  }
  
  /**
   * Get namespace name from CombinedID
   */
  getNamespace(combinedId: string): string | null {
    if (!combinedId.includes('-')) return null;
    
    const nsHash = combinedId.split('-')[0];
    const namespaces = this.catalog?.namespaces;
    
    if (!namespaces) return nsHash;
    
    for (const [name, hash] of Object.entries(namespaces)) {
      if (hash === nsHash) return name;
    }
    
    return 'unknown';
  }
}

// Usage
const catalog = new WDPCatalog();
await catalog.load('https://cdn.example.com/catalog.json');

const response = await fetch('/api/login', {
  method: 'POST',
  body: JSON.stringify({ email: '', password: '123' })
});

const body = await response.json();
const diagnostics = catalog.expand(body);

diagnostics.forEach(diag => {
  const icon = { 'E': '❌', 'W': '⚠️', 'C': 'πŸ”΄', 'I': 'ℹ️', 'H': 'πŸ’‘' }[diag.severity];
  console.log(`${icon} ${diag.severity}: ${diag.message}`);
});

3.2. Catalog Loading (Python)#

Python
import requests
import json
from typing import Dict, List, Optional, Any

class WDPCatalog:
    def __init__(self):
        self.catalog: Optional[Dict] = None
    
    def load(self, url: str):
        """Load catalog from URL."""
        response = requests.get(url)
        self.catalog = response.json()
    
    def load_file(self, path: str):
        """Load catalog from local file."""
        with open(path, 'r') as f:
            self.catalog = json.load(f)
    
    def expand(self, response: Dict) -> List[Dict]:
        """Expand diagnostics from wire protocol format."""
        if not self.catalog:
            raise ValueError("Catalog not loaded")
        
        # Extract diagnostics
        if 'wd' in response:
            diagnostics = response['wd']
        else:
            import re
            diagnostics = {
                k: v for k, v in response.items()
                if re.match(r'^[0-9A-Za-z]{5}(-[0-9A-Za-z]{5})?$', k)
            }
        
        results = []
        
        for wire_key, data in diagnostics.items():
            entry = self._lookup_diag(wire_key)
            
            if not entry:
                results.append({
                    'compact_id': wire_key,
                    'code': 'UNKNOWN',
                    'severity': 'E',
                    'message': f"Unknown diagnostic: {wire_key}"
                })
                continue
            
            message = entry['message']
            
            # Interpolate fields
            if 'f' in data:
                for field, value in data['f'].items():
                    message = message.replace(f"{{{{{field}}}}}", str(value))
            
            results.append({
                'compact_id': wire_key,
                'code': entry['code'],
                'severity': entry['severity'],
                'message': message,
                'description': entry.get('description'),
                'hints': entry.get('hints', [])
            })
        
        return results
    
    def _lookup_diag(self, wire_key: str) -> Optional[dict]:
        """Lookup diagnostic with namespace compatibility handling."""
        # Direct lookup
        if wire_key in self.catalog['diags']:
            return self.catalog['diags'][wire_key]
        
        # Detect catalog type
        first_key = next(iter(self.catalog['diags'].keys()), '')
        is_aggregated = '-' in first_key
        
        # Over-qualified (CombinedID β†’ single-namespace)
        if not is_aggregated and '-' in wire_key:
            ns_hash, compact_id = wire_key.split('-')
            
            # Optional: Verify namespace hash
            if 'namespace_hash' in self.catalog:
                if self.catalog['namespace_hash'] != ns_hash:
                    # Log warning
                    pass
            
            return self.catalog['diags'].get(compact_id)
        
        # Under-qualified (CompactID β†’ aggregated)
        if is_aggregated and '-' not in wire_key:
            raise ValueError(f"Aggregated catalog requires CombinedID, got '{wire_key}'")
            
        return None

# Usage
catalog = WDPCatalog()
catalog.load('https://cdn.example.com/catalog.json')

response = requests.post('/api/login', json={'email': '', 'password': '123'})
diagnostics = catalog.expand(response.json())

for diag in diagnostics:
    print(f"{diag['severity']}: {diag['message']}")

4. Testing#

4.1. Catalog Validation Tests (Pytest)#

Python
import pytest
import json
import re

def test_catalog_structure():
    """Test catalog has required fields and valid structure"""
    with open('catalog.json') as f:
        catalog = json.load(f)
    
    # Required top-level fields
    assert 'version' in catalog
    assert 'diags' in catalog
    
    # Version format
    assert re.match(r'^\d+\.\d+\.\d+$', catalog['version'])

def test_catalog_entries():
    """Test each catalog entry is valid"""
    with open('catalog.json') as f:
        catalog = json.load(f)
    
    for compact_id, entry in catalog['diags'].items():
        # Compact ID format
        assert len(compact_id) == 5
        assert re.match(r'^[0-9A-Za-z]{5}$', compact_id)
        
        # Required fields
        assert 'code' in entry
        assert 'severity' in entry
        assert 'message' in entry
        
        # Severity matches code
        assert entry['code'].startswith(entry['severity'])

def test_field_placeholders():
    """Test message placeholders match fields list"""
    with open('catalog.json') as f:
        catalog = json.load(f)
        
    for _, entry in catalog['diags'].items():
        message = entry['message']
        declared_fields = set(entry.get('fields', []))
        
        # Find {{field}} patterns
        placeholders = set(re.findall(r'{{(\w+)}}', message))
        
        # All placeholders must be declared
        assert placeholders.issubset(declared_fields), \
            f"Message requires fields {placeholders} but declared {declared_fields}"

5. Best Practices#

5.1. Production Safety#

  • Fail Open: Catalog loading failure MUST NOT crash the application. Fall back to displaying raw CompactIDs or generic error messages.
  • Async Loading: Load catalogs asynchronously to avoid blocking application startup.
  • Cache Aggressively: Catalogs are immutable artifacts associated with specific versions. Cache them heavily (e.g., Cache-Control: max-age=31536000, immutable).

5.2. Performance#

  • Minimal Format: Use the Minimal format (Part 9a) for mobile and IoT devices to reduce bandwidth.
  • Lazy Loading: Load catalogs only when the first error occurs, or pre-fetch during idle time.
  • Compression: Always serve catalogs with Brotli or Gzip compression (often >80% reduction).

5.3. Versioning#

  • Build Pipelines: Generate a new catalog artifact for every build/release.
  • URL Strategy: Include the version or content hash in the filename: catalog.v1.2.3.json or catalog.abc1234.json.
  • Backward Compatibility: Never remove IDs from a catalog once published if old clients might still encounter them. Deprecate instead.

6. References#