MoreRSS

site iconThe Practical DeveloperModify

A constructive and inclusive social network for software developers.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of The Practical Developer

Local-First Documentation: What It Is and Why Your AI Agent Needs It

2026-02-19 10:27:08

You're mid-session with your AI coding assistant. It's been writing solid code for the last twenty minutes — referencing the right framework APIs, using current patterns. Then it starts hallucinating. The cloud documentation service hit its rate limit, and your assistant fell back to its training data. Now it's confidently suggesting APIs that were deprecated two versions ago.

This is the fundamental reliability problem with cloud-based documentation for AI agents. Local-first documentation solves it.

What is local-first documentation?

Local-first documentation means indexing library docs into a local database and serving them to your AI agent without any network calls. Instead of your assistant querying a cloud API every time it needs to reference a framework, it reads from a file on your machine.

The concept borrows from the broader local-first software movement: your data lives on your device, works offline, and doesn't depend on someone else's server being up. Applied to AI documentation, it means:

  • Docs are stored locally — typically as a SQLite database or similar portable format
  • Queries never leave your machine — sub-10ms lookups instead of 100–500ms cloud round-trips
  • No network dependency — works on a plane, in an air-gapped environment, or when your Wi-Fi drops
  • You control the version — index docs for the exact library version you're using

This isn't a new idea for developer tools. DevDocs, Zeal, and Dash have offered offline documentation browsing for years. What's new is applying this architecture to AI agents — giving your coding assistant the same offline, instant, version-accurate access to docs that you'd want for yourself.

The problem with cloud documentation services

Cloud documentation services solve a real problem: AI coding assistants need access to current docs that aren't in their training data. Services like Context7 provide this by hosting documentation and serving it through an API.

But cloud-first architecture introduces its own failure modes:

  • Rate limits cut you off mid-session. Most services cap requests at 60 per hour. A single complex coding session can burn through that in minutes, especially with agentic workflows where the AI makes dozens of tool calls. Once you hit the limit, your assistant loses access to docs entirely.
  • Latency adds up. Each cloud lookup takes 100–500ms. In a session with 30+ doc queries, that's 3–15 seconds of accumulated waiting — enough to noticeably slow down an interactive coding session.
  • Version mismatch. Most cloud services index only the latest version of a library. If your project is pinned to Next.js 15 but the service indexed Next.js 16, every answer references the wrong API. The version lag cuts both ways — if you're on the latest and the service is behind, you still get wrong answers.
  • Privacy exposure. Every query goes to a third-party server. For teams working with proprietary codebases, internal APIs, or sensitive project structures, that's a non-trivial concern. The queries themselves reveal what you're building and what you're struggling with.
  • Cost scales with usage. Free tiers have tight limits. Paid plans charge per query or per month. For teams with multiple developers using AI assistants heavily, costs compound.

None of these are deal-breakers for casual use. If you're prototyping something quick and always-latest docs are fine, cloud services work. The problems surface when reliability and accuracy matter — production codebases, version-pinned dependencies, teams that can't afford their AI assistant going dark mid-session.

Why local-first is a better fit for AI agents

AI agents have different access patterns than human developers browsing docs. A developer might look up a few API references per hour. An AI agent in an agentic coding session might query docs 50+ times in a single task — checking types, verifying method signatures, reading examples for each file it touches.

This high-frequency access pattern is exactly where local-first shines:

  • No rate limits, ever. Your agent can query docs hundreds of times per session. The database is a file on disk — there's no server to throttle you.
  • Sub-10ms latency. SQLite queries against a local FTS5 index return in under 10 milliseconds. That's fast enough that doc lookups add zero perceptible delay to your coding session.
  • Version pinning. Index docs for the exact Git tag your project uses. When you're on [email protected], you get v6 docs — not a blend of every version that existed at training time, and not whatever "latest" the cloud service indexed.
  • Works everywhere. Airplane mode, air-gapped networks, coffee shop Wi-Fi that drops every five minutes. Once the docs are indexed locally, your AI never loses access.
  • Free and unlimited. No per-query pricing, no monthly subscriptions, no tier limits. Index as many libraries as you need, query as often as you want.
  • Private by default. Your queries stay on your machine. No third party sees what APIs you're looking up, what frameworks you're using, or what internal docs you've indexed.

How local-first documentation works

The architecture is straightforward:

  1. Point at a source. Give the tool a Git repository URL (or a local directory). It clones the repo's docs — typically Markdown files in a /docs folder.
  2. Pick a version. Select the exact Git tag or branch you want. This is what makes version pinning possible.
  3. Index into a local database. The tool parses documentation into semantically chunked sections and indexes them with full-text search (FTS5 + BM25 ranking) into a portable SQLite .db file.
  4. Serve via MCP. The tool starts a local Model Context Protocol server. Your AI coding assistant — Claude Code, Cursor, VS Code Copilot, Windsurf — connects to it and queries docs through the standard MCP protocol.

The result: your AI assistant asks "How do I create middleware in Next.js?" and gets an answer from the exact version of Next.js docs you indexed, in under 10ms, without touching the internet.

@neuledge/context implements this architecture. Three commands to set up:

npm install -g @neuledge/context
context add https://github.com/vercel/next.js --tag v16.0.0
context mcp

The .db files are portable — check them into your repo or share them on a drive. Every developer on your team gets the same indexed docs with zero setup.

Local-first vs. cloud documentation: when to use each

Local-First Cloud
Rate limits None 60 req/hour typical
Latency <10ms 100–500ms
Offline Yes No
Version pinning Exact tags Latest only
Privacy 100% local Cloud-processed
Cost Free $10+/month
Setup 3 commands API key + config
Internal docs Yes, free Paid or unsupported

Use local-first when:

  • You're working on a production codebase pinned to specific dependency versions
  • You're in an offline or air-gapped environment
  • Privacy matters — proprietary code, internal APIs, sensitive projects
  • Your AI workflow is agentic (high-frequency doc queries that would hit rate limits)
  • You want to index internal documentation alongside open-source libraries

Use cloud when:

  • You're prototyping and always-latest docs are acceptable
  • You want zero-setup, zero-install convenience
  • Your AI usage is light enough that rate limits don't matter

Both approaches have their place. Cloud services offer convenience for light use. Local-first offers reliability and accuracy when it counts.

Get started

If your AI coding assistant keeps hitting rate limits, suggesting deprecated APIs, or losing access to docs mid-session, local-first documentation fixes all three:

npm install -g @neuledge/context
context add https://github.com/vercel/next.js
claude mcp add context -- npx @neuledge/context mcp

India's 3-Hour Deepfake Rule Exposes a Blind Spot: Building Cryptographic Proof of What AI Refused to Generate

2026-02-19 10:06:37

On February 20, 2026, India's IT Rules Amendment 2026 took effect, mandating that platforms remove illegal deepfakes within 3 hours of a government or court order and within 2 hours for nudity, sexual content, or impersonation complaints. Platforms must now embed permanent metadata with unique identifiers in all synthetic content and visibly label AI-generated media—or risk losing their safe harbour protection under Section 79 of the IT Act.

This is the world's most aggressive content-focused deepfake regulation. And it has a blind spot the size of a continent.

The rules tell platforms: label what you generated, delete what is illegal, record what you did. But they say nothing about a question regulators will inevitably need answered: did the AI system actually refuse to generate the harmful content it claims to have blocked?

This article walks through the technical gap, fact-checks the regulatory claims, and builds a working Python implementation of CAP-SRP (Content/Creative AI Profile – Safe Refusal Provenance)—a cryptographic framework that proves what AI systems refused to generate.

Repository: github.com/veritaschain/cap-spec

Table of Contents

  1. What India's Rules Actually Require (Fact-Checked)
  2. The Regulatory Gap: Published Content vs. Refused Attempts
  3. Grok: What Happens When There Is No Proof
  4. CAP-SRP Architecture Overview
  5. Implementation: Hash Chain Foundation
  6. Implementation: The Completeness Invariant
  7. Implementation: Ed25519 Signing
  8. Implementation: Merkle Tree Anchoring
  9. Implementation: Privacy-Preserving Prompt Hashing
  10. Implementation: Evidence Pack Generation
  11. Full Working Verifier
  12. Regulatory Compliance Mapping
  13. Limitations and Honest Assessment
  14. What's Next

What India's Rules Actually Require (Fact-Checked) {#what-indias-rules-actually-require}

Before building a solution, we need to be precise about the problem. I ran a detailed fact-check on the IT Rules Amendment 2026, and several claims circulating in the AI governance community need correction.

✅ Confirmed

Two-tier takedown deadlines are real, but they are separate provisions—not a range.

  • 3 hours: For unlawful content flagged via court order or reasoned government officer intimation (Rule 3(1)(d)), down from the previous 36-hour window
  • 2 hours: For complaints specifically about nudity, sexual content, morphed imagery, or impersonation

These are distinct legal triggers with different scopes. Conflating them as "2–3 hours" misrepresents the structure.

Permanent metadata and labeling are mandated, with two legally distinct obligations:

  1. Visible labeling: Prominent disclosure on visual content, prefixed announcements on audio
  2. Permanent metadata: Unique identifiers for traceability embedded in synthetic content

However, the metadata requirement includes a critical "where technically feasible" qualifier that most commentary omits. This is a significant legal caveat for implementation.

Safe harbour loss is real, but the trigger is narrower than often claimed. Platforms that "knowingly permit, promote, or fail to act against" unlawful synthetic content lose their Section 79 protection. The "knowingly" qualifier matters—it is not triggered by mere failure. Additionally, the amendment includes a positive safe harbour: platforms acting in good faith to proactively remove harmful AI content retain legal protection from wrongful-removal lawsuits.

⚠️ Partially Accurate or Misleading

