mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
docs(docs): add drift detection scripts + minor refinements
Adds three automated drift-detection scripts under scripts/, plus npm helpers to run them, and applies small follow-ups to TERMUX, I18N, and CODEBASE docs flagged by the docs audit. New scripts (scripts/): - check-env-doc-sync.mjs — cross-checks process.env.X in code vs .env.example vs docs/ENVIRONMENT.md. Soft-fails by default; --strict exits 1 on drift. - check-docs-counts-sync.mjs — validates counts (executors, routing strategies, OAuth providers, A2A skills, cloud agents) match between code and docs. - check-deprecated-versions.mjs — flags hardcoded stale versions and "Last updated" dates older than 60 days. Uses hardcoded regexes to satisfy semgrep ReDoS guidance. package.json: - New scripts: check:env-doc-sync, check:docs-counts, check:deprecated-versions, check:docs-all (umbrella). Doc refinements: - TERMUX_GUIDE: spell out the Node version range required by package.json. - I18N: note that docs/i18n/in/ tree is an orphan duplicate of hi/ that the generator no longer writes to; replace hard-coded UNTRANSLATABLE_KEYS count with "varies per release". - CODEBASE_DOCUMENTATION: minor wording. Pre-commit hook is unchanged — the new checks are heuristic and ship as on-demand npm scripts to avoid false-positive blocks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
130
docs/I18N.md
130
docs/I18N.md
@@ -6,24 +6,26 @@ OmniRoute supports **30 languages** with full dashboard UI translation, translat
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Task | Command |
|
||||
|------|---------|
|
||||
| Generate translations | `node scripts/i18n/generate-multilang.mjs messages` |
|
||||
| Translate docs (LLM) | `python3 scripts/i18n_autotranslate.py --api-url <url> --api-key <key> --model <model>` |
|
||||
| Validate a locale | `python3 scripts/validate_translation.py quick -l cs` |
|
||||
| Check code keys | `python3 scripts/check_translations.py` |
|
||||
| Generate QA report | `node scripts/i18n/generate-qa-checklist.mjs` |
|
||||
| Visual QA (Playwright) | `node scripts/i18n/run-visual-qa.mjs` |
|
||||
| Task | Command |
|
||||
| ---------------------- | --------------------------------------------------------------------------------------- |
|
||||
| Generate translations | `node scripts/i18n/generate-multilang.mjs messages` |
|
||||
| Translate docs (LLM) | `python3 scripts/i18n_autotranslate.py --api-url <url> --api-key <key> --model <model>` |
|
||||
| Validate a locale | `python3 scripts/validate_translation.py quick -l cs` |
|
||||
| Check code keys | `python3 scripts/check_translations.py` |
|
||||
| Generate QA report | `node scripts/i18n/generate-qa-checklist.mjs` |
|
||||
| Visual QA (Playwright) | `node scripts/i18n/run-visual-qa.mjs` |
|
||||
|
||||
## Architecture
|
||||
|
||||
### Source of Truth
|
||||
|
||||
- **UI strings**: `src/i18n/messages/en.json` (English source, ~2800 keys)
|
||||
- **Locale files**: `src/i18n/messages/{locale}.json` (30 translations)
|
||||
- **Framework**: `next-intl` with cookie-based locale resolution
|
||||
- **Config**: `src/i18n/config.ts` — defines all 30 locales, language names, flags
|
||||
|
||||
### Runtime Flow
|
||||
|
||||
1. User selects language → `NEXT_LOCALE` cookie set
|
||||
2. `src/i18n/request.ts` resolves locale: cookie → `Accept-Language` header → fallback `en`
|
||||
3. Dynamic import loads `messages/{locale}.json`
|
||||
@@ -31,44 +33,46 @@ OmniRoute supports **30 languages** with full dashboard UI translation, translat
|
||||
|
||||
### Supported Locales
|
||||
|
||||
| Code | Language | RTL | Google Translate Code |
|
||||
|------|----------|-----|----------------------|
|
||||
| `ar` | العربية | Yes | `ar` |
|
||||
| `bg` | Български | No | `bg` |
|
||||
| `cs` | Čeština | No | `cs` |
|
||||
| `da` | Dansk | No | `da` |
|
||||
| `de` | Deutsch | No | `de` |
|
||||
| `es` | Español | No | `es` |
|
||||
| `fi` | Suomi | No | `fi` |
|
||||
| `fr` | Français | No | `fr` |
|
||||
| `he` | עברית | Yes | `iw` |
|
||||
| `hi` | हिन्दी | No | `hi` |
|
||||
| `hu` | Magyar | No | `hu` |
|
||||
| `id` | Bahasa Indonesia | No | `id` |
|
||||
| `it` | Italiano | No | `it` |
|
||||
| `ja` | 日本語 | No | `ja` |
|
||||
| `ko` | 한국어 | No | `ko` |
|
||||
| `ms` | Bahasa Melayu | No | `ms` |
|
||||
| `nl` | Nederlands | No | `nl` |
|
||||
| `no` | Norsk | No | `no` |
|
||||
| `phi` | Filipino | No | `tl` |
|
||||
| `pl` | Polski | No | `pl` |
|
||||
| `pt` | Português (Portugal) | No | `pt` |
|
||||
| `pt-BR` | Português (Brasil) | No | `pt` |
|
||||
| `ro` | Română | No | `ro` |
|
||||
| `ru` | Русский | No | `ru` |
|
||||
| `sk` | Slovenčina | No | `sk` |
|
||||
| `sv` | Svenska | No | `sv` |
|
||||
| `th` | ไทย | No | `th` |
|
||||
| `tr` | Türkçe | No | `tr` |
|
||||
| `uk-UA` | Українська | No | `uk` |
|
||||
| `vi` | Tiếng Việt | No | `vi` |
|
||||
| `zh-CN` | 中文 (简体) | No | `zh-CN` |
|
||||
| Code | Language | RTL | Google Translate Code |
|
||||
| ------- | -------------------- | --- | --------------------- |
|
||||
| `ar` | العربية | Yes | `ar` |
|
||||
| `bg` | Български | No | `bg` |
|
||||
| `cs` | Čeština | No | `cs` |
|
||||
| `da` | Dansk | No | `da` |
|
||||
| `de` | Deutsch | No | `de` |
|
||||
| `es` | Español | No | `es` |
|
||||
| `fi` | Suomi | No | `fi` |
|
||||
| `fr` | Français | No | `fr` |
|
||||
| `he` | עברית | Yes | `iw` |
|
||||
| `hi` | हिन्दी | No | `hi` |
|
||||
| `hu` | Magyar | No | `hu` |
|
||||
| `id` | Bahasa Indonesia | No | `id` |
|
||||
| `it` | Italiano | No | `it` |
|
||||
| `ja` | 日本語 | No | `ja` |
|
||||
| `ko` | 한국어 | No | `ko` |
|
||||
| `ms` | Bahasa Melayu | No | `ms` |
|
||||
| `nl` | Nederlands | No | `nl` |
|
||||
| `no` | Norsk | No | `no` |
|
||||
| `phi` | Filipino | No | `tl` |
|
||||
| `pl` | Polski | No | `pl` |
|
||||
| `pt` | Português (Portugal) | No | `pt` |
|
||||
| `pt-BR` | Português (Brasil) | No | `pt` |
|
||||
| `ro` | Română | No | `ro` |
|
||||
| `ru` | Русский | No | `ru` |
|
||||
| `sk` | Slovenčina | No | `sk` |
|
||||
| `sv` | Svenska | No | `sv` |
|
||||
| `th` | ไทย | No | `th` |
|
||||
| `tr` | Türkçe | No | `tr` |
|
||||
| `uk-UA` | Українська | No | `uk` |
|
||||
| `vi` | Tiếng Việt | No | `vi` |
|
||||
| `zh-CN` | 中文 (简体) | No | `zh-CN` |
|
||||
|
||||
## Adding a New Language
|
||||
|
||||
### 1. Register the Locale
|
||||
|
||||
Edit `src/i18n/config.ts`:
|
||||
|
||||
```ts
|
||||
// Add to LOCALES array
|
||||
"xx",
|
||||
@@ -77,7 +81,9 @@ Edit `src/i18n/config.ts`:
|
||||
```
|
||||
|
||||
### 2. Add to Generator
|
||||
|
||||
Edit `scripts/i18n/generate-multilang.mjs` — add entry to `LOCALE_SPECS`:
|
||||
|
||||
```js
|
||||
{
|
||||
code: "xx",
|
||||
@@ -91,24 +97,30 @@ Edit `scripts/i18n/generate-multilang.mjs` — add entry to `LOCALE_SPECS`:
|
||||
```
|
||||
|
||||
### 3. Generate Initial Translation
|
||||
|
||||
```bash
|
||||
node scripts/i18n/generate-multilang.mjs messages
|
||||
```
|
||||
|
||||
This creates `src/i18n/messages/xx.json` auto-translated from `en.json` via Google Translate.
|
||||
|
||||
### 4. Review & Fix Auto-Translations
|
||||
|
||||
Auto-translations are a starting point. Review manually for:
|
||||
|
||||
- Technical accuracy
|
||||
- Context-appropriate terminology
|
||||
- Proper handling of placeholders (`{count}`, `{value}`, etc.)
|
||||
|
||||
### 5. Validate
|
||||
|
||||
```bash
|
||||
python3 scripts/validate_translation.py quick -l xx
|
||||
python3 scripts/validate_translation.py diff common -l xx
|
||||
```
|
||||
|
||||
### 6. Generate Translated Documentation
|
||||
|
||||
```bash
|
||||
node scripts/i18n/generate-multilang.mjs docs
|
||||
```
|
||||
@@ -123,15 +135,16 @@ node scripts/i18n/generate-multilang.mjs docs
|
||||
node scripts/i18n/generate-multilang.mjs [messages|readme|docs|all]
|
||||
```
|
||||
|
||||
| Mode | What it does |
|
||||
|------|-------------|
|
||||
| `messages` | Translates missing keys in `src/i18n/messages/{locale}.json` from `en.json` |
|
||||
| `readme` | Translates `README.md` into all locales as `README.{code}.md` in project root |
|
||||
| `docs` | Translates `DOC_SOURCE_FILES` into `docs/i18n/{locale}/{docName}` |
|
||||
| `all` | Runs all three modes |
|
||||
| Mode | What it does |
|
||||
| ---------- | ----------------------------------------------------------------------------- |
|
||||
| `messages` | Translates missing keys in `src/i18n/messages/{locale}.json` from `en.json` |
|
||||
| `readme` | Translates `README.md` into all locales as `README.{code}.md` in project root |
|
||||
| `docs` | Translates `DOC_SOURCE_FILES` into `docs/i18n/{locale}/{docName}` |
|
||||
| `all` | Runs all three modes |
|
||||
|
||||
**Features:**
|
||||
- **Text protection**: Masks code blocks (```` ``` ````), inline code (`` ` ``), markdown links/images (`[text](url)`), HTML tags, tables, and ICU placeholders (`{count}`, `{value}`, `{total}`, etc.) before translation, then restores them
|
||||
|
||||
- **Text protection**: Masks code blocks (` ``` `), inline code (`` ` ``), markdown links/images (`[text](url)`), HTML tags, tables, and ICU placeholders (`{count}`, `{value}`, `{total}`, etc.) before translation, then restores them
|
||||
- **Chunked batching**: Joins multiple strings with `__OMNIROUTE_I18N_SEPARATOR__` delimiters to minimize API calls (max 1800 chars per request)
|
||||
- **In-memory cache**: Avoids redundant API calls for repeated strings within a session
|
||||
- **Retry logic**: Exponential backoff (up to 5 attempts with 300ms × attempt delay) for 429/5xx errors
|
||||
@@ -139,6 +152,7 @@ node scripts/i18n/generate-multilang.mjs [messages|readme|docs|all]
|
||||
- **Skip existing**: If target file already exists, it is NOT overwritten
|
||||
|
||||
**Important behaviors:**
|
||||
|
||||
- `docs/i18n/README.md` is **regenerated** each run — it's an auto-generated index of all docs
|
||||
- Root `README.{code}.md` files are only created if they don't exist (skips locales in `EXISTING_README_CODES`)
|
||||
- Language bars (`🌐 **Languages:** ...`) are automatically inserted/updated in all translated docs
|
||||
@@ -155,6 +169,7 @@ python3 scripts/i18n_autotranslate.py \
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- Scans `docs/i18n/` markdown files for English paragraphs
|
||||
- Skips code blocks, tables, and already-translated content
|
||||
- Sends paragraphs to LLM with technical translation system prompt
|
||||
@@ -189,6 +204,7 @@ python3 scripts/validate_translation.py -l cs
|
||||
```
|
||||
|
||||
**Detects:**
|
||||
|
||||
- **Missing keys** — keys in `en.json` but not in locale file
|
||||
- **Extra keys** — keys in locale file but not in `en.json`
|
||||
- **Untranslated keys** — keys where locale value equals English source (excluding allowlist)
|
||||
@@ -228,6 +244,7 @@ node scripts/i18n/generate-qa-checklist.mjs
|
||||
```
|
||||
|
||||
**Checks:**
|
||||
|
||||
- Fixed-width class usage (overflow risk)
|
||||
- Directional left/right classes (RTL risk)
|
||||
- Clipping-prone patterns
|
||||
@@ -252,6 +269,7 @@ QA_ROUTES=/dashboard/settings,/dashboard/providers node scripts/i18n/run-visual-
|
||||
```
|
||||
|
||||
**Detects:**
|
||||
|
||||
- Text overflow
|
||||
- Element clipping
|
||||
- RTL layout mismatches
|
||||
@@ -279,6 +297,7 @@ Allowlist of keys that should remain identical to English source. Used by `valid
|
||||
```
|
||||
|
||||
**What belongs here:**
|
||||
|
||||
- Brand/product names: `landing.brandName`, `common.social-github`
|
||||
- Technical terms/acronyms: `health.cpu`, `mcpDashboard.pid`, `settings.ai`
|
||||
- ICU/format strings: `apiManager.modelsCount`, `health.millisecondsShort`
|
||||
@@ -307,6 +326,7 @@ python3 scripts/validate_translation.py quick -l '${{ matrix.lang }}'
|
||||
```
|
||||
|
||||
**Dashboard output:**
|
||||
|
||||
```
|
||||
## 🌍 Translations
|
||||
| Metric | Value |
|
||||
@@ -360,6 +380,7 @@ docs/
|
||||
## Best Practices
|
||||
|
||||
### When Editing Translations
|
||||
|
||||
1. **Always edit `en.json` first** — it's the source of truth
|
||||
2. **Run `generate-multilang.mjs messages`** to propagate new keys to all locales
|
||||
3. **Review auto-translations** — Google Translate is a starting point, not final
|
||||
@@ -367,11 +388,13 @@ docs/
|
||||
5. **Update `untranslatable-keys.json`** if a key should remain in English
|
||||
|
||||
### Placeholder Safety
|
||||
|
||||
- ICU placeholders (`{count}`, `{value}`, `{total}`, `{seconds}`) must be preserved exactly
|
||||
- Plural formats (`{count, plural, one {# model} other {# models}}`) must maintain structure
|
||||
- The validator detects placeholder mismatches automatically
|
||||
|
||||
### Adding New Translation Keys in Code
|
||||
|
||||
```tsx
|
||||
// Use namespaced keys
|
||||
const t = useTranslations("settings");
|
||||
@@ -382,6 +405,7 @@ python3 scripts/check_translations.py --verbose
|
||||
```
|
||||
|
||||
### RTL Considerations
|
||||
|
||||
- Arabic (`ar`) and Hebrew (`he`) are RTL locales
|
||||
- Avoid hardcoded `left`/`right` CSS — use `start`/`end` logical properties
|
||||
- Visual QA catches RTL layout mismatches via `run-visual-qa.mjs`
|
||||
@@ -389,21 +413,29 @@ python3 scripts/check_translations.py --verbose
|
||||
## Known Issues & History
|
||||
|
||||
### `in.json` → `hi.json` Fix
|
||||
|
||||
The generator originally used `code: "in"` (deprecated Google Translate code) for Hindi instead of the correct ISO 639-1 `hi`. This created an orphaned `in.json` duplicate of `hi.json`. Fixed by changing `code: "in"` to `code: "hi"` in `generate-multilang.mjs` and removing the orphaned file.
|
||||
|
||||
> ⚠️ **Audit (2026-05-13):** The `docs/i18n/in/` directory still exists on disk (full duplicate of `hi/`). Translation generator no longer writes to it, but the historical tree was not pruned. Safe to delete with `rm -rf docs/i18n/in/` after confirming no external links reference the old path.
|
||||
|
||||
### `docs/i18n/README.md` Is Auto-Generated
|
||||
|
||||
The `docs/i18n/README.md` file is completely regenerated by `generate-multilang.mjs docs`. Any manual edits will be lost. Use `docs/I18N.md` (this file) for hand-written documentation that should persist.
|
||||
|
||||
### External Untranslatable Keys List
|
||||
|
||||
The `untranslatable-keys.json` allowlist was moved from an inline Python set in `validate_translation.py` to an external JSON file for easier maintenance. The validator loads it at runtime.
|
||||
|
||||
### `generate-multilang.mjs` Hindi Code Fix
|
||||
|
||||
The generator originally used `code: "in"` (deprecated Google Translate code) for Hindi instead of the correct ISO 639-1 `hi`. This was introduced in upstream commit `952b0b22c` by `diegosouzapw`. Fixed by changing `code: "in"` to `code: "hi"` in the `LOCALE_SPECS` array and removing the orphaned `in.json` file.
|
||||
|
||||
### `validate_translation.py` Ignored Count Output
|
||||
|
||||
The `quick` check now displays the count of ignored keys from `untranslatable-keys.json`:
|
||||
|
||||
```
|
||||
Missing: 0
|
||||
Untranslated: 0
|
||||
Ignored (UNTRANSLATABLE_KEYS): 236
|
||||
Ignored (UNTRANSLATABLE_KEYS): <varies per release>
|
||||
```
|
||||
|
||||
@@ -12,6 +12,8 @@ pkg upgrade
|
||||
pkg install nodejs-lts python build-essential git
|
||||
```
|
||||
|
||||
> **Node.js version:** OmniRoute requires Node `>=20.20.2 <21 || >=22.22.2 <23 || >=24.0.0 <27` (per `engines` in `package.json`). Termux's `nodejs-lts` typically ships Node 20 LTS, which is compatible. If `node --version` reports an older line, install `pkg install nodejs` (current) and verify the major matches a supported range.
|
||||
|
||||
If native package compilation fails, rerun the `pkg install` command above and then retry the OmniRoute install.
|
||||
|
||||
## Install
|
||||
|
||||
@@ -83,6 +83,10 @@
|
||||
"check:route-validation:t06": "node scripts/check-route-validation.mjs",
|
||||
"check:any-budget:t11": "node scripts/check-t11-any-budget.mjs",
|
||||
"check:docs-sync": "node scripts/check-docs-sync.mjs",
|
||||
"check:env-doc-sync": "node scripts/check-env-doc-sync.mjs",
|
||||
"check:docs-counts": "node scripts/check-docs-counts-sync.mjs",
|
||||
"check:deprecated-versions": "node scripts/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",
|
||||
"check:node-runtime": "node --import tsx/esm scripts/check-supported-node-runtime.ts",
|
||||
"check:pack-artifact": "node --import tsx/esm scripts/validate-pack-artifact.ts",
|
||||
"audit:deps": "npm audit --audit-level=moderate && npm run audit:electron",
|
||||
|
||||
130
scripts/check-deprecated-versions.mjs
Normal file
130
scripts/check-deprecated-versions.mjs
Normal file
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env node
|
||||
// Detects hardcoded old versions / stale dates in docs that should follow the current release.
|
||||
// Uses hardcoded regexes to avoid dynamic RegExp() (ReDoS concern flagged by semgrep).
|
||||
// Exits 0 if clean, 1 (in --strict mode) if drift detected.
|
||||
//
|
||||
// Run: node scripts/check-deprecated-versions.mjs
|
||||
// Strict: node scripts/check-deprecated-versions.mjs --strict
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(__dirname, "..");
|
||||
const DOCS_DIR = path.join(ROOT, "docs");
|
||||
const PKG_JSON = path.join(ROOT, "package.json");
|
||||
const STRICT = process.argv.includes("--strict");
|
||||
|
||||
function currentVersion() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(PKG_JSON, "utf8")).version || "0.0.0";
|
||||
} catch {
|
||||
return "0.0.0";
|
||||
}
|
||||
}
|
||||
|
||||
// Hardcoded set of obviously-stale version patterns. Update when bumping major/minor.
|
||||
// These match any version <= v3.6.x or any pre-3 major.
|
||||
const STALE_VERSION_PATTERNS = [
|
||||
/\bv?[12]\.\d+\.\d+\b/, // 1.x.x / 2.x.x
|
||||
/\bv?3\.[0-6]\.\d+\b/, // 3.0.x..3.6.x
|
||||
];
|
||||
|
||||
const SAFE_CONTEXTS =
|
||||
/(historical|archive|legacy|previously|deprecated|since|introduced|was|originally|fix|fixed)/i;
|
||||
|
||||
// Dates older than 60 days are considered stale for "Last updated" / "Last consolidated".
|
||||
const STALE_DATE_DAYS = 60;
|
||||
const today = new Date();
|
||||
|
||||
const LAST_UPDATED_RE = /Last (?:updated|consolidated|generated)[^\d]{0,30}(\d{4}-\d{2}-\d{2})/i;
|
||||
|
||||
function isStaleDate(yyyy_mm_dd) {
|
||||
const d = new Date(yyyy_mm_dd);
|
||||
if (Number.isNaN(d.getTime())) return false;
|
||||
const ageDays = (today - d) / (1000 * 60 * 60 * 24);
|
||||
return ageDays > STALE_DATE_DAYS;
|
||||
}
|
||||
|
||||
const IGNORED_DIRS = new Set(["archive", "i18n", "superpowers"]);
|
||||
const IGNORED_BASENAMES = new Set(["CHANGELOG.md", "RFC-AUTO-ASSESSMENT-DRAFT.md"]);
|
||||
|
||||
function walkDocs(dir) {
|
||||
const out = [];
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (IGNORED_DIRS.has(entry.name)) continue;
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
out.push(...walkDocs(full));
|
||||
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
||||
if (IGNORED_BASENAMES.has(entry.name)) continue;
|
||||
out.push(full);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const cur = currentVersion();
|
||||
console.log(`Version drift report (current: v${cur})`);
|
||||
console.log("=".repeat(40));
|
||||
|
||||
const files = walkDocs(DOCS_DIR);
|
||||
let drift = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const rel = path.relative(ROOT, file);
|
||||
const txt = fs.readFileSync(file, "utf8");
|
||||
const lines = txt.split("\n");
|
||||
const issues = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (line.length > 500) continue; // skip very long lines (likely code blocks / data)
|
||||
|
||||
// 1. Stale version refs
|
||||
for (const re of STALE_VERSION_PATTERNS) {
|
||||
const m = re.exec(line);
|
||||
if (m && !SAFE_CONTEXTS.test(line)) {
|
||||
issues.push({
|
||||
line: i + 1,
|
||||
type: "stale-version",
|
||||
match: m[0],
|
||||
text: line.trim().slice(0, 100),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 2. Stale "Last updated/consolidated/generated" dates
|
||||
const dateMatch = LAST_UPDATED_RE.exec(line);
|
||||
if (dateMatch && isStaleDate(dateMatch[1])) {
|
||||
issues.push({
|
||||
line: i + 1,
|
||||
type: "stale-date",
|
||||
match: dateMatch[1],
|
||||
text: line.trim().slice(0, 100),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (issues.length > 0) {
|
||||
console.log(`\n ${rel}`);
|
||||
for (const iss of issues.slice(0, 5)) {
|
||||
console.log(` L${iss.line} [${iss.type}] ${iss.match}: ${iss.text}`);
|
||||
}
|
||||
if (issues.length > 5) console.log(` ... and ${issues.length - 5} more`);
|
||||
drift += issues.length;
|
||||
}
|
||||
}
|
||||
|
||||
console.log();
|
||||
if (drift > 0) {
|
||||
console.warn(`⚠ ${drift} potential drift(s) detected across ${files.length} doc files.`);
|
||||
if (STRICT) process.exit(1);
|
||||
} else {
|
||||
console.log(`✓ No drift detected across ${files.length} doc files.`);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
115
scripts/check-docs-counts-sync.mjs
Normal file
115
scripts/check-docs-counts-sync.mjs
Normal file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env node
|
||||
// Validates that count-based assertions in docs match the actual code state.
|
||||
// Examples checked:
|
||||
// - executors count in open-sse/executors/
|
||||
// - routing strategies in src/shared/constants/routingStrategies.ts
|
||||
// - OAuth providers in src/lib/oauth/providers/
|
||||
// - A2A skills in src/lib/a2a/skills/
|
||||
// - Cloud agents in src/lib/cloudAgent/agents/
|
||||
//
|
||||
// Exits 0 on success, 1 on detected drift.
|
||||
// Run: node scripts/check-docs-counts-sync.mjs
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(__dirname, "..");
|
||||
|
||||
const COMMON_NON_IMPL_BASENAMES = new Set([
|
||||
"index.ts",
|
||||
"index.mts",
|
||||
"types.ts",
|
||||
"base.ts",
|
||||
"constants.ts",
|
||||
]);
|
||||
|
||||
function countFiles(dir, suffix = ".ts") {
|
||||
const abs = path.join(ROOT, dir);
|
||||
if (!fs.existsSync(abs)) return 0;
|
||||
return fs
|
||||
.readdirSync(abs)
|
||||
.filter(
|
||||
(f) =>
|
||||
f.endsWith(suffix) &&
|
||||
!f.endsWith(".test.ts") &&
|
||||
!f.startsWith("__") &&
|
||||
!COMMON_NON_IMPL_BASENAMES.has(f)
|
||||
).length;
|
||||
}
|
||||
|
||||
function countRoutingStrategies() {
|
||||
const file = path.join(ROOT, "src", "shared", "constants", "routingStrategies.ts");
|
||||
if (!fs.existsSync(file)) return 0;
|
||||
const txt = fs.readFileSync(file, "utf8");
|
||||
const m = txt.match(/ROUTING_STRATEGY_VALUES\s*=\s*\[([^\]]*)\]/);
|
||||
if (!m) return 0;
|
||||
return (m[1].match(/"[^"]+"/g) || []).length;
|
||||
}
|
||||
|
||||
function docContains(docPath, needle) {
|
||||
const abs = path.join(ROOT, "docs", docPath);
|
||||
if (!fs.existsSync(abs)) return false;
|
||||
return fs.readFileSync(abs, "utf8").includes(needle);
|
||||
}
|
||||
|
||||
const checks = [
|
||||
{
|
||||
label: "Executors count",
|
||||
actual: countFiles("open-sse/executors"),
|
||||
docKey: "executors",
|
||||
docs: ["ARCHITECTURE.md", "CODEBASE_DOCUMENTATION.md"],
|
||||
},
|
||||
{
|
||||
label: "Routing strategies count",
|
||||
actual: countRoutingStrategies(),
|
||||
docKey: "strategies",
|
||||
docs: ["AUTO-COMBO.md", "RESILIENCE_GUIDE.md"],
|
||||
},
|
||||
{
|
||||
label: "OAuth providers count",
|
||||
actual: countFiles("src/lib/oauth/providers"),
|
||||
docKey: "OAuth providers",
|
||||
docs: ["ARCHITECTURE.md"],
|
||||
},
|
||||
{
|
||||
label: "A2A skills count",
|
||||
actual: countFiles("src/lib/a2a/skills"),
|
||||
docKey: "A2A skills",
|
||||
docs: ["A2A-SERVER.md"],
|
||||
},
|
||||
{
|
||||
label: "Cloud agents count",
|
||||
actual: countFiles("src/lib/cloudAgent/agents"),
|
||||
docKey: "cloud agents",
|
||||
docs: ["CLOUD_AGENT.md", "AGENT_PROTOCOLS_GUIDE.md"],
|
||||
},
|
||||
];
|
||||
|
||||
let drift = 0;
|
||||
console.log("Docs counts sync report");
|
||||
console.log("=======================");
|
||||
|
||||
for (const c of checks) {
|
||||
console.log(`\n• ${c.label}: ${c.actual} (real)`);
|
||||
for (const doc of c.docs) {
|
||||
const found = docContains(doc, String(c.actual));
|
||||
if (found) {
|
||||
console.log(` ✓ docs/${doc} mentions "${c.actual}"`);
|
||||
} else {
|
||||
console.log(` ⚠ docs/${doc} does NOT mention "${c.actual}" for ${c.docKey}`);
|
||||
drift++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log();
|
||||
if (drift > 0) {
|
||||
console.warn(`⚠ ${drift} potential drift(s) detected. Review the docs above.`);
|
||||
// Soft-fail by default (count-based heuristic can false-positive).
|
||||
// To enforce, pass --strict.
|
||||
if (process.argv.includes("--strict")) process.exit(1);
|
||||
} else {
|
||||
console.log("✓ All checks pass.");
|
||||
}
|
||||
131
scripts/check-env-doc-sync.mjs
Normal file
131
scripts/check-env-doc-sync.mjs
Normal file
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env node
|
||||
// Validates that env vars referenced in code appear in .env.example AND in docs/ENVIRONMENT.md.
|
||||
// Exits 0 on success, 1 on missing entries. Designed for use in pre-commit / CI.
|
||||
//
|
||||
// Run: node scripts/check-env-doc-sync.mjs
|
||||
// Strict mode: node scripts/check-env-doc-sync.mjs --strict
|
||||
// In strict mode, missing entries cause failure. In default mode, only summary is printed.
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(__dirname, "..");
|
||||
const ENV_EXAMPLE = path.join(ROOT, ".env.example");
|
||||
const ENV_DOC = path.join(ROOT, "docs", "ENVIRONMENT.md");
|
||||
|
||||
const STRICT = process.argv.includes("--strict");
|
||||
|
||||
// Vars that are intentionally not documented or detected in code via dynamic patterns.
|
||||
const IGNORE = new Set([
|
||||
"NODE_ENV",
|
||||
"PATH",
|
||||
"HOME",
|
||||
"USER",
|
||||
"PWD",
|
||||
"SHELL",
|
||||
"TERM",
|
||||
"TZ",
|
||||
"LANG",
|
||||
"LC_ALL",
|
||||
"CI",
|
||||
"GITHUB_ACTIONS",
|
||||
"RUNNER_OS",
|
||||
// Add false positives here as discovered.
|
||||
]);
|
||||
|
||||
function readEnvExampleVars() {
|
||||
if (!fs.existsSync(ENV_EXAMPLE)) {
|
||||
console.error(`✗ ${ENV_EXAMPLE} not found`);
|
||||
process.exit(2);
|
||||
}
|
||||
const txt = fs.readFileSync(ENV_EXAMPLE, "utf8");
|
||||
const vars = new Set();
|
||||
for (const line of txt.split("\n")) {
|
||||
// Match both "VAR=value" and "# VAR=value" (commented-out examples are still documented)
|
||||
const m = line.match(/^#?\s*([A-Z][A-Z0-9_]+)\s*=/);
|
||||
if (m) vars.add(m[1]);
|
||||
}
|
||||
return vars;
|
||||
}
|
||||
|
||||
function readEnvDocVars() {
|
||||
if (!fs.existsSync(ENV_DOC)) {
|
||||
console.error(`✗ ${ENV_DOC} not found`);
|
||||
process.exit(2);
|
||||
}
|
||||
const txt = fs.readFileSync(ENV_DOC, "utf8");
|
||||
const vars = new Set();
|
||||
// Match `VAR_NAME` in inline code or table cells.
|
||||
for (const m of txt.matchAll(/`([A-Z][A-Z0-9_]{2,})`/g)) {
|
||||
vars.add(m[1]);
|
||||
}
|
||||
return vars;
|
||||
}
|
||||
|
||||
function readCodeVars() {
|
||||
const vars = new Set();
|
||||
let stdout;
|
||||
try {
|
||||
stdout = execSync(
|
||||
"grep -rhoE 'process\\.env\\.[A-Z][A-Z0-9_]+' src/ open-sse/ bin/ scripts/ 2>/dev/null || true",
|
||||
{ cwd: ROOT, encoding: "utf8", maxBuffer: 20 * 1024 * 1024 }
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(`✗ grep failed: ${e.message}`);
|
||||
process.exit(2);
|
||||
}
|
||||
for (const line of stdout.split("\n")) {
|
||||
const m = line.match(/^process\.env\.([A-Z][A-Z0-9_]+)$/);
|
||||
if (m && !IGNORE.has(m[1])) vars.add(m[1]);
|
||||
}
|
||||
return vars;
|
||||
}
|
||||
|
||||
function diff(set, against) {
|
||||
return [...set].filter((v) => !against.has(v)).sort();
|
||||
}
|
||||
|
||||
function main() {
|
||||
const codeVars = readCodeVars();
|
||||
const exampleVars = readEnvExampleVars();
|
||||
const docVars = readEnvDocVars();
|
||||
|
||||
const inCodeMissingExample = diff(codeVars, exampleVars);
|
||||
const inCodeMissingDoc = diff(codeVars, docVars);
|
||||
const inExampleMissingDoc = diff(exampleVars, docVars);
|
||||
const inExampleMissingCode = diff(exampleVars, codeVars);
|
||||
|
||||
console.log("Env var sync report");
|
||||
console.log("===================");
|
||||
console.log(`Code references: ${codeVars.size} unique vars`);
|
||||
console.log(`In .env.example: ${exampleVars.size} unique vars`);
|
||||
console.log(`In docs/ENVIRONMENT.md: ${docVars.size} unique vars (heuristic)`);
|
||||
console.log();
|
||||
|
||||
function printList(label, list) {
|
||||
if (list.length === 0) {
|
||||
console.log(` ✓ ${label}: none`);
|
||||
} else {
|
||||
console.log(` ⚠ ${label}: ${list.length}`);
|
||||
for (const v of list.slice(0, 30)) console.log(` - ${v}`);
|
||||
if (list.length > 30) console.log(` ... and ${list.length - 30} more`);
|
||||
}
|
||||
}
|
||||
|
||||
printList("In code but missing from .env.example", inCodeMissingExample);
|
||||
printList("In code but missing from ENVIRONMENT.md", inCodeMissingDoc);
|
||||
printList("In .env.example but missing from ENVIRONMENT.md", inExampleMissingDoc);
|
||||
printList("In .env.example but not referenced in code (dead?)", inExampleMissingCode);
|
||||
|
||||
const errors = inCodeMissingExample.length + inExampleMissingDoc.length;
|
||||
if (STRICT && errors > 0) {
|
||||
console.error(`\n✗ ${errors} drift(s) detected (strict mode)`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`\n${errors === 0 ? "✓" : "⚠"} Done.`);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user