diff --git a/README.md b/README.md index 0ba48f8..e7b7a3c 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ internal even when the public site is available on Tor or I2P. - [Configuration](docs/configuration.md) - [Daemon management](docs/daemon-management.md) - [Fixture manifests](docs/fixture-manifests.md) +- [Interaction capabilities](docs/interaction-capabilities.md) - [Interactive applications](docs/interactive-apps.md) - [Wownero integration](docs/wownero-integration.md) - [Autodocs and drift gates](docs/autodocs.md) diff --git a/ampg/docsgen.py b/ampg/docsgen.py index 5deeccd..12d7d7a 100644 --- a/ampg/docsgen.py +++ b/ampg/docsgen.py @@ -2,7 +2,15 @@ from __future__ import annotations from pathlib import Path -from .metadata import CONFIG_FIELDS, DAEMON_ADAPTERS, RENDER_PROFILES +from .metadata import ( + CONFIG_FIELDS, + DAEMON_ADAPTERS, + IDENTITY_ADAPTERS, + INTERACTION_TIERS, + PAYMENT_ADAPTERS, + RENDER_PROFILES, + TRANSPORT_INTERACTION_CAPABILITIES, +) GENERATED_DIR = Path("docs/generated") @@ -12,6 +20,7 @@ def generate_docs(root: Path, *, check: bool = False) -> list[Path]: docs = { GENERATED_DIR / "config-schema.md": _config_schema_doc(), GENERATED_DIR / "daemon-adapters.md": _daemon_adapters_doc(), + GENERATED_DIR / "interaction-capabilities.md": _interaction_capabilities_doc(), GENERATED_DIR / "render-profiles.md": _render_profiles_doc(), } changed: list[Path] = [] @@ -83,3 +92,48 @@ def _render_profiles_doc() -> str: sections.append(f"- {default}") sections.append("") return "\n".join(sections) + + +def _interaction_capabilities_doc() -> str: + sections = [_header("Generated Interaction Capabilities")] + sections.append("## Tiers\n") + sections.append("| Tier | Summary | Examples | Notes |") + sections.append("| --- | --- | --- | --- |") + for tier in INTERACTION_TIERS: + sections.append( + f"| `{tier.name}` | {tier.summary} | {', '.join(tier.examples)} | " + f"{' '.join(tier.notes)} |" + ) + sections.append("") + + sections.append("## Identity Adapters\n") + sections.append("| Adapter | Status | Transports | Notes |") + sections.append("| --- | --- | --- | --- |") + for adapter in IDENTITY_ADAPTERS: + sections.append( + f"| `{adapter.name}` | `{adapter.status}` | {', '.join(adapter.transports)} | " + f"{' '.join(adapter.notes)} |" + ) + sections.append("") + + sections.append("## Payment Adapters\n") + sections.append("| Adapter | Status | Transports | Notes |") + sections.append("| --- | --- | --- | --- |") + for adapter in PAYMENT_ADAPTERS: + sections.append( + f"| `{adapter.name}` | `{adapter.status}` | {', '.join(adapter.transports)} | " + f"{' '.join(adapter.notes)} |" + ) + sections.append("") + + sections.append("## Transport Limits\n") + sections.append("| Transport | Public Max Tier | Identity | Payments | Realtime | Notes |") + sections.append("| --- | --- | --- | --- | --- | --- |") + for capability in TRANSPORT_INTERACTION_CAPABILITIES: + sections.append( + f"| `{capability.transport}` | `{capability.public_max_tier}` | " + f"{capability.identity} | {capability.payments} | {capability.realtime} | " + f"{' '.join(capability.notes)} |" + ) + sections.append("") + return "\n".join(sections) diff --git a/ampg/metadata.py b/ampg/metadata.py index 78f21bd..864d9c6 100644 --- a/ampg/metadata.py +++ b/ampg/metadata.py @@ -33,6 +33,40 @@ class DaemonAdapter: notes: tuple[str, ...] +@dataclass(frozen=True) +class InteractionTier: + name: str + summary: str + examples: tuple[str, ...] + notes: tuple[str, ...] + + +@dataclass(frozen=True) +class IdentityAdapter: + name: str + status: str + transports: tuple[str, ...] + notes: tuple[str, ...] + + +@dataclass(frozen=True) +class PaymentAdapter: + name: str + status: str + transports: tuple[str, ...] + notes: tuple[str, ...] + + +@dataclass(frozen=True) +class TransportInteractionCapability: + transport: str + public_max_tier: str + identity: str + payments: str + realtime: str + notes: tuple[str, ...] + + CONFIG_FIELDS = ( ConfigField("site", "id", "string", True, "", "Stable site identifier used in output paths."), ConfigField("site", "domain", "string", True, "", "Canonical public domain for the site."), @@ -57,6 +91,38 @@ CONFIG_FIELDS = ( "[]", "Route patterns that must not be published.", ), + ConfigField( + "site.interactions.route", + "match", + "glob", + False, + "", + "Route pattern for an explicit interaction policy.", + ), + ConfigField( + "site.interactions.route", + "tier", + "enum", + False, + "site.interactions.default_tier", + "Interaction tier for a route group.", + ), + ConfigField( + "site.interactions.route", + "identity", + "enum", + False, + "none", + "Identity adapter required by a route group.", + ), + ConfigField( + "site.interactions.route", + "payments", + "enum", + False, + "none", + "Payment adapter required by a route group.", + ), ConfigField( "site.protocols.", "enabled", @@ -108,6 +174,175 @@ CONFIG_FIELDS = ( ) +INTERACTION_TIERS = ( + InteractionTier( + name="static", + summary="Rendered files only.", + examples=("documentation", "landing pages", "catalog pages"), + notes=("Works across every currently modeled transport.",), + ), + InteractionTier( + name="interactive-lite", + summary="Client-visible state with deterministic or server-rendered updates.", + examples=("deterministic games", "leaderboards", "quote forms", "status lookups"), + notes=( + "Does not require accounts or payment confirmation.", + "Constrained transports may receive static snapshots or form-style actions.", + ), + ), + InteractionTier( + name="identity", + summary="Authenticated sessions or signed wallet identity.", + examples=("account pages", "wallet sign-in", "claim pages"), + notes=( + "Requires explicit identity adapter selection.", + "HTTP transports can use strict cookies; wallet sign-in uses signed challenges.", + ), + ), + InteractionTier( + name="transactional", + summary="Server-confirmed orders, wagers, deposits, invoices, or payment intents.", + examples=("basic stores", "paid downloads", "game entry fees", "donations"), + notes=( + "Requires explicit payment adapter selection.", + "Callbacks, webhook receivers, and ledger internals remain internal.", + ), + ), + InteractionTier( + name="realtime", + summary="Live multiplayer, dashboards, websocket streams, or fast state sync.", + examples=("multiplayer games", "live markets", "operator consoles"), + notes=( + "HTTP transports only in the current model.", + "Usually clearnet or private Tor/I2P until transport-specific realtime support exists.", + ), + ), + InteractionTier( + name="internal", + summary="Private/admin/worker surfaces that must not be published automatically.", + examples=("admin panels", "webhooks", "worker APIs", "health endpoints"), + notes=("Always deny by default on public outputs.",), + ), +) + + +IDENTITY_ADAPTERS = ( + IdentityAdapter( + name="none", + status="ready", + transports=("clearnet", "tor", "i2p", "gemini", "ipfs", "reticulum"), + notes=("No account or signed identity required.",), + ), + IdentityAdapter( + name="http-session", + status="planned", + transports=("clearnet", "tor", "i2p"), + notes=( + "Use HTTP-only cookies and strict transport-specific session scope.", + "Profiles must not be shared across transports.", + ), + ), + IdentityAdapter( + name="siwe", + status="planned", + transports=("clearnet", "tor", "i2p"), + notes=( + "Sign-In with Ethereum style challenge/response.", + "Wallet transport availability is a browser-shell concern.", + ), + ), + IdentityAdapter( + name="signed-link", + status="research", + transports=("gemini", "reticulum"), + notes=("Use only for narrow flows with short-lived, scoped capabilities.",), + ), +) + + +PAYMENT_ADAPTERS = ( + PaymentAdapter( + name="none", + status="ready", + transports=("clearnet", "tor", "i2p", "gemini", "ipfs", "reticulum"), + notes=("No payment required.",), + ), + PaymentAdapter( + name="server-invoice", + status="planned", + transports=("clearnet", "tor", "i2p"), + notes=( + "Server creates invoice or payment intent and confirms settlement.", + "Callback/webhook routes stay internal.", + ), + ), + PaymentAdapter( + name="wallet-signature", + status="planned", + transports=("clearnet", "tor", "i2p"), + notes=("Browser wallet signs a request; server verifies before fulfilling.",), + ), + PaymentAdapter( + name="static-instructions", + status="planned", + transports=("gemini", "ipfs", "reticulum"), + notes=("Render payment instructions or donation addresses without automatic fulfillment.",), + ), +) + + +TRANSPORT_INTERACTION_CAPABILITIES = ( + TransportInteractionCapability( + transport="clearnet", + public_max_tier="realtime", + identity="http-session, siwe", + payments="server-invoice, wallet-signature", + realtime="yes", + notes=("Highest-fidelity browser surface.",), + ), + TransportInteractionCapability( + transport="tor", + public_max_tier="transactional", + identity="http-session, siwe", + payments="server-invoice, wallet-signature", + realtime="private-only", + notes=("Prefer server-rendered flows and reduced JavaScript by default.",), + ), + TransportInteractionCapability( + transport="i2p", + public_max_tier="transactional", + identity="http-session, siwe", + payments="server-invoice, wallet-signature", + realtime="private-only", + notes=("Prefer server-rendered flows and reduced JavaScript by default.",), + ), + TransportInteractionCapability( + transport="gemini", + public_max_tier="interactive-lite", + identity="signed-link research", + payments="static-instructions", + realtime="no", + notes=("Use curated prompts or polling-friendly status pages.",), + ), + TransportInteractionCapability( + transport="ipfs", + public_max_tier="static", + identity="none", + payments="static-instructions", + realtime="no", + notes=("Content-addressed snapshots; no server-confirmed state by default.",), + ), + TransportInteractionCapability( + transport="reticulum", + public_max_tier="interactive-lite", + identity="signed-link research", + payments="static-instructions", + realtime="research", + notes=("Resilient/private routing; not an anonymity layer.",), + ), +) + + RENDER_PROFILES = ( RenderProfile( name="clearnet", diff --git a/docs/configuration.md b/docs/configuration.md index 4a37d24..fab7903 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -31,6 +31,12 @@ plan_root = "/var/lib/ampg/plans" default_tier = "static" deny_routes = ["/admin/*", "/internal/*", "/webhooks/*"] +[[site.interactions.route]] +match = "/checkout/*" +tier = "transactional" +identity = "http-session" +payments = "server-invoice" + [site.protocols.clearnet] enabled = true renderer = "clearnet" @@ -151,7 +157,7 @@ upstream = "http://127.0.0.1:8000" openapi = "http://127.0.0.1:9000/openapi.json" [site.interactions] -default_tier = "forms" +default_tier = "interactive-lite" session_policy = "http-only-cookie" deny_routes = ["/admin/*", "/api/internal/*", "/webhooks/*"] @@ -161,11 +167,13 @@ tier = "internal" [[site.interactions.route]] match = "/checkout/*" -tier = "sessions" +tier = "transactional" +identity = "http-session" +payments = "server-invoice" [[site.interactions.route]] match = "/catalog/*" -tier = "forms" +tier = "interactive-lite" [site.protocols.tor] enabled = true @@ -177,7 +185,7 @@ allow_javascript = false enabled = true renderer = "gemtext" daemon_policy = "auto" -max_tier = "forms" +max_tier = "interactive-lite" ``` `max_tier` is a hard cap. If a route needs a higher tier than the protocol allows, AMPG diff --git a/docs/generated/config-schema.md b/docs/generated/config-schema.md index 5087047..9863fbf 100644 --- a/docs/generated/config-schema.md +++ b/docs/generated/config-schema.md @@ -15,6 +15,10 @@ This file is generated from code. Do not edit it by hand. | site.outputs | `plan_root` | path | no | ../dist/ampg-plan | Generated plan artifact root. | | site.interactions | `default_tier` | enum | no | static | Default interaction tier for routes. | | site.interactions | `deny_routes` | array | no | [] | Route patterns that must not be published. | +| site.interactions.route | `match` | glob | no | - | Route pattern for an explicit interaction policy. | +| site.interactions.route | `tier` | enum | no | site.interactions.default_tier | Interaction tier for a route group. | +| site.interactions.route | `identity` | enum | no | none | Identity adapter required by a route group. | +| site.interactions.route | `payments` | enum | no | none | Payment adapter required by a route group. | | site.protocols. | `enabled` | boolean | no | false | Whether this protocol target is built and planned. | | site.protocols. | `renderer` | enum | no | | Renderer profile to use for this protocol. | | site.protocols. | `daemon` | enum | no | protocol default | Ingress daemon adapter. | diff --git a/docs/generated/interaction-capabilities.md b/docs/generated/interaction-capabilities.md new file mode 100644 index 0000000..99d376e --- /dev/null +++ b/docs/generated/interaction-capabilities.md @@ -0,0 +1,46 @@ +# Generated Interaction Capabilities + +> Status: generated | Updated by `python3 -m ampg docs generate` | Applies to: AMPG + +This file is generated from code. Do not edit it by hand. + + +## Tiers + +| Tier | Summary | Examples | Notes | +| --- | --- | --- | --- | +| `static` | Rendered files only. | documentation, landing pages, catalog pages | Works across every currently modeled transport. | +| `interactive-lite` | Client-visible state with deterministic or server-rendered updates. | deterministic games, leaderboards, quote forms, status lookups | Does not require accounts or payment confirmation. Constrained transports may receive static snapshots or form-style actions. | +| `identity` | Authenticated sessions or signed wallet identity. | account pages, wallet sign-in, claim pages | Requires explicit identity adapter selection. HTTP transports can use strict cookies; wallet sign-in uses signed challenges. | +| `transactional` | Server-confirmed orders, wagers, deposits, invoices, or payment intents. | basic stores, paid downloads, game entry fees, donations | Requires explicit payment adapter selection. Callbacks, webhook receivers, and ledger internals remain internal. | +| `realtime` | Live multiplayer, dashboards, websocket streams, or fast state sync. | multiplayer games, live markets, operator consoles | HTTP transports only in the current model. Usually clearnet or private Tor/I2P until transport-specific realtime support exists. | +| `internal` | Private/admin/worker surfaces that must not be published automatically. | admin panels, webhooks, worker APIs, health endpoints | Always deny by default on public outputs. | + +## Identity Adapters + +| Adapter | Status | Transports | Notes | +| --- | --- | --- | --- | +| `none` | `ready` | clearnet, tor, i2p, gemini, ipfs, reticulum | No account or signed identity required. | +| `http-session` | `planned` | clearnet, tor, i2p | Use HTTP-only cookies and strict transport-specific session scope. Profiles must not be shared across transports. | +| `siwe` | `planned` | clearnet, tor, i2p | Sign-In with Ethereum style challenge/response. Wallet transport availability is a browser-shell concern. | +| `signed-link` | `research` | gemini, reticulum | Use only for narrow flows with short-lived, scoped capabilities. | + +## Payment Adapters + +| Adapter | Status | Transports | Notes | +| --- | --- | --- | --- | +| `none` | `ready` | clearnet, tor, i2p, gemini, ipfs, reticulum | No payment required. | +| `server-invoice` | `planned` | clearnet, tor, i2p | Server creates invoice or payment intent and confirms settlement. Callback/webhook routes stay internal. | +| `wallet-signature` | `planned` | clearnet, tor, i2p | Browser wallet signs a request; server verifies before fulfilling. | +| `static-instructions` | `planned` | gemini, ipfs, reticulum | Render payment instructions or donation addresses without automatic fulfillment. | + +## Transport Limits + +| Transport | Public Max Tier | Identity | Payments | Realtime | Notes | +| --- | --- | --- | --- | --- | --- | +| `clearnet` | `realtime` | http-session, siwe | server-invoice, wallet-signature | yes | Highest-fidelity browser surface. | +| `tor` | `transactional` | http-session, siwe | server-invoice, wallet-signature | private-only | Prefer server-rendered flows and reduced JavaScript by default. | +| `i2p` | `transactional` | http-session, siwe | server-invoice, wallet-signature | private-only | Prefer server-rendered flows and reduced JavaScript by default. | +| `gemini` | `interactive-lite` | signed-link research | static-instructions | no | Use curated prompts or polling-friendly status pages. | +| `ipfs` | `static` | none | static-instructions | no | Content-addressed snapshots; no server-confirmed state by default. | +| `reticulum` | `interactive-lite` | signed-link research | static-instructions | research | Resilient/private routing; not an anonymity layer. | diff --git a/docs/interaction-capabilities.md b/docs/interaction-capabilities.md new file mode 100644 index 0000000..97a1f56 --- /dev/null +++ b/docs/interaction-capabilities.md @@ -0,0 +1,24 @@ +# Interaction Capabilities + +> Status: draft | Updated 2026-07-06 | Applies to: AMPG route exposure policy + +Interaction capabilities define what AMPG may expose for dynamic applications. They are +public, generic rules. Project strategy, target names, and implementation notes belong in +`docs/private/`. + +## Generated Policy + +The code-owned policy tables live in +[generated interaction capabilities](generated/interaction-capabilities.md). + +They cover: + +- interaction tiers from `static` through `internal`. +- identity adapters such as HTTP sessions and wallet challenge/response. +- payment adapters such as server invoices and wallet-signed requests. +- per-transport limits for clearnet, Tor, I2P, Gemini, IPFS, and Reticulum. + +## Rule + +AMPG only exposes a route when the route tier, identity adapter, payment adapter, and +transport capability all agree. When in doubt, the route stays private. diff --git a/docs/interactive-apps.md b/docs/interactive-apps.md index 914ec62..cffd172 100644 --- a/docs/interactive-apps.md +++ b/docs/interactive-apps.md @@ -1,39 +1,43 @@ # Interactive Applications -> Status: draft | Updated 2026-07-05 | Applies to: dynamic app support +> Status: draft | Updated 2026-07-06 | Applies to: dynamic app support AMPG must support real applications without pretending every transport can offer the same -experience. The design goal is graceful capability mapping: expose the safest useful version -of each surface on each protocol, and keep private surfaces private. +experience. The design goal is graceful capability mapping: expose the safest useful +version of each public surface on each protocol, and keep private surfaces private. -## Interaction tiers +## Interaction Tiers `static` : Rendered files only. Works everywhere. -`forms` -: Server-rendered GET/POST actions with full-page responses. Works on clearnet, Tor, I2P, - and can be mapped to Gemini input prompts or Reticulum page scripts for simple actions. +`interactive-lite` +: Deterministic games, status lookups, quote forms, and other flows that do not require + account state or payment confirmation. -`sessions` -: Authenticated account flows. Supported on HTTP transports with strict cookies. Gemini can - use client certificates or signed links later, but this is not a v1 requirement. +`identity` +: Authenticated sessions, wallet sign-in, or signed capability links. + +`transactional` +: Server-confirmed orders, wagers, deposits, invoices, donations, or payment intents. `realtime` -: Rich browser UI, dashboards, live charts, websockets, or client-side state. HTTP transports - only, and usually clearnet or private Tor/I2P. +: Multiplayer, dashboards, websockets, streaming updates, or fast shared state. `internal` -: Worker APIs, webhooks, private admin APIs, payment callbacks, chain watchers, and service - health endpoints. AMPG must not publish these automatically. +: Admin, worker APIs, webhooks, callback endpoints, chain watchers, health checks, and + private operator surfaces. AMPG must not publish these automatically. -## Application adapter contract +The code-owned capability matrix lives in +[generated interaction capabilities](generated/interaction-capabilities.md). + +## Application Adapter Contract An application adapter describes what AMPG may expose: - upstream service and health check. - OpenAPI spec or route manifest when available. -- route groups with interaction tier, auth requirement, and idempotency behavior. +- route groups with interaction tier, auth requirement, payment requirement, and idempotency behavior. - form/action schemas for protocol translation. - public asset and cache policy. - private route denylist. @@ -41,83 +45,45 @@ An application adapter describes what AMPG may expose: Adapters do not bypass the application. They translate constrained protocol input into the application's public action surface, then render safe responses. -## Protocol mapping +## Protocol Mapping -| Tier | Clearnet | Tor/I2P | Gemini | Reticulum | -| --- | --- | --- | --- | --- | -| `static` | yes | yes | yes | yes | -| `forms` | yes | yes, no JS by default | simple single-field prompts | simple scripts/actions | -| `sessions` | yes | yes, strict cookies | later | later | -| `realtime` | yes | optional private surface | no | no | -| `internal` | no public publish | no public publish | no | no | +| Tier | Clearnet | Tor/I2P | Gemini | IPFS | Reticulum | +| --- | --- | --- | --- | --- | --- | +| `static` | yes | yes | yes | yes | yes | +| `interactive-lite` | yes | yes | curated prompts/status pages | static snapshots only | curated actions | +| `identity` | yes | yes, strict sessions | research | no | research | +| `transactional` | yes | yes, explicit adapter | instructions/status only | instructions only | instructions/status only | +| `realtime` | yes | private/opt-in only | no | no | research | +| `internal` | no public publish | no public publish | no | no | no | -## Challenge profile: Medusa multi-tenant platform +## Identity And Payments -Source: `../medusa-multi-tenant-platform`. +Identity and payments are optional adapters, not default site behavior. A route group must +explicitly request them before AMPG exposes account or payment flows on any transport. -Why it matters: +Supported design tracks: -- multi-tenant domain routing. -- storefront, tenant admin, backend, worker, database, Redis, S3/MinIO. -- cart, checkout, orders, bookings, blogs, email, payment providers, and webhooks. -- tenant-specific themes and custom domains. +- strict HTTP sessions for clearnet, Tor, and I2P. +- Sign-In with Ethereum style challenge/response for HTTP transports. +- server-created invoices or payment intents with internal callbacks. +- wallet-signed requests verified by the upstream app. +- static payment instructions for read-only or constrained outputs. -AMPG support target: +Payment callbacks, webhooks, ledger internals, worker routes, and private reconciliation +jobs remain `internal` even when public checkout or donation pages are exposed. -- Clearnet: adopt existing Caddy/reverse-proxy deployment. -- Tor/I2P: expose storefront routes with `forms` or `sessions` tier, no JavaScript by default. -- Gemini/Reticulum: expose catalog, blog, policy, product detail, and inquiry/request flows. -- Admin: `internal` unless explicitly bound to a private operator transport. -- Webhooks, payment callbacks, worker queues, health internals: always `internal`. - -Required adapter capabilities: - -- read tenant/domain registry or accept an exported tenant manifest. -- generate per-tenant protocol targets and daemon plans. -- cap checkout/payment routes by protocol. -- use OpenAPI route metadata where available. -- never expose tenant admin or backend admin by wildcard. - -## Challenge profile: evaluetron / ai-gen-bot - -Source: `../ai-gen-bot`. - -Why it matters: - -- OpenAPI-first core. -- customer storefront plus operator console. -- auth, payments, credit ledger, job queue, workers, moderation, and generated artifacts. -- public web channel, Telegram channel, internal worker APIs, and admin APIs. - -AMPG support target: - -- Clearnet: public storefront and docs. -- Tor/I2P: public storefront with server-rendered account/job flows when enabled. -- Gemini/Reticulum: catalog browsing, deposit instructions, simple generation request, and job - status lookup only after explicit adapter support. -- Operator console: private clearnet, LAN, mesh, or private onion only. -- Worker/internal routes: never published. - -Required adapter capabilities: - -- consume `api/openapi.yaml` as the action contract. -- map job creation to idempotent form submissions. -- render job status as polling-friendly pages. -- hide unavailable generators when no worker advertises the required capability. -- preserve the ledger and payment invariants of the upstream app. - -## Design constraints learned from the challenge apps +## Design Constraints - Public route discovery must start deny-first. - OpenAPI is useful, but not sufficient; route tiers and privacy intent must be explicit. - Admin and worker surfaces are separate sites or route groups, not accidental subpaths. -- Gemini and Reticulum need curated actions, not automatic exposure of every POST route. +- Gemini, IPFS, and Reticulum need curated outputs, not automatic exposure of every POST route. - Multi-tenant apps need generated protocol targets per tenant, with shared daemon adapters. -## Reusable checklist +## Reusable Checklist - [ ] Classify every route group by interaction tier. - [ ] Deny private/admin/internal routes before enabling protocols. - [ ] Prefer OpenAPI or route manifests over HTML scraping for actions. - [ ] Provide safe alternates for constrained transports. -- [ ] Verify no worker, webhook, payment callback, or admin route is publicly published. +- [ ] Verify no worker, webhook, payment callback, ledger internal, or admin route is publicly published. diff --git a/tests/test_public_private_split.py b/tests/test_public_private_split.py new file mode 100644 index 0000000..b6d2118 --- /dev/null +++ b/tests/test_public_private_split.py @@ -0,0 +1,30 @@ +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +PRIVATE_TARGET_TERMS = ( + "wowrace", + "wowngeon", + "smirk-monorepo", + "medusa-multi-tenant-platform", + "ai-gen-bot", +) + + +class PublicPrivateSplitTest(unittest.TestCase): + def test_private_target_names_do_not_appear_in_public_docs(self): + failures = [] + for path in sorted((ROOT / "docs").rglob("*.md")): + rel = path.relative_to(ROOT) + if "private" in rel.parts: + continue + text = path.read_text(encoding="utf-8").lower() + for term in PRIVATE_TARGET_TERMS: + if term in text: + failures.append(f"{rel}: contains private target term {term!r}") + self.assertEqual([], failures) + + +if __name__ == "__main__": + unittest.main()