Merge FASE 4: diagrams folder with 8 Mermaid + SVGs

Resolves two conflicts:

- docs/diagrams/README.md: FASE 3 created a placeholder, FASE 4 created the
  canonical content. Adopts FASE 4 content and updates the doc paths to the
  FASE 3 subfolder layout (architecture/, frameworks/, routing/, guides/).
- package.json: combined FASE 1's new scripts/build/ and scripts/check/ paths
  with FASE 4's new docs:render-diagrams script.

Post-merge fixes:
- Rewrites diagram link paths in the 7 subfolder docs from ./diagrams/X to
  ../diagrams/X (FASE 4 added flat-layout links before FASE 3's subfolder move).
- Adds the i18n-flow diagram link to docs/guides/I18N.md (auto-merge missed it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
diegosouzapw
2026-05-13 16:24:45 -03:00
29 changed files with 373 additions and 6 deletions

View File

@@ -79,7 +79,10 @@ API routes follow a consistent pattern: `Route → CORS preflight → Zod body v
## Resilience Runtime State
OmniRoute has three related but distinct temporary-failure mechanisms. Keep their
scope separate when debugging routing behavior.
scope separate when debugging routing behavior. See the
[3-layer resilience diagram](./docs/diagrams/exported/resilience-3layers.svg)
(source: [docs/diagrams/resilience-3layers.mmd](./docs/diagrams/resilience-3layers.mmd))
for an at-a-glance map.
### Provider Circuit Breaker

View File

@@ -78,6 +78,21 @@ Primary runtime model:
- Next.js app routes under `src/app/api/*` implement both dashboard APIs and compatibility APIs
- A shared SSE/routing core in `src/sse/*` + `open-sse/*` handles provider execution, translation, streaming, fallback, and usage
## Reference Diagrams
Canonical, version-controlled Mermaid sources for the v3.8.0 platform live in
[`docs/diagrams/`](../diagrams/README.md). Two are reproduced below for orientation;
the rest are linked from their domain-specific guides.
![Request pipeline (/v1/chat/completions)](../diagrams/exported/request-pipeline.svg)
> Source: [diagrams/request-pipeline.mmd](../diagrams/request-pipeline.mmd)
![3-layer resilience model](../diagrams/exported/resilience-3layers.svg)
> Source: [diagrams/resilience-3layers.mmd](../diagrams/resilience-3layers.mmd) — also linked from
> [RESILIENCE_GUIDE.md](./RESILIENCE_GUIDE.md) and the `CLAUDE.md` resilience reference.
## Scope and Boundaries
### In Scope

View File

@@ -5,6 +5,10 @@
OmniRoute has a route-aware authorization pipeline that gates every API request. Classification is **deterministic** and **fail-closed** — anything that cannot be classified ends up as `MANAGEMENT` and demands a session or management-grade token. This page explains the model for engineers maintaining routes or designing new endpoints.
![AuthZ pipeline (3 route classes + policy evaluation)](../diagrams/exported/authz-pipeline.svg)
> Source: [diagrams/authz-pipeline.mmd](../diagrams/authz-pipeline.mmd)
## Two Auth Modes
### 1. API Key (Bearer)

View File

@@ -286,6 +286,10 @@ Top-level files in `src/lib/`:
Singleton SQLite database (`getDbInstance()` in `core.ts`, WAL journaling).
**Never write raw SQL in routes or handlers** — go through these modules.
![Database schema overview (selected core tables)](../diagrams/exported/db-schema-overview.svg)
> Source: [diagrams/db-schema-overview.mmd](../diagrams/db-schema-overview.mmd)
Domain modules (each owns one or more tables): `apiKeys.ts`, `backup.ts`,
`batches.ts`, `cleanup.ts`, `cliToolState.ts`, `combos.ts`,
`commandCodeAuth.ts`, `compression.ts`, `compressionAnalytics.ts`,
@@ -643,6 +647,10 @@ Organized into 6 subfolders by purpose.
## 9. Request Pipeline (Summary)
![Request pipeline (/v1/chat/completions)](../diagrams/exported/request-pipeline.svg)
> Source: [diagrams/request-pipeline.mmd](../diagrams/request-pipeline.mmd)
```
Client request
→ /v1/chat/completions (route.ts)

View File

@@ -2,6 +2,10 @@
OmniRoute has three distinct but related resilience mechanisms. Each has a different scope and purpose. Keep them separate when debugging routing behavior.
![3-layer resilience model](../diagrams/exported/resilience-3layers.svg)
> Source: [diagrams/resilience-3layers.mmd](../diagrams/resilience-3layers.mmd)
## 1. Provider Circuit Breaker
**Scope:** entire provider (e.g., `glm`, `openai`, `anthropic`).

View File

@@ -1,13 +1,56 @@
# Diagrams
Mermaid sources (`.mmd`) and exported SVGs/PNGs for architecture flows.
Mermaid sources (`.mmd`) and exported SVGs for OmniRoute v3.8.0 architecture flows.
Populated incrementally — FASE 4 of the platform overhaul creates 8 canonical diagrams.
## Canonical diagrams
Source diagrams: link from architecture docs using:
| Source | Exported | Used in |
| -------------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------ |
| [request-pipeline.mmd](./request-pipeline.mmd) | [SVG](./exported/request-pipeline.svg) | docs/architecture/ARCHITECTURE.md, docs/architecture/CODEBASE_DOCUMENTATION.md |
| [auto-combo-9factor.mmd](./auto-combo-9factor.mmd) | [SVG](./exported/auto-combo-9factor.svg) | docs/routing/AUTO-COMBO.md |
| [resilience-3layers.mmd](./resilience-3layers.mmd) | [SVG](./exported/resilience-3layers.svg) | docs/architecture/RESILIENCE_GUIDE.md, CLAUDE.md |
| [i18n-flow.mmd](./i18n-flow.mmd) | [SVG](./exported/i18n-flow.svg) | docs/guides/I18N.md |
| [mcp-tools-37.mmd](./mcp-tools-37.mmd) | [SVG](./exported/mcp-tools-37.svg) | docs/frameworks/MCP-SERVER.md |
| [cloud-agent-flow.mmd](./cloud-agent-flow.mmd) | [SVG](./exported/cloud-agent-flow.svg) | docs/frameworks/CLOUD_AGENT.md |
| [authz-pipeline.mmd](./authz-pipeline.mmd) | [SVG](./exported/authz-pipeline.svg) | docs/architecture/AUTHZ_GUIDE.md |
| [db-schema-overview.mmd](./db-schema-overview.mmd) | [SVG](./exported/db-schema-overview.svg) | docs/architecture/CODEBASE_DOCUMENTATION.md |
## How to update
1. Edit `*.mmd`.
2. Re-render: `npm run docs:render-diagrams` (uses `@mermaid-js/mermaid-cli`).
3. Commit both `.mmd` and `.svg`.
If `@mermaid-js/mermaid-cli` is not available locally, install it once:
```bash
npm install -g @mermaid-js/mermaid-cli
```
The script renders every `.mmd` in `docs/diagrams/` into `docs/diagrams/exported/*.svg`
with a white background, suitable for both dark and light themes.
## Linking from a doc
From a doc in `docs/<subfolder>/`, the relative path becomes `../diagrams/...`:
```markdown
![Diagram name](./exported/diagram-name.svg)
![Request pipeline](../diagrams/exported/request-pipeline.svg)
> [Mermaid source](./diagram-name.mmd)
> Source: [../diagrams/request-pipeline.mmd](../diagrams/request-pipeline.mmd)
```
From the repo root (e.g. `CLAUDE.md`):
```markdown
![Resilience layers](./docs/diagrams/exported/resilience-3layers.svg)
```
## Conventions
- One concept per diagram. Don't try to fit the whole platform in one chart.
- Keep node labels short (3-6 words). Use `<br/>` for line breaks inside nodes.
- Prefer `flowchart LR` for pipelines and `flowchart TB` for layered models.
- Use `sequenceDiagram` for interactive (request/response) flows.
- Use `erDiagram` for database schema overviews.
- Update both `.mmd` and `.svg` in the same commit. Keep them in lock-step.

View File

@@ -0,0 +1,23 @@
%% AuthZ pipeline (3 route classes + policy evaluation)
%% Reflects: src/middleware.ts, src/server/authz/pipeline.ts,
%% src/server/authz/policies/{public,clientApi,management}.ts
%% v3.8.0
flowchart LR
Req["Incoming request"] --> Strip["Strip trusted internal headers<br/>(x-omniroute-auth-*)"]
Strip --> Classify["classifyRoute()"]
Classify -->|PUBLIC| PubP["publicPolicy<br/>(login, health, status)"]
Classify -->|CLIENT_API| CliP["clientApiPolicy<br/>(/api/v1/*, /v1/*)"]
Classify -->|MANAGEMENT| MgmtP["managementPolicy<br/>(dashboard, settings, admin)"]
PubP -->|allow| Stamp["Stamp x-omniroute-auth-*<br/>(kind / id / label / scopes)"]
CliP -->|extract Bearer| Validate["validateApiKey()"]
Validate -->|valid| Stamp
Validate -->|"invalid (REQUIRE_API_KEY=true)"| Reject401["401 AUTH_002"]
Validate -->|"absent (REQUIRE_API_KEY=false)"| Anon["anonymous fallthrough"] --> Stamp
MgmtP -->|session ok| Stamp
MgmtP -->|Bearer w/ manage scope| Stamp
MgmtP -->|"Bearer invalid"| Reject403["403 AUTH_001"]
MgmtP -->|no auth| Reject401Mgmt["401 / 302 /login"]
Stamp --> Handler["Handler<br/>(uses assertAuth)"]

View File

@@ -0,0 +1,23 @@
%% Auto-Combo 9-factor scoring
%% Reflects: open-sse/services/autoCombo/scoring.ts (DEFAULT_WEIGHTS, sum = 1.0)
%% v3.8.0
flowchart TB
Request["Incoming request"] --> Candidates["Eligible candidates<br/>(provider × model × account)"]
Candidates --> Score["Compute composite score<br/>per candidate"]
subgraph Factors["9-factor scoring weights (sum = 1.0)"]
f1["health (0.22)"]
f2["quota (0.17)"]
f3["costInv (0.17)"]
f4["latencyInv (0.13)"]
f5["taskFit (0.08)"]
f6["specificityMatch (0.08)"]
f7["stability (0.05)"]
f8["tierPriority (0.05)"]
f9["tierAffinity (0.05)"]
end
Score --> Factors
Factors --> Final["Sort by score<br/>(desc)"]
Final --> Top["Pick top-N targets"]
Top --> Dispatch["Dispatch sequentially<br/>(short-circuit on success)"]

View File

@@ -0,0 +1,32 @@
%% Cloud Agent task lifecycle
%% Reflects: src/lib/cloudAgent/* and src/app/api/v1/agents/tasks/*
%% Supported agents: codex-cloud, devin, jules
%% v3.8.0
sequenceDiagram
participant U as User
participant API as /v1/agents/tasks
participant Reg as Cloud Agent Registry
participant Ag as Cloud Agent<br/>(codex-cloud / devin / jules)
participant DB as SQLite domain DB
U->>API: POST createTask (description, sources)
API->>API: Zod validation + AuthZ (management)
API->>Reg: getAgent(providerId)
Reg->>Ag: instantiate with credentials
Ag->>Ag: createTask() returns planning
Ag-->>API: taskId + status: planning
API->>DB: insertCloudAgentTask
API-->>U: taskId
U->>API: GET /tasks/:id (poll)
API->>Ag: getStatus(externalId)
Ag-->>API: status, plan, messages
API->>DB: updateCloudAgentTask
API-->>U: snapshot
U->>API: POST approvePlan
API->>Ag: approvePlan(externalId)
Ag-->>API: status: running
Note over Ag: Async execution upstream
Ag-->>DB: stream events / final result

View File

@@ -0,0 +1,16 @@
%% Database schema overview (selected core tables)
%% Reflects: src/lib/db/* (45+ modules, 55 migrations)
%% v3.8.0
erDiagram
api_keys ||--o{ api_key_usage : tracks
api_keys ||--o{ rate_limits : enforces
providers ||--o{ provider_connections : holds
provider_connections ||--o{ domain_circuit_breakers : protected_by
combos ||--o{ combo_targets : contains
combos ||--o{ combo_executions : logs
spend_buffer ||--o{ usage_aggregations : flushed_to
memory_documents ||--o{ memory_chunks : split_into
skills_runs ||--o{ skills_logs : produced
agent_tasks ||--o{ agent_task_events : streams
audit_log }|--|| api_keys : by
mcp_audit }|--|| api_keys : by

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 40 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -0,0 +1,14 @@
%% Incremental hash-based i18n pipeline
%% Reflects: scripts/i18n/* and docs/i18n/<locale>/*.md mirror layout
%% v3.8.0
flowchart LR
Src["Source MDs<br/>(CLAUDE.md, docs/**/*.md)"] --> Hash["sha256 hash"]
Hash --> State[".i18n-state.json"]
State -->|diff| Dirty["Mark source dirty"]
Dirty --> Loop{"For each locale<br/>(39 langs)"}
Loop --> LLM["OmniRoute<br/>/chat/completions<br/>(cx/gpt-5.4-mini)"]
LLM --> Target["Write<br/>docs/i18n/&lt;locale&gt;/&lt;rel-path&gt;.md"]
Target --> State
Target --> CI{"CI drift check<br/>(npm run i18n:check)"}
CI -->|in sync| Pass["pass"]
CI -->|drift| Fail["fail<br/>(exit 1)"]

View File

@@ -0,0 +1,23 @@
%% MCP Server tool inventory by category
%% Reflects: open-sse/mcp-server/tools/* and docs/MCP-SERVER.md
%% v3.8.0
flowchart LR
MCP["MCP Server<br/>37 tools total"] --> Core["Core (30)"]
MCP --> Mem["Memory (3)"]
MCP --> Skl["Skills (4)"]
Core --> Essential["Essential (8)<br/>get_health, list_combos,<br/>switch_combo, check_quota,<br/>route_request, cost_report,<br/>list_models_catalog,<br/>get_combo_metrics"]
Core --> Search["Search (1)<br/>web_search"]
Core --> Advanced["Advanced (11)<br/>simulate_route, set_budget_guard,<br/>set_routing_strategy,<br/>set_resilience_profile,<br/>test_combo, get_provider_metrics,<br/>best_combo_for_task, explain_route,<br/>get_session_snapshot,<br/>db_health_check, sync_pricing"]
Core --> Cache["Cache (2)"]
Core --> Compression["Compression (5)"]
Core --> Tunnels["1Proxy / Tunnels (3)"]
Mem --> M1["memory_search"]
Mem --> M2["memory_save"]
Mem --> M3["memory_delete"]
Skl --> S1["skill_invoke"]
Skl --> S2["skill_list"]
Skl --> S3["skill_diagnose"]
Skl --> S4["skill_uninstall"]

View File

@@ -0,0 +1,24 @@
%% Request pipeline for /v1/chat/completions
%% Reflects: src/app/api/v1/chat/completions/route.ts → open-sse/handlers/chatCore.ts
%% v3.8.0
flowchart LR
Client["Client<br/>(IDE/CLI/SDK)"] --> Route["Next.js Route<br/>/v1/chat/completions"]
Route --> CORS["CORS preflight"]
CORS --> Validate["Zod validation<br/>(request body)"]
Validate --> Authz["AuthZ pipeline<br/>(extractApiKey +<br/>isValidApiKey)"]
Authz --> Policy["API key policy<br/>(allowlist + scopes)"]
Policy --> Guard["Prompt-injection<br/>guardrail"]
Guard --> ChatCore["handleChatCore"]
ChatCore --> Cache{"Cache hit?"}
Cache -->|yes| Return["Return cached"]
Cache -->|no| Rate["Rate limit<br/>(per-key, per-IP)"]
Rate --> Combo{"Combo<br/>target?"}
Combo -->|combo| Resolve["resolveComboTargets<br/>(14 strategies)"]
Resolve --> Single["handleSingleModel<br/>(per target)"]
Combo -->|single| Single
Single --> Translate["translateRequest<br/>(OpenAI↔Claude↔Gemini)"]
Translate --> Exec["getExecutor<br/>(31 executors)"]
Exec --> Upstream["Upstream Provider<br/>(177 providers)"]
Upstream --> Stream["SSE / JSON"]
Stream --> Transformer["responsesTransformer<br/>(Responses↔Chat)"]
Transformer --> Client

View File

@@ -0,0 +1,21 @@
%% 3-layer resilience model
%% Reflects: src/shared/utils/circuitBreaker.ts, src/sse/services/auth.ts,
%% open-sse/services/accountFallback.ts
%% v3.8.0
flowchart TB
Req["Request"] --> L1{"Provider Circuit Breaker<br/>(scope: whole provider)"}
L1 -->|CLOSED| L2
L1 -->|OPEN| Block1["Skip provider<br/>(or 503 retry-after)"]
L1 -->|HALF_OPEN| Probe["Allow probe"]
Probe --> L2
L2{"Connection Cooldown<br/>(scope: one account/key)"} -->|available| L3
L2 -->|cooling down| Skip2["Skip this connection<br/>fall back to next"]
L3{"Model Lockout<br/>(scope: provider × conn × model)"} -->|allowed| Exec["Execute upstream"]
L3 -->|locked| Skip3["Fall back to next model"]
Exec -->|success| Clear["Clear cooldowns/lockouts"]
Exec -->|"408 / 500 / 502 / 503 / 504"| TripL1["Trip provider breaker"]
Exec -->|"401 / 403 (account)"| TripL2["Start connection cooldown"]
Exec -->|"429 quota (model)"| TripL3["Start model lockout"]

View File

@@ -12,6 +12,10 @@ A Cloud Agent task is **not** a regular chat completion. It is a durable, multi-
unit of work that may take minutes to hours, can produce a Pull Request as its
artifact, and supports follow-up messages and (in some providers) plan approval gates.
![Cloud Agent task lifecycle](../diagrams/exported/cloud-agent-flow.svg)
> Source: [diagrams/cloud-agent-flow.mmd](../diagrams/cloud-agent-flow.mmd)
## Supported Agents
| Provider ID | Class | Source | Upstream Base URL | Plan Approval |

View File

@@ -4,6 +4,10 @@
>
> Source of truth: `open-sse/mcp-server/schemas/tools.ts` (30 tools) + `open-sse/mcp-server/tools/memoryTools.ts` (3 tools) + `open-sse/mcp-server/tools/skillTools.ts` (4 tools). Tool registration and scope wiring lives in `open-sse/mcp-server/server.ts`.
![MCP tool inventory (37 tools by category)](../diagrams/exported/mcp-tools-37.svg)
> Source: [diagrams/mcp-tools-37.mmd](../diagrams/mcp-tools-37.mmd)
## Installation
OmniRoute MCP is built-in. Start it with:

View File

@@ -17,6 +17,10 @@ OmniRoute supports **30 languages** with full dashboard UI translation, translat
## Architecture
![Incremental hash-based i18n pipeline](../diagrams/exported/i18n-flow.svg)
> Source: [diagrams/i18n-flow.mmd](../diagrams/i18n-flow.mmd)
### Source of Truth
- **UI strings**: `src/i18n/messages/en.json` (English source, ~2800 keys)

View File

@@ -19,6 +19,10 @@ OmniRoute supports **30 languages** with full dashboard UI translation, translat
## आर्किटेक्चर
![Incremental hash-based i18n pipeline](./diagrams/exported/i18n-flow.svg)
> Source: [diagrams/i18n-flow.mmd](./diagrams/i18n-flow.mmd)
### Source of Truth
- **UI strings**: `src/i18n/messages/en.json` (English source, ~2800 keys)

View File

@@ -77,6 +77,10 @@ Auto-scoring selects best provider/model per request
The Auto-Combo Engine dynamically selects the best provider/model for each request using a **9-factor scoring function** (defined in `open-sse/services/autoCombo/scoring.ts``DEFAULT_WEIGHTS`). All weights sum to **1.0**.
![Auto-Combo 9-factor scoring](../diagrams/exported/auto-combo-9factor.svg)
> Source: [diagrams/auto-combo-9factor.mmd](../diagrams/auto-combo-9factor.mmd)
| Factor | Default Weight | Description |
| :----------------- | :------------- | :---------------------------------------------------------------------- |
| `health` | 0.22 | Health score from circuit breaker (CLOSED=1.0, HALF_OPEN=0.5, OPEN=0.0) |

View File

@@ -87,6 +87,7 @@
"check:docs-counts": "node scripts/check/check-docs-counts-sync.mjs",
"check:deprecated-versions": "node scripts/check/check-deprecated-versions.mjs",
"check:docs-all": "npm run check:docs-sync && npm run check:docs-counts && npm run check:env-doc-sync && npm run check:deprecated-versions",
"docs:render-diagrams": "node scripts/docs/render-diagrams.mjs",
"check:node-runtime": "node --import tsx/esm scripts/check/check-supported-node-runtime.ts",
"check:pack-artifact": "node --import tsx/esm scripts/build/validate-pack-artifact.ts",
"audit:deps": "npm audit --audit-level=moderate && npm run audit:electron",

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env node
/**
* Render every Mermaid source in docs/diagrams/*.mmd into docs/diagrams/exported/*.svg
*
* Usage:
* npm run docs:render-diagrams
*
* Requirements:
* - @mermaid-js/mermaid-cli (`mmdc`) on PATH or installed globally.
* `npm install -g @mermaid-js/mermaid-cli` if missing.
*
* Notes:
* - Puppeteer needs `--no-sandbox` on Ubuntu 23.10+ / WSL. A temp config file
* is written automatically.
* - Each diagram is rendered with `--backgroundColor white` so the SVG works
* against both light and dark themes.
* - The script exits non-zero on first failure so CI / pre-commit hooks can
* gate on it.
*/
import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { tmpdir } from "node:os";
const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(__dirname, "..", "..");
const srcDir = resolve(repoRoot, "docs", "diagrams");
const outDir = resolve(srcDir, "exported");
if (!existsSync(srcDir)) {
console.error(`[render-diagrams] missing source dir: ${srcDir}`);
process.exit(1);
}
if (!existsSync(outDir)) {
mkdirSync(outDir, { recursive: true });
}
// Puppeteer needs --no-sandbox on many Linux distros (Ubuntu 23.10+, WSL).
const puppeteerConfigPath = join(tmpdir(), "omniroute-mmdc-puppeteer.json");
writeFileSync(
puppeteerConfigPath,
JSON.stringify({ args: ["--no-sandbox", "--disable-setuid-sandbox"] }, null, 2)
);
const sources = readdirSync(srcDir)
.filter((f) => f.endsWith(".mmd"))
.sort();
if (sources.length === 0) {
console.error(`[render-diagrams] no .mmd files in ${srcDir}`);
process.exit(1);
}
console.log(`[render-diagrams] rendering ${sources.length} diagram(s)`);
let failures = 0;
for (const src of sources) {
const input = join(srcDir, src);
const output = join(outDir, src.replace(/\.mmd$/, ".svg"));
console.log(` - ${src} -> ${output.replace(repoRoot + "/", "")}`);
const result = spawnSync(
"mmdc",
[
"-i",
input,
"-o",
output,
"--backgroundColor",
"white",
"--puppeteerConfigFile",
puppeteerConfigPath,
],
{ stdio: "inherit" }
);
if (result.status !== 0) {
console.error(` [FAIL] ${src} (exit ${result.status})`);
failures += 1;
}
}
if (failures > 0) {
console.error(`[render-diagrams] ${failures} failure(s)`);
process.exit(1);
}
console.log(`[render-diagrams] all ${sources.length} diagram(s) rendered.`);