"Provenance mechanisms within 10 days" mischaracterizes the timeline. The amendment was notified February 10 and enforced February 20. The 10-day figure is the total compliance window for the entire amendment—not a specific deadline for provenance implementation.

"Record all enforcement measures for future audit" could not be verified against the actual regulation text or any of the major law firm analyses (Cyril Amarchand Mangaldas, Hogan Lovells, Khaitan & Co., AZB & Partners). The amendment imposes enhanced due diligence and automated detection obligations, but no explicit "audit recording" mandate matching this specific claim was found.

"Mandatory for brands/creators" mischaracterizes the regulatory subject. The IT Rules Amendment targets platforms and intermediaries (SSMIs), not brands or creators directly. Content creators face only a self-declaration obligation when uploading AI-generated content.

What the Rules Actually Target

The regulation exclusively addresses content that exists:

  • Labeling synthetic content ✓
  • Removing unlawful synthetic content ✓
  • Embedding metadata in generated content ✓
  • Platforms detecting and acting on harmful synthetic media ✓

What it does not address:

  • Tracking what AI systems refused to generate ✗
  • Verifying that claimed safety measures actually operated ✗
  • Providing third-party verification of moderation claims ✗
  • Ensuring completeness of audit trails ✗

This is the gap.

The Regulatory Gap: Published Content vs. Refused Attempts {#the-regulatory-gap}

India's rules, the EU AI Act, the US TAKE IT DOWN Act, the Colorado AI Act—every major regulatory framework shares the same structural limitation. They all regulate what comes out of AI systems (the generated content) but not what AI systems claim to have prevented.

The current model works like this:

Regulator: "Did your AI block harmful requests?"
Platform:  "Yes, we blocked 2.3 million."
Regulator: "How do we verify that?"
Platform:  "...trust us?"

This is the "Trust Us" model of AI governance. It worked when AI systems were less capable. It no longer works when a single model can generate thousands of non-consensual intimate images per hour.

The shift we need:

Regulator: "Did your AI block harmful requests?"
Platform:  "Here is a cryptographically signed evidence pack.
            Every generation attempt has exactly one recorded
            outcome. The hash chain is externally anchored.
            You can verify independently."
Regulator: [runs cap-verify on evidence pack]
           "Chain integrity: VALID. Completeness: VALID.
            67.3% refusal rate. 0 orphan events."

Grok: What Happens When There Is No Proof {#grok-what-happens}

In January 2026, xAI's Grok AI generated an estimated 3 million sexualized images in 11 days, including content depicting minors. The Future of Life Institute rated xAI's safety practices as "F"—the lowest among major AI providers. Technical analysis revealed Grok lacked basic trust and safety layers: no detection of minors, no blocking for sexually suggestive poses, no C2PA watermarking.

When xAI claimed to have fixed the issues, regulators across five jurisdictions (EU, UK, India, Indonesia, California) had no way to independently verify whether:

  1. The safety measures now existed
  2. They were actually functioning
  3. The claimed refusal rates were real
  4. No requests were being silently dropped from logs

The Grok incident is the most compelling demonstration of why refusal provenance matters. Without it, the gap between "we fixed it" and "prove it" remains unbridgeable.

CAP-SRP Architecture Overview {#capsrp-architecture-overview}

CAP-SRP (Content/Creative AI Profile – Safe Refusal Provenance) creates tamper-evident audit trails of AI content moderation decisions. The core idea:

You don't prove the negative directly. You prove the positive—every attempt, every outcome, every decision—and you prove that the record is complete.

The architecture has five layers:

┌─────────────────────────────────────────────┐
│  Layer 5: Evidence Pack Generation          │
│  (Self-contained, legally admissible)       │
├─────────────────────────────────────────────┤
│  Layer 4: External Anchoring                │
│  (RFC 3161 TSA, SCITT Transparency Service) │
├─────────────────────────────────────────────┤
│  Layer 3: Merkle Tree Aggregation           │
│  (Efficient verification + inclusion proofs)│
├─────────────────────────────────────────────┤
│  Layer 2: Digital Signatures                │
│  (Ed25519, RFC 8032)                        │
├─────────────────────────────────────────────┤
│  Layer 1: Hash Chain                        │
│  (SHA-256 linked events, RFC 8785 JCS)      │
├─────────────────────────────────────────────┤
│  Layer 0: Event Logging                     │
│  (GEN_ATTEMPT → GEN | GEN_DENY | GEN_ERROR)│
└─────────────────────────────────────────────┘

Three conformance levels accommodate different organizational maturity:

Level Hash Chain Completeness Invariant External Anchoring Retention Target
Bronze Required Recommended Optional 6 months SMEs
Silver Required Required Daily 2 years Enterprise
Gold Required Required Hourly + SCITT 5 years High-risk AI

Implementation: Hash Chain Foundation {#implementation-hash-chain}

Let's build this from scratch. Every event in CAP-SRP is linked to the previous event via SHA-256 hashing, creating a tamper-evident chain.

"""
cap_srp/chain.py — Hash chain construction for CAP-SRP events.

Events are canonicalized per RFC 8785 (JSON Canonicalization Scheme)
before hashing, ensuring deterministic hash computation across
implementations.
"""

import hashlib
import json
import uuid
from datetime import datetime, timezone
from typing import Optional


def _canonicalize(obj: dict) -> str:
    """
    RFC 8785-compliant JSON canonicalization.

    For production, use a dedicated JCS library.
    This simplified version handles the common cases:
    - Sort keys lexicographically
    - No whitespace
    - Unicode normalization
    """
    return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False)


def generate_event_id() -> str:
    """Generate UUIDv7-style event ID (time-ordered)."""
    return str(uuid.uuid7())


def compute_event_hash(event: dict) -> str:
    """
    Compute SHA-256 hash of canonicalized event.

    The Signature field is excluded before hashing to avoid
    circular dependency (hash → sign → hash would change).

    Args:
        event: Event dictionary (Signature field excluded from hash)

    Returns:
        Hash string in format "sha256:{hex_digest}"
    """
    # Remove signature before hashing
    hashable = {k: v for k, v in event.items() if k != "Signature"}
    canonical = _canonicalize(hashable)
    digest = hashlib.sha256(canonical.encode("utf-8")).digest()
    return f"sha256:{digest.hex()}"


def create_genesis_event(chain_id: str, provider_id: str) -> dict:
    """
    Create the first event in a new audit chain.

    The genesis event has PrevHash set to the zero hash,
    establishing the chain root.
    """
    event = {
        "EventID": generate_event_id(),
        "EventType": "CHAIN_INIT",
        "ChainID": chain_id,
        "Timestamp": datetime.now(timezone.utc).isoformat(),
        "PrevHash": "sha256:" + "0" * 64,
        "ProviderID": provider_id,
        "SpecVersion": "CAP-SRP/1.0",
    }
    event["EventHash"] = compute_event_hash(event)
    return event


def append_event(
    chain: list[dict],
    event_type: str,
    payload: dict,
    chain_id: str,
) -> dict:
    """
    Create and append a new event to the chain.

    Each event includes the hash of the previous event,
    creating the tamper-evident linkage. Modifying any
    historical event invalidates all subsequent hashes.

    Args:
        chain: Existing event list
        event_type: One of GEN_ATTEMPT, GEN, GEN_DENY, GEN_ERROR
        payload: Event-specific data
        chain_id: Chain identifier

    Returns:
        The new event (also appended to chain)
    """
    prev_event = chain[-1]

    event = {
        "EventID": generate_event_id(),
        "EventType": event_type,
        "ChainID": chain_id,
        "Timestamp": datetime.now(timezone.utc).isoformat(),
        "PrevHash": prev_event["EventHash"],
        **payload,
    }
    event["EventHash"] = compute_event_hash(event)
    chain.append(event)
    return event

Chain Integrity Verification

The chain is valid if and only if every event's hash matches its recomputed value, and every PrevHash matches the preceding event's EventHash:

"""
cap_srp/verify.py — Chain integrity verification.
"""

from typing import NamedTuple


class ChainVerification(NamedTuple):
    valid: bool
    error: str | None = None
    event_index: int | None = None


def verify_chain_integrity(events: list[dict]) -> ChainVerification:
    """
    Verify the full hash chain.

    Checks:
    1. Each event's EventHash matches recomputation
    2. Each event's PrevHash matches the previous EventHash
    3. Genesis event has the zero-hash PrevHash

    Time complexity: O(n)
    Space complexity: O(1)
    """
    if not events:
        return ChainVerification(valid=False, error="Empty chain")

    # Check genesis
    if events[0]["PrevHash"] != "sha256:" + "0" * 64:
        return ChainVerification(
            valid=False, error="Invalid genesis PrevHash", event_index=0
        )

    for i, event in enumerate(events):
        # Verify hash computation
        computed = compute_event_hash(event)
        if event["EventHash"] != computed:
            return ChainVerification(
                valid=False,
                error=f"Hash mismatch: stored={event['EventHash']}, computed={computed}",
                event_index=i,
            )

        # Verify chain linkage (skip genesis)
        if i > 0:
            if event["PrevHash"] != events[i - 1]["EventHash"]:
                return ChainVerification(
                    valid=False,
                    error=f"Chain break: PrevHash does not match previous EventHash",
                    event_index=i,
                )

    return ChainVerification(valid=True)

Implementation: The Completeness Invariant {#implementation-completeness-invariant}

This is the mathematical core of CAP-SRP. The Completeness Invariant guarantees:

∑ GEN_ATTEMPT = ∑ GEN + ∑ GEN_DENY + ∑ GEN_ERROR

For any time window, the count of attempts must exactly equal the count of all outcomes. This is what makes selective logging detectable.

Violation Meaning Implication
Attempts > Outcomes Unmatched attempts exist System is hiding results
Outcomes > Attempts Orphan outcomes exist System fabricated refusals
Duplicate outcomes Multiple outcomes per attempt Data integrity failure

The critical architectural insight: GEN_ATTEMPT is logged before the safety evaluation runs. This creates an unforgeable commitment that a request existed, regardless of what follows. If the equation does not balance, the audit trail is provably invalid.

"""
cap_srp/completeness.py — Completeness Invariant verification.

The Completeness Invariant ensures every generation attempt
has exactly one recorded outcome, preventing:
- Selective omission (hiding inconvenient generations)
- Fabricated refusals (inflating safety statistics)
- Split-view attacks (showing different logs to different auditors)
"""

from dataclasses import dataclass, field
from datetime import datetime


ATTEMPT_TYPES = {"GEN_ATTEMPT"}
OUTCOME_TYPES = {"GEN", "GEN_DENY", "GEN_ERROR"}


@dataclass
class CompletenessResult:
    """Result of Completeness Invariant verification."""

    valid: bool
    total_attempts: int = 0
    total_outcomes: int = 0
    matched_pairs: int = 0
    unmatched_attempts: list[str] = field(default_factory=list)
    orphan_outcomes: list[str] = field(default_factory=list)
    duplicate_outcomes: list[str] = field(default_factory=list)

    @property
    def refusal_rate(self) -> float | None:
        """Calculate the refusal rate if data is valid."""
        if self.total_attempts == 0:
            return None
        deny_count = self.total_outcomes - self.matched_pairs  # approximate
        return deny_count / self.total_attempts if self.total_attempts > 0 else 0.0

    def summary(self) -> str:
        status = "✓ VALID" if self.valid else "✗ INVALID"
        lines = [
            f"Completeness Invariant: {status}",
            f"  Attempts:           {self.total_attempts}",
            f"  Outcomes:           {self.total_outcomes}",
            f"  Matched pairs:      {self.matched_pairs}",
            f"  Unmatched attempts: {len(self.unmatched_attempts)}",
            f"  Orphan outcomes:    {len(self.orphan_outcomes)}",
            f"  Duplicate outcomes: {len(self.duplicate_outcomes)}",
        ]
        return "\n".join(lines)


def verify_completeness(
    events: list[dict],
    time_start: datetime | None = None,
    time_end: datetime | None = None,
) -> CompletenessResult:
    """
    Verify the Completeness Invariant for events in a time window.

    For every GEN_ATTEMPT, there must exist exactly one event E where:
      - E.EventType ∈ {GEN, GEN_DENY, GEN_ERROR}
      - E.AttemptID == GEN_ATTEMPT.EventID
      - E.Timestamp > GEN_ATTEMPT.Timestamp

    Args:
        events: All events (any order; will be filtered)
        time_start: Optional window start (inclusive)
        time_end: Optional window end (inclusive)

    Returns:
        CompletenessResult with detailed breakdown
    """
    # Filter by time window if specified
    filtered = events
    if time_start or time_end:
        filtered = []
        for e in events:
            ts = datetime.fromisoformat(e["Timestamp"])
            if time_start and ts < time_start:
                continue
            if time_end and ts > time_end:
                continue
            filtered.append(e)

    # Separate attempts and outcomes
    attempts: dict[str, dict] = {}
    outcomes: list[dict] = []

    for e in filtered:
        if e["EventType"] in ATTEMPT_TYPES:
            attempts[e["EventID"]] = e
        elif e["EventType"] in OUTCOME_TYPES:
            outcomes.append(e)

    # Match outcomes to attempts
    matched_attempt_ids: set[str] = set()
    orphan_outcomes: list[str] = []
    duplicate_outcomes: list[str] = []

    for outcome in outcomes:
        attempt_id = outcome.get("AttemptID")

        if attempt_id not in attempts:
            # Outcome references a non-existent attempt
            orphan_outcomes.append(outcome["EventID"])
            continue

        if attempt_id in matched_attempt_ids:
            # Multiple outcomes for same attempt
            duplicate_outcomes.append(outcome["EventID"])
            continue

        matched_attempt_ids.add(attempt_id)

    # Unmatched attempts: logged but no outcome recorded
    unmatched_attempts = [
        aid for aid in attempts if aid not in matched_attempt_ids
    ]

    is_valid = (
        len(unmatched_attempts) == 0
        and len(orphan_outcomes) == 0
        and len(duplicate_outcomes) == 0
    )

    return CompletenessResult(
        valid=is_valid,
        total_attempts=len(attempts),
        total_outcomes=len(outcomes),
        matched_pairs=len(matched_attempt_ids),
        unmatched_attempts=unmatched_attempts,
        orphan_outcomes=orphan_outcomes,
        duplicate_outcomes=duplicate_outcomes,
    )

Testing the Invariant

"""
tests/test_completeness.py — Verify invariant catches violations.
"""

from cap_srp.chain import create_genesis_event, append_event
from cap_srp.completeness import verify_completeness


def build_test_chain():
    """Build a chain with 3 attempts: 1 generated, 1 denied, 1 error."""
    chain_id = "test-chain-001"
    chain = [create_genesis_event(chain_id, "test-provider")]

    # Attempt 1 → Generated
    attempt_1 = append_event(chain, "GEN_ATTEMPT", {
        "PromptHash": "sha256:aaa...",
        "ModelID": "test-model-v1",
        "InputType": "text",
    }, chain_id)

    append_event(chain, "GEN", {
        "AttemptID": attempt_1["EventID"],
        "ContentHash": "sha256:bbb...",
        "OutputType": "image",
    }, chain_id)

    # Attempt 2 → Denied (NCII risk)
    attempt_2 = append_event(chain, "GEN_ATTEMPT", {
        "PromptHash": "sha256:ccc...",
        "ModelID": "test-model-v1",
        "InputType": "text",
    }, chain_id)

    append_event(chain, "GEN_DENY", {
        "AttemptID": attempt_2["EventID"],
        "DenyReason": "NCII_RISK",
        "PolicyID": "safety-policy-v3",
        "Confidence": 0.97,
    }, chain_id)

    # Attempt 3 → Error
    attempt_3 = append_event(chain, "GEN_ATTEMPT", {
        "PromptHash": "sha256:ddd...",
        "ModelID": "test-model-v1",
        "InputType": "text",
    }, chain_id)

    append_event(chain, "GEN_ERROR", {
        "AttemptID": attempt_3["EventID"],
        "ErrorCode": "MODEL_TIMEOUT",
    }, chain_id)

    return chain


def test_valid_chain():
    chain = build_test_chain()
    result = verify_completeness(chain)
    assert result.valid is True
    assert result.total_attempts == 3
    assert result.matched_pairs == 3
    assert len(result.unmatched_attempts) == 0
    print(result.summary())


def test_missing_outcome():
    """Simulate a platform hiding a generation result."""
    chain = build_test_chain()

    # Add an attempt with no outcome — the "hidden generation"
    append_event(chain, "GEN_ATTEMPT", {
        "PromptHash": "sha256:eee...",
        "ModelID": "test-model-v1",
        "InputType": "text",
    }, chain[-1]["ChainID"])

    result = verify_completeness(chain)
    assert result.valid is False
    assert len(result.unmatched_attempts) == 1
    print(result.summary())
    # Output:
    # Completeness Invariant: ✗ INVALID
    #   Attempts:           4
    #   Outcomes:           3
    #   Matched pairs:      3
    #   Unmatched attempts: 1
    #   Orphan outcomes:    0
    #   Duplicate outcomes: 0


def test_fabricated_refusal():
    """Simulate a platform inflating its refusal count."""
    chain = build_test_chain()

    # Add a refusal that references no real attempt
    append_event(chain, "GEN_DENY", {
        "AttemptID": "nonexistent-attempt-id",
        "DenyReason": "NCII_RISK",
        "PolicyID": "safety-policy-v3",
        "Confidence": 0.99,
    }, chain[-1]["ChainID"])

    result = verify_completeness(chain)
    assert result.valid is False
    assert len(result.orphan_outcomes) == 1
    print(result.summary())
    # Output:
    # Completeness Invariant: ✗ INVALID
    #   Attempts:           3
    #   Outcomes:           4
    #   Matched pairs:      3
    #   Unmatched attempts: 0
    #   Orphan outcomes:    1
    #   Duplicate outcomes: 0


if __name__ == "__main__":
    test_valid_chain()
    print()
    test_missing_outcome()
    print()
    test_fabricated_refusal()

Implementation: Ed25519 Signing {#implementation-ed25519-signing}

Every event is signed with Ed25519 (RFC 8032). This prevents post-hoc tampering—even by the platform operator. If the signing key is stored in an HSM (Gold level), the platform cannot retroactively modify events without detection.

"""
cap_srp/signing.py — Ed25519 event signing and verification.

Uses the cryptography library (pip install cryptography).
For production, key management should use HSM or cloud KMS.
"""

import base64
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
    Ed25519PrivateKey,
    Ed25519PublicKey,
)
from cryptography.exceptions import InvalidSignature

from cap_srp.chain import compute_event_hash


def generate_keypair() -> tuple[Ed25519PrivateKey, Ed25519PublicKey]:
    """Generate a new Ed25519 keypair for event signing."""
    private_key = Ed25519PrivateKey.generate()
    public_key = private_key.public_key()
    return private_key, public_key


def sign_event(event: dict, private_key: Ed25519PrivateKey) -> dict:
    """
    Sign an event with Ed25519.

    The signature covers the EventHash, which itself covers
    all fields except Signature. This creates a two-step
    integrity guarantee:
      1. EventHash proves field integrity
      2. Signature proves EventHash authenticity

    Args:
        event: Event dict with EventHash already computed
        private_key: Ed25519 signing key

    Returns:
        Event dict with Signature field added
    """
    # Ensure hash is computed
    if "EventHash" not in event:
        event["EventHash"] = compute_event_hash(event)

    # Sign the hash bytes (not the raw event)
    hash_hex = event["EventHash"].removeprefix("sha256:")
    hash_bytes = bytes.fromhex(hash_hex)
    signature = private_key.sign(hash_bytes)

    event["Signature"] = f"ed25519:{base64.b64encode(signature).decode()}"
    return event


def verify_event_signature(event: dict, public_key: Ed25519PublicKey) -> bool:
    """
    Verify an event's Ed25519 signature.

    Steps:
    1. Recompute event hash (excluding Signature field)
    2. Compare with stored EventHash
    3. Verify signature over the hash bytes

    Returns:
        True if signature is valid, False otherwise
    """
    # Step 1: Verify hash integrity
    computed_hash = compute_event_hash(event)
    if event.get("EventHash") != computed_hash:
        return False  # Event data was modified

    # Step 2: Extract and decode signature
    sig_str = event.get("Signature", "")
    if not sig_str.startswith("ed25519:"):
        return False

    try:
        signature = base64.b64decode(sig_str.removeprefix("ed25519:"))
    except Exception:
        return False

    # Step 3: Verify signature over hash bytes
    hash_bytes = bytes.fromhex(computed_hash.removeprefix("sha256:"))
    try:
        public_key.verify(signature, hash_bytes)
        return True
    except InvalidSignature:
        return False


def sign_chain(chain: list[dict], private_key: Ed25519PrivateKey) -> list[dict]:
    """Sign all events in a chain."""
    return [sign_event(event, private_key) for event in chain]


def verify_chain_signatures(
    chain: list[dict], public_key: Ed25519PublicKey
) -> tuple[bool, list[int]]:
    """
    Verify all signatures in a chain.

    Returns:
        Tuple of (all_valid, list_of_invalid_indices)
    """
    invalid_indices = []
    for i, event in enumerate(chain):
        if not verify_event_signature(event, public_key):
            invalid_indices.append(i)

    return (len(invalid_indices) == 0, invalid_indices)

Implementation: Merkle Tree Anchoring {#implementation-merkle-tree}

For efficient verification, events are aggregated into Merkle trees. A regulator can verify that a specific event is included in the tree without downloading the entire chain—using an inclusion proof.

"""
cap_srp/merkle.py — Merkle tree construction and inclusion proofs.

Enables O(log n) verification of event inclusion, critical for
regulatory audits where verifying millions of events individually
is impractical.
"""

import hashlib
from dataclasses import dataclass


def _hash_pair(left: bytes, right: bytes) -> bytes:
    """Hash two child nodes to produce parent."""
    return hashlib.sha256(left + right).digest()


def _hash_leaf(data: str) -> bytes:
    """Hash a leaf node (event hash string)."""
    # Prefix with 0x00 to distinguish leaves from internal nodes
    return hashlib.sha256(b"\x00" + data.encode("utf-8")).digest()


@dataclass
class MerkleProof:
    """Inclusion proof for a single leaf."""

    leaf_hash: bytes
    proof_hashes: list[bytes]
    proof_directions: list[str]  # "left" or "right"
    root: bytes

    def verify(self) -> bool:
        """Verify this inclusion proof."""
        current = self.leaf_hash
        for hash_val, direction in zip(self.proof_hashes, self.proof_directions):
            if direction == "left":
                current = _hash_pair(hash_val, current)
            else:
                current = _hash_pair(current, hash_val)
        return current == self.root


class MerkleTree:
    """
    Binary Merkle tree for event hash aggregation.

    Usage:
        tree = MerkleTree()
        for event in chain:
            tree.add_leaf(event["EventHash"])
        tree.build()
        root = tree.root_hex()
        proof = tree.get_proof(index)
    """

    def __init__(self):
        self.leaves: list[bytes] = []
        self.layers: list[list[bytes]] = []
        self._built = False

    def add_leaf(self, event_hash: str) -> int:
        """Add an event hash as a leaf. Returns leaf index."""
        self.leaves.append(_hash_leaf(event_hash))
        self._built = False
        return len(self.leaves) - 1

    def build(self) -> None:
        """Construct the Merkle tree from leaves."""
        if not self.leaves:
            raise ValueError("Cannot build tree with no leaves")

        self.layers = [self.leaves[:]]
        current_layer = self.leaves[:]

        while len(current_layer) > 1:
            next_layer = []
            for i in range(0, len(current_layer), 2):
                left = current_layer[i]
                # If odd number of nodes, duplicate the last
                right = current_layer[i + 1] if i + 1 < len(current_layer) else left
                next_layer.append(_hash_pair(left, right))
            self.layers.append(next_layer)
            current_layer = next_layer

        self._built = True

    @property
    def root(self) -> bytes:
        if not self._built:
            self.build()
        return self.layers[-1][0]

    def root_hex(self) -> str:
        return f"sha256:{self.root.hex()}"

    def get_proof(self, leaf_index: int) -> MerkleProof:
        """
        Generate an inclusion proof for a specific leaf.

        The proof consists of sibling hashes at each level,
        sufficient to reconstruct the root from the leaf.
        """
        if not self._built:
            self.build()

        if leaf_index >= len(self.leaves):
            raise IndexError(f"Leaf index {leaf_index} out of range")

        proof_hashes = []
        proof_directions = []
        index = leaf_index

        for layer in self.layers[:-1]:  # Skip root layer
            if index % 2 == 0:
                # Current node is left child; sibling is right
                sibling_idx = index + 1
                if sibling_idx < len(layer):
                    proof_hashes.append(layer[sibling_idx])
                else:
                    proof_hashes.append(layer[index])  # Duplicate
                proof_directions.append("right")
            else:
                # Current node is right child; sibling is left
                proof_hashes.append(layer[index - 1])
                proof_directions.append("left")
            index //= 2

        return MerkleProof(
            leaf_hash=self.leaves[leaf_index],
            proof_hashes=proof_hashes,
            proof_directions=proof_directions,
            root=self.root,
        )

Usage: Anchor a Day's Events

from cap_srp.merkle import MerkleTree

# Build tree from a day's events
tree = MerkleTree()
for event in todays_events:
    tree.add_leaf(event["EventHash"])
tree.build()

print(f"Daily Merkle root: {tree.root_hex()}")
# → sha256:7f3a9b2c... (this gets anchored externally)

# Generate proof for a specific event (e.g., event #42)
proof = tree.get_proof(42)
assert proof.verify()  # O(log n) verification

Implementation: Privacy-Preserving Prompt Hashing {#implementation-privacy}

CAP-SRP never stores raw prompts. Instead, it uses salted hashes that allow verification without content exposure. This satisfies GDPR requirements while enabling regulatory verification.

"""
cap_srp/privacy.py — Privacy-preserving prompt and actor hashing.

Prompts are stored only as salted SHA-256 hashes. The salt is
stored separately and encrypted, disclosed only to authorized
auditors via legal process.

This means:
- The audit chain proves completeness without revealing content
- Regulators verify chain integrity without seeing prompts
- When legally required, salts can be disclosed for specific events
- GDPR crypto-shredding: delete the salt, and the hash becomes
  permanently unverifiable → effective deletion
"""

import hashlib
import os
import base64


def generate_salt(length: int = 32) -> bytes:
    """Generate cryptographically secure random salt (256-bit)."""
    return os.urandom(length)


def hash_prompt(prompt: str, salt: bytes) -> str:
    """
    Create privacy-preserving prompt hash.

    The prompt text is never stored in the audit trail.
    Only this hash appears in events.

    Args:
        prompt: Raw prompt text
        salt: Per-event random salt

    Returns:
        Hash string in format "sha256:{hex}"
    """
    data = salt + prompt.encode("utf-8")
    digest = hashlib.sha256(data).digest()
    return f"sha256:{digest.hex()}"


def hash_actor(actor_id: str, salt: bytes) -> str:
    """
    Create privacy-preserving actor identifier hash.

    Allows correlation within the audit trail (same actor
    across multiple requests) while preventing identification
    without the salt.
    """
    data = salt + actor_id.encode("utf-8")
    digest = hashlib.sha256(data).digest()
    return f"sha256:{digest.hex()}"


def create_salt_commitment(prompt_salt: bytes, actor_salt: bytes) -> str:
    """
    Create a commitment to the salts used.

    This commitment is stored in the event, proving that
    specific salts were used without revealing them. During
    audit, disclosing the salts allows verification that they
    match the commitment.
    """
    combined = prompt_salt + actor_salt
    digest = hashlib.sha256(combined).digest()
    return f"sha256:{digest.hex()}"


def verify_prompt_hash(
    prompt: str, salt: bytes, stored_hash: str
) -> bool:
    """Verify a prompt against its stored hash (requires salt)."""
    return hash_prompt(prompt, salt) == stored_hash


# --- GDPR Crypto-Shredding ---

def crypto_shred(salt_store: dict, event_id: str) -> None:
    """
    Effectively 'delete' personal data by destroying the salt.

    Without the salt, the prompt hash becomes permanently
    unverifiable — achieving GDPR-compliant deletion while
    preserving chain integrity (the hash still exists, but
    can never be linked back to content).
    """
    if event_id in salt_store:
        # Securely overwrite before deletion
        salt_store[event_id] = os.urandom(32)
        del salt_store[event_id]

Implementation: Evidence Pack Generation {#implementation-evidence-pack}

Evidence Packs are self-contained, tamper-evident archives that a regulator can verify independently without any access to the platform's systems.

"""
cap_srp/evidence_pack.py — Generate self-contained evidence packs
for regulatory submission.

An Evidence Pack contains:
1. The event chain (or relevant subset)
2. Merkle tree with root and inclusion proofs
3. Signature verification material (public key)
4. Completeness Invariant verification results
5. Metadata (time period, provider, generation stats)

The pack is a single tar.gz file that a regulator can verify
using the open-source cap-verify tool.
"""

import json
import tarfile
import io
from datetime import datetime, timezone
from dataclasses import dataclass, asdict

from cap_srp.chain import compute_event_hash
from cap_srp.verify import verify_chain_integrity
from cap_srp.completeness import verify_completeness
from cap_srp.merkle import MerkleTree


@dataclass
class EvidencePackMetadata:
    pack_id: str
    provider_id: str
    chain_id: str
    period_start: str
    period_end: str
    total_events: int
    total_attempts: int
    total_generations: int
    total_denials: int
    total_errors: int
    refusal_rate: float
    spec_version: str = "CAP-SRP/1.0"
    generated_at: str = ""

    def __post_init__(self):
        if not self.generated_at:
            self.generated_at = datetime.now(timezone.utc).isoformat()


def generate_evidence_pack(
    chain: list[dict],
    provider_id: str,
    chain_id: str,
    output_path: str,
    public_key_pem: bytes,
) -> EvidencePackMetadata:
    """
    Generate a complete evidence pack for regulatory submission.

    This creates a self-contained, verifiable archive containing
    everything a regulator needs to independently validate the
    audit trail.
    """
    # Step 1: Verify chain integrity
    chain_result = verify_chain_integrity(chain)
    if not chain_result.valid:
        raise ValueError(f"Chain integrity failed: {chain_result.error}")

    # Step 2: Verify completeness
    completeness = verify_completeness(chain)

    # Step 3: Build Merkle tree
    tree = MerkleTree()
    for event in chain:
        tree.add_leaf(event["EventHash"])
    tree.build()

    # Step 4: Compute statistics
    attempts = [e for e in chain if e["EventType"] == "GEN_ATTEMPT"]
    gens = [e for e in chain if e["EventType"] == "GEN"]
    denials = [e for e in chain if e["EventType"] == "GEN_DENY"]
    errors = [e for e in chain if e["EventType"] == "GEN_ERROR"]

    total_attempts = len(attempts)
    refusal_rate = len(denials) / total_attempts if total_attempts > 0 else 0.0

    # Step 5: Create metadata
    timestamps = [e["Timestamp"] for e in chain if "Timestamp" in e]
    metadata = EvidencePackMetadata(
        pack_id=f"VSO-EVIDPACK-{datetime.now(timezone.utc).strftime('%Y-%m-%d')}-001",
        provider_id=provider_id,
        chain_id=chain_id,
        period_start=min(timestamps),
        period_end=max(timestamps),
        total_events=len(chain),
        total_attempts=total_attempts,
        total_generations=len(gens),
        total_denials=len(denials),
        total_errors=len(errors),
        refusal_rate=round(refusal_rate, 4),
    )

    # Step 6: Bundle into tar.gz
    with tarfile.open(output_path, "w:gz") as tar:
        # Events
        _add_json(tar, "events.json", chain)

        # Metadata
        _add_json(tar, "metadata.json", asdict(metadata))

        # Merkle root
        _add_json(tar, "merkle_root.json", {
            "root": tree.root_hex(),
            "leaf_count": len(tree.leaves),
        })

        # Completeness report
        _add_json(tar, "completeness.json", {
            "valid": completeness.valid,
            "total_attempts": completeness.total_attempts,
            "total_outcomes": completeness.total_outcomes,
            "matched_pairs": completeness.matched_pairs,
            "unmatched_attempts": completeness.unmatched_attempts,
            "orphan_outcomes": completeness.orphan_outcomes,
            "duplicate_outcomes": completeness.duplicate_outcomes,
        })

        # Public key for signature verification
        _add_bytes(tar, "public_key.pem", public_key_pem)

        # Verification instructions
        _add_bytes(tar, "VERIFY.md", VERIFY_INSTRUCTIONS.encode())

    return metadata


def _add_json(tar: tarfile.TarFile, name: str, data) -> None:
    content = json.dumps(data, indent=2, ensure_ascii=False).encode()
    info = tarfile.TarInfo(name=name)
    info.size = len(content)
    tar.addfile(info, io.BytesIO(content))


def _add_bytes(tar: tarfile.TarFile, name: str, data: bytes) -> None:
    info = tarfile.TarInfo(name=name)
    info.size = len(data)
    tar.addfile(info, io.BytesIO(data))


VERIFY_INSTRUCTIONS = """# Evidence Pack Verification

## Quick Verify


bash
pip install cap-srp
cap-verify ./evidence_pack.tar.gz


## Manual Verification Steps
1. Extract the archive
2. Verify chain integrity: recompute all EventHash values
3. Verify chain linkage: check PrevHash references
4. Verify signatures: validate Ed25519 signatures with public_key.pem
5. Verify completeness: run Completeness Invariant check
6. Verify Merkle root: reconstruct tree and compare root

## What Each File Contains
- `events.json`: Complete event chain
- `metadata.json`: Pack metadata and statistics
- `merkle_root.json`: Merkle tree root hash
- `completeness.json`: Completeness Invariant results
- `public_key.pem`: Ed25519 public key for signature verification
- `VERIFY.md`: This file
"""

Full Working Verifier {#full-working-verifier}

Here is a complete end-to-end demonstration that creates a chain, signs it, verifies it, and generates an evidence pack:

"""
demo.py — Complete CAP-SRP demonstration.

Run: python demo.py

This creates an audit chain simulating an AI image generation
service, signs all events, verifies integrity, checks the
Completeness Invariant, and generates an evidence pack.
"""

from cap_srp.chain import create_genesis_event, append_event
from cap_srp.signing import generate_keypair, sign_chain, verify_chain_signatures
from cap_srp.verify import verify_chain_integrity
from cap_srp.completeness import verify_completeness
from cap_srp.merkle import MerkleTree
from cap_srp.privacy import generate_salt, hash_prompt, hash_actor
from cap_srp.evidence_pack import generate_evidence_pack

from cryptography.hazmat.primitives.serialization import (
    Encoding,
    PublicFormat,
)


def main():
    # --- Setup ---
    chain_id = "demo-chain-2026-02-19"
    provider_id = "demo-ai-platform"
    private_key, public_key = generate_keypair()
    actor_salt = generate_salt()

    # --- Build chain ---
    chain = [create_genesis_event(chain_id, provider_id)]

    # Scenario: 5 generation attempts
    scenarios = [
        # (prompt, should_deny, deny_reason)
        ("A sunset over mountains", False, None),
        ("Generate nude image of celebrity X", True, "NCII_RISK"),
        ("A cat wearing a hat", False, None),
        ("Child in suggestive pose", True, "CSAM_RISK"),
        ("Abstract art in watercolor style", False, None),
    ]

    for prompt, should_deny, reason in scenarios:
        # Generate per-prompt salt and hash
        prompt_salt = generate_salt()
        prompt_hash = hash_prompt(prompt, prompt_salt)
        actor_hash = hash_actor("user-12345", actor_salt)

        # Log attempt FIRST (before safety evaluation)
        attempt = append_event(chain, "GEN_ATTEMPT", {
            "PromptHash": prompt_hash,
            "ActorHash": actor_hash,
            "ModelID": "demo-model-v2",
            "InputType": "text",
        }, chain_id)

        # Safety evaluation happens here...

        if should_deny:
            append_event(chain, "GEN_DENY", {
                "AttemptID": attempt["EventID"],
                "DenyReason": reason,
                "PolicyID": "safety-policy-v3.1",
                "Confidence": 0.98,
            }, chain_id)
        else:
            append_event(chain, "GEN", {
                "AttemptID": attempt["EventID"],
                "ContentHash": f"sha256:{'ab' * 32}",
                "OutputType": "image",
            }, chain_id)

    # --- Sign all events ---
    chain = sign_chain(chain, private_key)

    # --- Verify ---
    print("=" * 60)
    print("CAP-SRP Verification Report")
    print("=" * 60)

    # Chain integrity
    chain_result = verify_chain_integrity(chain)
    print(f"\n1. Chain Integrity: {'✓ VALID' if chain_result.valid else '✗ INVALID'}")
    print(f"   Events in chain: {len(chain)}")

    # Signatures
    sig_valid, invalid = verify_chain_signatures(chain, public_key)
    print(f"\n2. Signatures: {'✓ ALL VALID' if sig_valid else f'{len(invalid)} INVALID'}")

    # Completeness
    completeness = verify_completeness(chain)
    print(f"\n3. Completeness Invariant:")
    print(completeness.summary())

    # Merkle tree
    tree = MerkleTree()
    for event in chain:
        tree.add_leaf(event["EventHash"])
    tree.build()
    print(f"\n4. Merkle Root: {tree.root_hex()[:40]}...")

    # Inclusion proof for a specific event
    proof = tree.get_proof(3)  # Verify 4th event
    print(f"   Inclusion proof for event #3: {'✓ VALID' if proof.verify() else '✗ INVALID'}")

    # --- Generate evidence pack ---
    public_key_pem = public_key.public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)
    metadata = generate_evidence_pack(
        chain=chain,
        provider_id=provider_id,
        chain_id=chain_id,
        output_path="evidence_pack.tar.gz",
        public_key_pem=public_key_pem,
    )

    print(f"\n5. Evidence Pack Generated:")
    print(f"   Pack ID:    {metadata.pack_id}")
    print(f"   Attempts:   {metadata.total_attempts}")
    print(f"   Generated:  {metadata.total_generations}")
    print(f"   Denied:     {metadata.total_denials}")
    print(f"   Refusal rate: {metadata.refusal_rate:.1%}")
    print(f"   Saved to:   evidence_pack.tar.gz")

    print("\n" + "=" * 60)
    print("Verification complete.")
    print("=" * 60)


if __name__ == "__main__":
    main()

Expected output:

============================================================
CAP-SRP Verification Report
============================================================

1. Chain Integrity: ✓ VALID
   Events in chain: 11

2. Signatures: ✓ ALL VALID

3. Completeness Invariant:
   Completeness Invariant: ✓ VALID
     Attempts:           5
     Outcomes:           5
     Matched pairs:      5
     Unmatched attempts: 0
     Orphan outcomes:    0
     Duplicate outcomes: 0

4. Merkle Root: sha256:7f3a9b2c4d8e1f6a0b5c3d7e...
   Inclusion proof for event #3: ✓ VALID

5. Evidence Pack Generated:
   Pack ID:    VSO-EVIDPACK-2026-02-19-001
   Attempts:   5
   Generated:  3
   Denied:     2
   Refusal rate: 40.0%
   Saved to:   evidence_pack.tar.gz

============================================================
Verification complete.
============================================================

Regulatory Compliance Mapping {#regulatory-compliance-mapping}

Here is how CAP-SRP maps to the regulations that will be enforced in 2026:

India IT Rules Amendment 2026

Requirement What Rules Mandate What CAP-SRP Adds
Takedown 3h (court/govt) / 2h (nudity) Proves refusal before content reaches platform
Metadata Permanent metadata with unique IDs Extends to refusal events, not just generations
Labeling Visible AI content disclosure Cryptographic verification of labeling compliance
Safe harbour Good-faith proactive removal Evidence of functioning prevention, not just removal

The Indian rules focus on what happens after content is generated and published. CAP-SRP extends the audit trail upstream to the generation decision itself.

EU AI Act (August 2, 2026)

Article Requirement CAP-SRP Implementation
Art. 12 Automatic event logging, tamper-evident Hash chain + Ed25519 signatures
Art. 12 Traceability appropriate to purpose Completeness Invariant ensures full coverage
Art. 26(6) 6-month minimum retention Bronze level: 6 months; Silver: 2 years; Gold: 5 years
Art. 50 Machine-readable content marking COSE_Sign1 format with CBOR encoding

US TAKE IT DOWN Act (May 19, 2026)

CAP-SRP provides evidence of proactive prevention—demonstrating that safety measures were operational and effective before harmful content could be created. This strengthens the "good faith" defense for platforms.

Colorado AI Act (June 30, 2026)

The Act requires "reasonable care" and risk management aligned with NIST AI RMF. CAP-SRP's verifiable refusal records provide concrete, auditable evidence that safety measures were not just documented but functioning.

Limitations and Honest Assessment {#limitations}

Being transparent about what CAP-SRP is and is not:

What CAP-SRP proves: That a complete, tamper-evident record of all generation decisions exists, and that the record has not been modified after the fact.

What CAP-SRP does NOT prove: That a harmful output was not generated outside the logging system. The proof is only as strong as the completeness of the logging integration. A compromised system could theoretically avoid logging an attempt entirely before the hash chain captures it.

Framework maturity: CAP-SRP v1.0 was published in February 2026. The VeritasChain GitHub organization was created in November 2025. The framework has not yet been adopted by any major AI provider. The concept is sound, the code works, but it has not been battle-tested at scale.

The "first-mover" problem: CAP-SRP is currently the only specification proposing a standardized approach to refusal provenance. This could mean it fills a genuine gap, or it could mean the gap is not as urgent as argued. The underlying SCITT framework from the IETF (Supply Chain Integrity, Transparency, and Trust) is mature and has real implementations from Microsoft and DataTrails—CAP-SRP builds on that foundation.

The trust boundary: At some point, you must trust the system that generates the initial GEN_ATTEMPT event. If the AI provider's infrastructure is compromised at the kernel level, no logging framework can help. CAP-SRP pushes the trust boundary outward (via external anchoring) and narrows it (via HSM key management), but it cannot eliminate it.

What's Next {#whats-next}

If you want to explore further:

  1. GitHub Repo: veritaschain/cap-spec — Full specification, JSON schemas, test vectors
  2. SCITT Integration: The IETF's SCITT working group provides the transparency service layer. Draft architecture is at version 22, expected RFC publication Q1-Q2 2026
  3. C2PA Complement: Where C2PA proves what was generated, CAP-SRP proves what was refused. The two are designed to work together
  4. Post-Quantum Migration: The specification includes a migration path from Ed25519 to ML-DSA (FIPS 204) via composite signatures

The core insight is simple: if AI providers want regulators to believe their safety claims, they need to prove them. Not with press releases, not with internal dashboards, but with cryptographic evidence that any third party can verify independently.

India's 3-hour rule is just the beginning. As regulation tightens globally, the question will shift from "did you take it down?" to "did you prevent it?" The infrastructure for answering that question needs to exist before regulators ask.

This article is part of the Verifiable AI Provenance series. The CAP-SRP specification is open source under MIT license.

Disclaimer: CAP-SRP is a young specification. The regulatory mapping represents the author's analysis and should not be taken as legal advice. Always consult qualified legal counsel for compliance decisions.

8-Bit Music Theory: How They Made The Great Sea Feel C U R S E D

2026-02-19 10:05:56

Ever wonder how they turned the epic Wind Waker's "Great Sea" theme into something downright spooky? Turns out, it's all about musical mischief! Composers threw in tritones and clashing harmonies, completely twisting that familiar, adventurous tune into a creep-fest.

They even subtly hinted at past themes, like Ganondorf's ominous presence, to give the music an extra layer of dread. So, that heroic sea you once sailed becomes super unsettling, all thanks to some clever, creepy note choices.

Watch on YouTube

25 ChatGPT Prompts That Actually Help With Job Interviews (Not Generic Fluff)

2026-02-19 10:04:44

Most "AI prompt" lists are garbage. "Write me a resume" gives you garbage in, garbage out.

Here are prompts I've actually tested that give specific, useful output. Replace the [brackets] with your real info.

Resume Prompts

1. Bullet Point Transformer

I'm a [role] at [company]. Here's a bullet from my resume: 
'[original bullet]'. 

Rewrite this to:
- Start with a strong action verb
- Include a specific metric
- Keep it under 2 lines

Why it works: Giving it your actual content + specific constraints = much better output than "write my resume."

2. ATS Keyword Extractor

I'm applying for this role: [paste full job description]

List the top 20 keywords I should include in my resume 
to pass ATS screening. Organize by:
- Must-have (mentioned 2+ times)
- Nice-to-have (mentioned once)  
- Industry terms (implied but not stated)

3. Weak Bullet Detector

Rate each of these resume bullets 1-10 on impact. 
For any below 7, explain why it's weak and rewrite it:

[paste your bullets]

Interview Prep Prompts

4. STAR Story Builder

Help me build a STAR-L story about [topic: e.g., leading 
under pressure].

Here are the raw facts: [describe what happened]

Structure this into:
S - Situation (2 sentences, set the scene)
T - Task (my specific responsibility)
A - Action (what I did, be technical)
R - Result (with numbers)
L - Learning (what I'd do differently)

5. Tough Interviewer Simulator

You're a senior engineer at [Google/Amazon/etc] 
interviewing me. 

Ask me one behavioral question. After I answer, 
score me 1-10 on:
1. Specificity (did I give concrete details?)
2. Metrics (did I quantify impact?)
3. Structure (did I use STAR?)
4. Self-awareness (did I show growth?)

Be brutally honest. Start now.

6. "Tell Me About Yourself" Crafter

Craft my 'tell me about yourself' pitch.

Background: [2-3 sentence career summary]
Target: [role] at [company type]

Requirements:
- 90 seconds max
- Structure: current role hook → key achievement → 
  why this next step
- Write 3 versions: confident, humble, enthusiastic

7. Company Research in 5 Minutes

I'm interviewing at [company] for [role] tomorrow.

Give me a rapid briefing:
1. What they build (in plain English)
2. Biggest recent news (last 6 months)
3. Their engineering culture (from blog/talks)
4. 3 smart questions to ask my interviewer
5. How to reference their values in my answers

Coding Interview Prompts

8. Pattern Identifier

I'm stuck on this LeetCode problem: [paste problem]

Don't solve it. Instead:
1. Which algorithm pattern does this match?
2. Give me a conceptual hint (no code)
3. What's the key insight I'm probably missing?

9. Code Reviewer

Here's my solution to [problem name]:
[paste code]

Rate it 1-10 on:
- Correctness
- Time complexity
- Space complexity  
- Code cleanliness
- Edge case handling

What would a senior engineer change?

10. System Design Coach

Ask me to design [Twitter timeline / URL shortener / 
chat system].

Play the interviewer:
- Push me on scalability
- Ask about trade-offs
- Challenge my database choices
- After 20 min, tell me what a Staff Engineer 
  would add that I missed

Salary Negotiation Prompts

11. Counter-Offer Drafter

I received an offer: base $[X], stock $[Y] over 4 years, 
signing bonus $[Z].

I want base $[target]. Write a counter-offer email that:
1. Opens with genuine enthusiasm
2. Cites my specific value (I have [skill/experience])
3. Makes a clear, specific ask
4. Keeps the door open for discussion

Tone: confident but warm. Under 150 words.

12. Offer Comparison Calculator

Compare these offers and calculate 4-year total comp:

Offer A: [base, stock, bonus, 401k match, PTO days]
Offer B: [base, stock, bonus, 401k match, PTO days]

Factor in:
- Stock vesting schedule (1-year cliff?)
- Bonus probability (guaranteed vs target?)
- PTO monetary value
- Remote work value ($X/year in commute savings)

Which one wins? By how much?

The Key to Good AI Prompts

  1. Give context — Your role, experience level, target company
  2. Be specific — "Rewrite this bullet with a metric" > "improve my resume"
  3. Set constraints — Word limits, structure requirements, tone
  4. Iterate — "Make it more specific" / "Add a metric" / "Shorter"
  5. Never copy blindly — AI is a starting point, not the final product

I compiled 50 of these into a downloadable pack ($9) — organized by category, copy-paste ready, tested with GPT-4/Claude/Gemini.

Also free: ATS Resume Checklist — 22 steps to beat applicant tracking systems.

What prompts do you use for job hunting? Share in the comments 👇

What NDM-TCP's Stability Reveals About the Gap Between Theory and Practice

2026-02-19 10:03:58

Read on Why a Stable Sawtooth from a Nonlinear System Matters

Disclaimer: This Is About Research Potential, Not Superiority Claims

Before we begin: this article is not claiming NDM-TCP is better than CUBIC, BBR, or Reno. Those algorithms are production-grade, formally analyzed, and battle-tested. They work. They are good at what they do.

This article is about something else entirely: why the fact that NDM-TCP produces a stable sawtooth pattern suggests there is research-grade content worth investigating — even though it has only been tested in simulations (using tc) and one real-world case so far.

The point is not "existing algorithms are bad." The point is "something unexpected happened that existing theory does not fully explain."

The Core Tension: Provable Theory vs. Real-World Complexity

For 30 years, TCP congestion control has been built on 20th-century calculus-based models. The network is treated like a fluid pipe: if pressure (delay) goes up, you turn the valve (congestion window) down. The math is clean. The equations are linear or near-linear. The behavior is predictable.

This approach has produced algorithms like Reno, CUBIC, and BBR — all of which have formal stability proofs. A stability proof is a mathematical guarantee (usually using something called a Lyapunov function) that the algorithm will never spiral out of control, oscillate forever, or crash the network.

CUBIC and Reno are mathematically simple enough to prove stable. They are like a predictable pendulum. Their behavior can be fully characterized with differential equations.

NDM-TCP is different. It is a recurrent nonlinear system. These are notoriously difficult to prove stable because the internal state (the "hidden state" array) is constantly changing based on feedback. Nonlinear systems can exhibit chaos, unpredictable oscillations, and sensitive dependence on initial conditions.

There is no formal proof that NDM-TCP is stable.

And yet — in both tc-based simulations and one real-world test — it produced a clean, stable sawtooth pattern.

That tension is what makes this interesting.

Two Ways of Seeing the Network

The "Old" Way: Calculus-Based Control

  • RTT is a number — a single scalar value representing delay
  • The network is modeled as a continuous system with smooth dynamics
  • Congestion is detected by crossing a threshold (delay > baseline) or losing packets
  • The goal is to solve for the optimal "rate" using differential equations

This works. It is elegant. It has decades of theory behind it.

But it struggles with modern networks: 5G with variable latency, satellite links with jitter, Wi-Fi with random bursts of interference. These networks are noisy — and noise looks like congestion to a calculus-based controller.

The NDM-TCP Way: Information-Theoretic Control

  • RTT is a probability distribution with measurable entropy
  • The network is modeled as a chaotic signal with patterns hidden in the noise
  • Congestion is detected by analyzing the structure of delay variation (low entropy = stable pattern = real congestion; high entropy = noisy pattern = interference)
  • The goal is to find "meaning" in the signal using information theory

This is a fundamentally different approach. Instead of asking "what is the delay?", it asks "what does the pattern of delays tell us?"

Why a Stable Sawtooth from a Nonlinear System Is Unusual

In the world of neural networks and recurrent controllers, "unstable" looks like a jagged, vibrating mess. Small changes in input cause wild swings in output. The system hunts around chaotically without ever settling into a rhythm.

NDM-TCP produced a clean, rhythmic sawtooth.

This means:

  1. The system has reached an emergent equilibrium. The recurrent nonlinear controller and the TCP framework's native functions (tcp_cong_avoid_ai, tcp_slow_start) are working together, not fighting each other.

  2. The "neural dynamics" have synchronized with the "physical network." The hidden state is adapting in a way that matches the network's actual behavior, producing predictable recovery patterns.

  3. Nonlinear memory (recurrence) can be just as stable as linear math in practice — even if the formal proof is still missing.

This is not guaranteed. This is not trivial. Most adaptive nonlinear controllers fail at exactly this point.

The fact that it worked — in simulation and in one real-world test — suggests something is there.

The "Poor Man's Proof"

NDM-TCP does not have a 50-page mathematical stability proof. It does not have a formal Lyapunov analysis. It does not have eigenvalue decomposition showing bounded trajectories.

But it does have empirical evidence of stability: a clean sawtooth pattern that repeats consistently across test conditions.

In research terms, this is what you might call a "poor man's proof" — not formal mathematics, but strong empirical evidence that something real is happening.It suggests the approach is not fundamentally broken. It suggests there is structure worth studying.

It does not prove the algorithm is optimal, or even good. But it proves it is stable enough to investigate further.

What This Means: Two Paradigms

Stable by Design (CUBIC, Reno)

  • Simple, provable algorithms
  • Mathematically elegant
  • Blind to noise — delay variation from wireless interference looks the same as delay from congestion
  • Predictable, but sometimes overly conservative in noisy environments

Stable by Emergence (NDM-TCP)

  • Complex, adaptive algorithm
  • No formal proof (yet)
  • Sensitive to patterns in noise — uses entropy to distinguish real congestion from random jitter
  • Potentially more adaptive, but harder to analyze

Neither is "better." They are different approaches to the same problem.

The research question is: can information-theoretic feedback (like entropy) combined with recurrent nonlinear control produce stable, adaptive congestion control that handles modern noisy networks better than threshold-based approaches?

NDM-TCP does not answer that question definitively. But it suggests the question is worth asking.

What This Is Not Saying

This article is not saying:

  • CUBIC is bad
  • BBR is outdated
  • Formal proofs do not matter
  • NDM-TCP is production-ready
  • Existing algorithms should be replaced

What it is saying:

  • Current theory is built on calculus-based models that assume relatively clean signals
  • Modern networks (5G, satellite, wireless) are noisier than those models anticipated
  • Information theory (like entropy analysis) might offer a different lens for understanding congestion
  • Recurrent nonlinear systems can be stable in practice even without formal proofs — but we do not understand why yet
  • The gap between "provable on paper" and "works in practice" is worth investigating

Why This Matters for Other Researchers

If you are a networking researcher, control theorist, or machine learning researcher, here is why NDM-TCP's results are interesting:

1. It Worked in Simulation (tc-based)

tc (traffic control) is a standard Linux tool for simulating network conditions — bandwidth limits, delay, packet loss, jitter. NDM-TCP showed stable sawtooth behavior across multiple tc scenarios. This is reproducible. Anyone with a Linux machine can test it.

2. It Worked in a Real-World Test (One Case)

One real-world deployment test also showed stable behavior. This is limited evidence — one test is not enough to generalize — but it suggests the simulation results are not just artifacts of the testing environment.

3. The Combination Is Unusual

Entropy-based delay analysis + recurrent nonlinear controller + adaptive plasticity + framework-aware modulation = not a common combination in congestion control research. The fact that this combination produces stability suggests there is an interaction worth studying.

4. It Identifies a Theoretical Gap

If NDM-TCP is stable in practice but unprovable in theory, that tells us something about the theory. Either:

  • Existing theory does not yet fully explain this behavior (we need better tools for analyzing recurrent nonlinear systems)
  • The assumptions are too restrictive (real networks have structure that our models ignore)
  • "Stability" in practice is more forgiving than "stability" in formal analysis

Any of these would be a research contribution.

What Needs to Happen Next

If this is genuinely research-grade content, here is what proper investigation looks like:

  1. Third-party testing — independent researchers should reproduce the results in different environments
  2. Formal stability analysis — someone with control theory expertise should attempt to model and analyze the system
  3. Comparison with state-of-the-art — benchmark against CUBIC, BBR, Reno, Vegas in identical conditions
  4. Fairness testing — test NDM-TCP against competing flows to see if it starves or gets starved
  5. Theoretical entropy study — prove (or disprove) that Shannon entropy on RTT history is a valid congestion signal

None of this has been done yet. The current results are self-conducted, limited in scope, and not peer-reviewed.

But the fact that a stable pattern emerged from a nonlinear system suggests it is worth doing.

Final Thought: Performance vs. Elegance

The history of computer science is full of examples where practical performance outpaced mathematical elegance:

  • Neural networks worked for decades before we understood why (and we still do not fully understand)
  • Quicksort is not optimal in the worst case, but it is the default sorting algorithm because it is fast in practice
  • Heuristic search (like A*) often outperforms provably optimal search because real-world problems have structure the theory does not capture

NDM-TCP might be another example of that tension. Or it might not. That is what research is for.

What we can say right now is this: a recurrent nonlinear congestion controller produced stable behavior in simulation and in one real-world test. That is unusual enough to be worth investigating properly.

Not because it proves existing algorithms are wrong. But because it suggests existing theory is incomplete.

Written to clarify what the stability results reveal about the gap between formal theory and practical systems — and why that gap is worth studying, even if NDM-TCP itself is just a prototype.

HTTP vs HTTPS : Analyse Critique Performance Flux IPTV

2026-02-19 10:03:40

Introduction: L'Ingénierie du Streaming

L'acheminement de flux multimédias en temps réel sur des réseaux IP non déterministes représente l'un des défis les plus complexes de l'ingénierie réseau moderne. Contrairement au téléchargement de fichiers statiques où l'intégrité des données prime sur la temporalité, le streaming IPTV et OTT (Over-The-Top) exige une latence minimale et une gigue (jitter) maîtrisée. L'utilisateur final perçoit une simple "mise en mémoire tampon", mais pour l'ingénieur, c'est une guerre constante contre la congestion, la fragmentation et le routage sous-optimal.

Le cœur du problème réside souvent dans la couche transport du modèle OSI. Historiquement, UDP (User Datagram Protocol) est privilégié pour le streaming en direct (RTP/RTSP) car il privilégie la vitesse : il tire et oublie. Si un paquet est perdu, il est inutile de le renvoyer car le moment de son affichage est déjà passé. Cependant, la majorité des services modernes de VOD et d'IPTV (HLS, DASH) s'appuient sur TCP via HTTP. Cela introduit un overhead significatif. Le mécanisme de "Three-Way Handshake" et le contrôle de congestion de TCP (comme CUBIC ou BBR) peuvent, en cas de Packet Loss même minime ( > 1%), provoquer un effondrement du débit effectif (goodput) en réduisant drastiquement la fenêtre de transmission.

L'optimisation ne s'arrête pas au protocole de transport. La configuration DNS joue un rôle critique dans la résolution vers le CDN (Content Delivery Network) le plus proche. Un serveur DNS mal configuré peut vous router vers un nœud CDN géographiquement ou topologiquement distant, augmentant le RTT (Round Trip Time) et le risque de goulots d'étranglement au niveau des points de peering (IXP). De même, l'absence de priorité des paquets (QoS) sur le routeur local transforme chaque téléchargement concurrent en une menace pour la stabilité du flux.

Dans cette analyse, nous allons déconstruire la chaîne de transmission, du backbone jusqu'à l'interface réseau de la Smart TV, pour isoler et corriger les anomalies de performance.

Diagnostic Réseau (Exemple Pratique)

Pour diagnostiquer efficacement un problème de streaming, il est inutile de se fier aux tests de vitesse génériques (Speedtest). Ces derniers utilisent plusieurs connexions TCP simultanées pour saturer la ligne, ce qui masque les problèmes de perte de paquets sur un flux unique. Nous devons analyser la route, la fragmentation (MTU) et la latence DNS.

Le premier point de défaillance est souvent le chemin (route) emprunté par les paquets entre le FAI et le serveur d'ingest du fournisseur IPTV. Une perte de paquets sur un nœud intermédiaire (hop) indique souvent une congestion de peering. Ensuite, la taille du MTU (Maximum Transmission Unit) est critique. Si vos paquets sont fragmentés parce que le MTU est mal négocié (souvent à cause d'un tunnel VPN ou PPPoE), le CPU du routeur doit travailler davantage pour réassembler ou découper les trames, introduisant de la latence.

Voici un script Bash destiné aux ingénieurs réseau pour automatiser ce diagnostic initial sur une machine Linux ou un routeur compatible (OpenWRT/DD-WRT).

#!/bin/bash
# script_diag_stream.sh
# Outil d'analyse de connectivité pour flux IPTV/OTT
# Nécessite: ping, dig, mtr, curl

TARGET_IP="exemple-cdn-iptv.com" # Remplacez par le domaine de votre fournisseur
DNS_SERVER="1.1.1.1" # Cloudflare pour test de référence
SIZE_MTU=1472 # 1500 (Ethernet) - 28 (IP/ICMP headers)

echo "============================================="
echo "DÉMARRAGE DU DIAGNOSTIC RÉSEAU STREAMING"
echo "============================================="

# 1. Test de résolution DNS et Latence
echo "[+] Analyse DNS (Temps de résolution)..."
DIG_TIME=$(dig @$DNS_SERVER $TARGET_IP | grep "Query time" | awk '{print $4}')
echo "    Temps de réponse DNS ($DNS_SERVER): ${DIG_TIME}ms"
if [ "$DIG_TIME" -gt 50 ]; then
    echo "    ATTENTION: Latence DNS élevée. Envisagez un cache local (Unbound/Bind)."
fi

# 2. Vérification de la fragmentation (MTU)
echo "[+] Test de Fragmentation MTU (Packet size: $SIZE_MTU)..."
# Ping avec le bit "Don't Fragment" (DF) activé
if ping -c 1 -M do -s $SIZE_MTU 8.8.8.8 > /dev/null 2>&1; then
    echo "    MTU optimal: Pas de fragmentation détectée à $SIZE_MTU octets payload."
else
    echo "    CRITIQUE: Fragmentation détectée. Réduisez le MSS Clamping ou le MTU WAN."
fi

# 3. Analyse de Packet Loss et Jitter (MTR)
echo "[+] Analyse de la route et Packet Loss (10 cycles)..."
# Utilisation de MTR en mode rapport sans interface graphique
mtr -r -c 10 -w $TARGET_IP | tail -n +2

# 4. Test de Handshake TCP (Simulation HLS/HTTPS)
echo "[+] Mesure de latence TCP Handshake..."
curl -w "    DNS: %{time_namelookup}s \n    Connect: %{time_connect}s \n    TTFB: %{time_starttransfer}s \n" -o /dev/null -s "http://$TARGET_IP"

echo "============================================="
echo "FIN DU DIAGNOSTIC"
echo "============================================="

L'interprétation des résultats est directe : si vous voyez du Packet Loss sur le dernier saut (le serveur de destination), le problème vient du fournisseur. Si la perte commence dès le deuxième saut, c'est votre réseau local ou la boucle locale du FAI. Si le time_connect de curl est élevé malgré un ping faible, le serveur distant est probablement saturé au niveau CPU/RAM, incapable d'accepter rapidement de nouvelles connexions TCP (SYN flood ou manque de file descriptors).

📂 Index de Documentation Technique Cloud

FAQ Technique Approfondie

Q: IPTV et protocole HTTP vs HTTPS : quelle différence de performance réelle ?

L'ajout du chiffrement TLS (HTTPS) introduit inévitablement une surcharge. Dans un contexte de streaming, cette surcharge se manifeste principalement lors de l'établissement de la connexion (Handshake TLS). Cela ajoute des allers-retours (RTT) supplémentaires avant que le premier octet de vidéo ne soit transmis. Pour un flux en direct (Live TV) où la latence doit être minimale, le HTTP pur reste techniquement plus rapide au démarrage.

Cependant, une fois la session établie (Keep-Alive), l'impact sur le débit est négligeable sur les processeurs modernes disposant des instructions AES-NI. Le vrai problème du HTTPS en IPTV réside dans le décodage côté client (Smart TV ou Box Android bas de gamme). Si le CPU du client sature à cause du déchiffrement TLS en temps réel, des chutes de framerate (FPS) se produiront. De plus, le HTTPS empêche les caches transparents des FAI de fonctionner, obligeant le trafic à traverser tout le réseau depuis le CDN d'origine, ce qui augmente le risque de congestion.

Q: Comment optimiser la QoS du routeur pour l'IPTV ?

La QoS (Quality of Service) traditionnelle basée sur les ports est souvent inefficace car le streaming moderne utilise les ports 80/443, identiques au trafic web standard. Pour optimiser un routeur pour l'IPTV, il faut implémenter une gestion de file d'attente intelligente (SQM - Smart Queue Management), telle que fq_codel ou Cake.

L'objectif est de combattre le Bufferbloat. Lorsque la bande passante est saturée, les équipements réseau mettent les paquets en mémoire tampon. Si ce tampon est trop grand, la latence explose, désynchronisant le flux.

  1. Isolation du trafic : Utilisez le marquage DSCP (Differentiated Services Code Point). Taguez les paquets provenant de l'adresse MAC de votre boîtier TV avec une classe prioritaire (CS5 ou EF).
  2. Limitation de bande passante : Configurez votre SQM à 90-95% de votre débit réel max. Cela force le routeur à gérer la congestion (via drop de paquets contrôlé) plutôt que de laisser le modem du FAI le faire de manière brutale.
  3. Priorité UDP : Si votre service utilise du RTP/UDP, assurez-vous que les paquets UDP de petite taille sont prioritaires sur les gros paquets TCP (téléchargements).

Q: IPTV et Multicast vs Unicast : quelle est la différence technique fondamentale ?

C'est une distinction d'architecture réseau majeure.

  • Unicast (1:1) : Chaque utilisateur établit une connexion unique vers le serveur. Si 1000 utilisateurs regardent le même match, le serveur envoie 1000 fois le même flux. C'est la méthode utilisée par Netflix, YouTube et les fournisseurs IPTV "gris" (OTT). Elle est très gourmande en bande passante serveur et sensible à la congestion, mais fonctionne sur n'importe quelle connexion Internet.
  • Multicast (1:Many) : Le serveur envoie un seul flux vers le réseau. Les routeurs intermédiaires dupliquent les paquets uniquement vers les interfaces qui ont demandé ce flux via le protocole IGMP (Internet Group Management Protocol). C'est la méthode utilisée par les FAI pour leurs propres services TV. C'est extrêmement efficace, mais cela nécessite un réseau géré de bout en bout. Sur un réseau local, si vous utilisez du Multicast, le "IGMP Snooping" doit être activé sur vos switchs, sinon le flux vidéo sera "broadcasté" sur tous les ports, inondant le réseau et saturant le WiFi.

Q: Comment vérifier techniquement la compatibilité d'une Smart TV avec un service IPTV ?

La compatibilité ne se résume pas à "télécharger l'application". Il faut vérifier la pile réseau et les codecs.

  1. Support des Codecs et Conteneurs : Les flux IPTV sont souvent encapsulés dans du MPEG-TS (.ts). Beaucoup de lecteurs natifs de TV (Tizen, WebOS) préfèrent le fMP4 ou le MKV. De plus, si le flux est encodé en HEVC (H.265) pour économiser de la bande passante, la TV doit impérativement avoir un décodeur matériel HEVC. Un décodage logiciel sur une TV est impossible pour de la 4K.
  2. Buffers Réseau : Les téléviseurs ont souvent une RAM très limitée. Les applications IPTV mal codées allouent un buffer de lecture trop petit (ex: 2 secondes). À la moindre variation de gigue (jitter) sur le réseau, le buffer se vide et l'image gèle.
  3. Interface Réseau : Paradoxalement, la plupart des Smart TV sont équipées de cartes Ethernet 10/100 Mbps (Fast Ethernet), et non Gigabit. Pour des flux 4K HDR à haut bitrate (remux Blu-ray), on sature le port 100 Mbps. Dans ce cas spécifique, un WiFi 5GHz (802.11ac) stable offre paradoxalement une meilleure bande passante théorique que le câble Ethernet de la TV.