diff --git a/README.md b/README.md index ac552ed8f..1fc8e6c21 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,11 @@ [![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents) [![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml) -[![89 MCP tools](https://img.shields.io/badge/MCP_tools-89-5c4ee5?style=flat-square)](#89-tools-for-investigation) +[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](#tool-catalog-for-investigation) [![Node.js 22+](https://img.shields.io/badge/Node.js-22.19%2B-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/) [![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE) -[Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [89 tools](#89-tools-for-investigation) · [Roadmap](#roadmap) · [How it works](#how-it-works) +[Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [Tool catalog](#tool-catalog-for-investigation) · [Roadmap](#roadmap) · [How it works](#how-it-works)
@@ -320,7 +320,7 @@ REA handles the app analysis in steps 1–5. The agent performs step 6 with its - Analyze Swift and Objective-C metadata without manually untangling every mangled symbol. - Leave names, comments, and bookmarks in Hopper so human and agent analysis reinforce each other. -## 89 tools for investigation +## Tool catalog for investigation | Tool family | Count | Examples | | ------------------------- | ----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | diff --git a/README_ar.md b/README_ar.md index 35715586c..6c693e856 100644 --- a/README_ar.md +++ b/README_ar.md @@ -10,11 +10,11 @@ [![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents) [![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml) -[![89 MCP tools](https://img.shields.io/badge/MCP_tools-89-5c4ee5?style=flat-square)](#منصة-من-89-أداة) +[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](#منصة-أدوات-التحقيق) [![Node.js 22+](https://img.shields.io/badge/Node.js-22.19%2B-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/) [![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE) -[البدء السريع](#البدء-السريع) · [من الملف التنفيذي إلى السلوك](#من-الملف-التنفيذي-إلى-السلوك) · [89 أداة](#منصة-من-89-أداة) · [كيف يعمل](#كيف-يعمل) · [الأسئلة الشائعة](#الأسئلة-الشائعة) +[البدء السريع](#البدء-السريع) · [من الملف التنفيذي إلى السلوك](#من-الملف-التنفيذي-إلى-السلوك) · [دليل الأدوات](#منصة-أدوات-التحقيق) · [كيف يعمل](#كيف-يعمل) · [الأسئلة الشائعة](#الأسئلة-الشائعة)
@@ -157,7 +157,7 @@ rea uninstall --purge-data افحص التحقق من الإدخال، والتشفير، والصلاحيات، والتخزين، ومسارات الأخطاء من دون رفع الملف التنفيذي إلى خدمة بعيدة. -## منصة من 89 أداة +## منصة أدوات التحقيق | عائلة الأدوات | العدد | أمثلة | | --------------------- | ----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | diff --git a/README_ja.md b/README_ja.md index 84405f1bd..a6ddce770 100644 --- a/README_ja.md +++ b/README_ja.md @@ -10,11 +10,11 @@ [![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents) [![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml) -[![89 MCP tools](https://img.shields.io/badge/MCP_tools-89-5c4ee5?style=flat-square)](#89-ツールのワークベンチ) +[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](#調査ツールカタログ) [![Node.js 22+](https://img.shields.io/badge/Node.js-22.19%2B-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/) [![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE) -[クイックスタート](#クイックスタート) · [バイナリから動作へ](#バイナリから動作へ) · [89 ツール](#89-ツールのワークベンチ) · [仕組み](#仕組み) · [FAQ](#faq) +[クイックスタート](#クイックスタート) · [バイナリから動作へ](#バイナリから動作へ) · [ツールカタログ](#調査ツールカタログ) · [仕組み](#仕組み) · [FAQ](#faq)
@@ -147,7 +147,7 @@ REA は手順 1〜5 のバイナリ解析を処理し、手順 6 はエージェ - Swift / Objective-C のメタデータを解析する。 - Hopper に名前、コメント、ブックマークを残し、人間とエージェントの調査を共有する。 -## 89 ツールのワークベンチ +## 調査ツールカタログ | ツール群 | 数 | 例 | | ---------------------- | --: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/README_ko.md b/README_ko.md index 5b11f0eef..244f590f3 100644 --- a/README_ko.md +++ b/README_ko.md @@ -10,11 +10,11 @@ [![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents) [![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml) -[![89 MCP tools](https://img.shields.io/badge/MCP_tools-89-5c4ee5?style=flat-square)](#89개-도구로-구성된-워크벤치) +[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](#조사-도구-카탈로그) [![Node.js 22+](https://img.shields.io/badge/Node.js-22.19%2B-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/) [![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE) -[빠른 시작](#빠른-시작) · [바이너리에서 동작까지](#바이너리에서-동작까지) · [89개 도구](#89개-도구로-구성된-워크벤치) · [작동 방식](#작동-방식) · [FAQ](#faq) +[빠른 시작](#빠른-시작) · [바이너리에서 동작까지](#바이너리에서-동작까지) · [도구 카탈로그](#조사-도구-카탈로그) · [작동 방식](#작동-방식) · [FAQ](#faq)
@@ -147,7 +147,7 @@ REA는 1–5단계의 바이너리 분석을 처리합니다. 6단계는 에이 - Swift 및 Objective-C 메타데이터를 분석합니다. - Hopper에 이름, 주석, 북마크를 남겨 사람과 에이전트의 분석을 연결합니다. -## 89개 도구로 구성된 워크벤치 +## 조사 도구 카탈로그 | 도구 그룹 | 수 | 예시 | | ------------------- | --: | --------------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/README_zh.md b/README_zh.md index 456674d9d..9d30dd0a8 100644 --- a/README_zh.md +++ b/README_zh.md @@ -10,11 +10,11 @@ [![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents) [![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml) -[![89 MCP tools](https://img.shields.io/badge/MCP_tools-89-5c4ee5?style=flat-square)](#89-个工具组成的工作台) +[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](#调查工具目录) [![Node.js 22+](https://img.shields.io/badge/Node.js-22.19%2B-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/) [![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE) -[快速开始](#快速开始) · [从二进制到行为](#从二进制到行为) · [89 个工具](#89-个工具组成的工作台) · [工作原理](#工作原理) · [常见问题](#常见问题) +[快速开始](#快速开始) · [从二进制到行为](#从二进制到行为) · [工具目录](#调查工具目录) · [工作原理](#工作原理) · [常见问题](#常见问题)
@@ -147,7 +147,7 @@ REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常 - 分析 Swift 和 Objective-C 元数据。 - 在 Hopper 中留下名称、注释与书签,使人与智能体的分析互相增强。 -## 89 个工具组成的工作台 +## 调查工具目录 | 工具类别 | 数量 | 示例 | | -------------- | ---: | ------------------------------------------------------------------------------------------------------------------------- | diff --git a/docs/architecture.mermaid b/docs/architecture.mermaid index 091fcf491..db6384914 100644 --- a/docs/architecture.mermaid +++ b/docs/architecture.mermaid @@ -33,7 +33,7 @@ graph TD end subgraph "Contracts Layer" - CONTRACTS["src/contracts/
Caller-visible tool schemas
86 analysis, managed, browser, Electron, application, and session tools"] + CONTRACTS["src/contracts/
Caller-visible schemas for analysis, managed, browser, Electron, application, and session tools"] end subgraph "Domain Layer" diff --git a/docs/mcp-prompts.md b/docs/mcp-prompts.md index e6349a580..ff2ce7aec 100644 --- a/docs/mcp-prompts.md +++ b/docs/mcp-prompts.md @@ -1,7 +1,7 @@ # Guided MCP prompts and completion REA exposes six provider-neutral investigation workflows through the MCP -`prompts` capability. They complement the 89 tools; they do not add, remove, or +`prompts` capability. They complement the generated tool catalog; they do not add, remove, or invoke tools by themselves. ## Prompt inventory diff --git a/scripts/generate-skill-metadata.mjs b/scripts/generate-skill-metadata.mjs new file mode 100644 index 000000000..3c683aca6 --- /dev/null +++ b/scripts/generate-skill-metadata.mjs @@ -0,0 +1,35 @@ +import { readFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +import { ensureGeneratedFile } from "./lib/generated-file.mjs"; + +const arguments_ = new Set(process.argv.slice(2)); +for (const argument of arguments_) + if (argument !== "--check") + throw new Error(`Unknown skill metadata option: ${argument}`); + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const path = join(root, "skills/rea-analysis/SKILL.md"); +const current = await readFile(path, "utf8"); +const { CATALOG_IDENTITY } = await import( + `${pathToFileURL(join(root, "dist/catalogIdentity.js")).href}?${String(Date.now())}` +); +const withCount = current.replace( + /^\s{2}tool_count:\s*\d+\s*$/mu, + ` tool_count: ${String(CATALOG_IDENTITY.counts.mcp_tools)}`, +); +const digestLine = ` catalog_digest: "${CATALOG_IDENTITY.digests.combined_sha256}"`; +const source = /^\s{2}catalog_digest:/mu.test(withCount) + ? withCount.replace( + /^\s{2}catalog_digest:\s*"[a-f0-9]{64}"\s*$/mu, + digestLine, + ) + : withCount.replace(/^(\s{2}tool_count:\s*\d+\s*)$/mu, `$1\n${digestLine}`); + +await ensureGeneratedFile({ + path, + source, + check: arguments_.has("--check"), + generateCommand: "npm run docs:generate", +}); diff --git a/scripts/lib/docs-facts.mjs b/scripts/lib/docs-facts.mjs index 4171544a1..3c14951b2 100644 --- a/scripts/lib/docs-facts.mjs +++ b/scripts/lib/docs-facts.mjs @@ -10,26 +10,20 @@ const README_PATHS = [ ]; const familyLabel = (id) => (id === "electron" ? "Electron" : id); -const tableCounts = (content, path, total) => { +const tableCounts = (content, path, expectedCounts) => { const lines = content.split(/\r?\n/u); - const headingIndex = lines.findIndex( - (line) => line.startsWith("## ") && line.includes(String(total)), - ); - if (headingIndex === -1) { - throw new Error(`Missing ${total}-tool heading in ${path}`); - } - const counts = []; - for (const line of lines.slice(headingIndex + 1)) { - if (line.startsWith("## ")) break; - if (!line.trim().startsWith("|") || /^\|\s*-/u.test(line)) continue; - const count = Number(line.split("|")[2]?.trim()); - if (!Number.isInteger(count)) { - if (counts.length === 0) continue; - throw new Error(`Non-numeric tool count in ${path}`); + for (const [index, line] of lines.entries()) { + if (!/^\|\s*-/u.test(line)) continue; + const counts = []; + for (const row of lines.slice(index + 1)) { + if (!row.trim().startsWith("|")) break; + const count = Number(row.split("|")[2]?.trim()); + if (!Number.isInteger(count)) break; + counts.push(count); } - counts.push(count); + if (counts.length === expectedCounts.length) return counts; } - return counts; + throw new Error(`Missing tool-family inventory table in ${path}`); }; const requireText = (issues, path, content, expected) => { @@ -42,16 +36,11 @@ export const documentationFactIssues = async (root, catalog) => { const expectedCounts = catalog.tools.families.map(({ count }) => count); for (const path of README_PATHS) { const content = await readFile(join(root, path), "utf8"); - requireText( - issues, - path, - content, - `MCP_tools-${String(catalog.tools.total)}`, - ); + requireText(issues, path, content, "MCP-tool_catalog"); for (const client of catalog.setup_clients) requireText(issues, path, content, client.display_name); try { - const actualCounts = tableCounts(content, path, catalog.tools.total); + const actualCounts = tableCounts(content, path, expectedCounts); if (JSON.stringify(actualCounts) !== JSON.stringify(expectedCounts)) issues.push( `${path}: tool family counts ${JSON.stringify(actualCounts)} do not match ${JSON.stringify(expectedCounts)}`, diff --git a/skills/rea-analysis/SKILL.md b/skills/rea-analysis/SKILL.md index a82349914..280c2111f 100644 --- a/skills/rea-analysis/SKILL.md +++ b/skills/rea-analysis/SKILL.md @@ -4,6 +4,7 @@ description: Reverse engineer native, Electron/JavaScript, and web applications metadata: version: "20" tool_count: 89 + catalog_digest: "59253c96793f162725d5dc08e611c1e0c19938b23de1e9490dcc1cc800dde955" --- # REA diff --git a/src/application/Doctor.ts b/src/application/Doctor.ts index 1df5b9e7c..4bbfa28c7 100644 --- a/src/application/Doctor.ts +++ b/src/application/Doctor.ts @@ -77,10 +77,18 @@ export interface DoctorHost { providerInspections?(): Promise; installationPaths?(): Promise; installedSkillVersion?(): Promise; + installedSkillIdentity?(): Promise; clientRegistrations?(): Promise; javascriptReplayCheck?(): Promise; } +/** Parsed identity committed by an installed REA skill. */ +interface InstalledSkillIdentity { + readonly version: string | null; + readonly toolCount: number | null; + readonly catalogDigest: string | null; +} + /** * Check requirements and an optional target without mutating the host. * Every failed check includes remediation suitable for either the one-shot CLI @@ -109,7 +117,9 @@ export const runDoctor = async ( }; readonly skill: { readonly installed_version: string | null; - readonly state: "aligned" | "stale" | "unknown"; + readonly installed_tool_count: number | null; + readonly installed_catalog_digest: string | null; + readonly state: "aligned" | "stale" | "missing"; readonly remediation: string | null; }; readonly registrations: readonly ClientRegistrationStatus[]; @@ -216,7 +226,37 @@ export const runDoctor = async ( }), ); const installationPaths = (await host.installationPaths?.()) ?? []; - const installedSkillVersion = await host.installedSkillVersion?.(); + const observedSkillIdentity = await host.installedSkillIdentity?.(); + const legacySkillVersion = + observedSkillIdentity === undefined + ? await host.installedSkillVersion?.() + : undefined; + const installedSkillIdentity = + observedSkillIdentity ?? + (legacySkillVersion === undefined + ? undefined + : { + version: legacySkillVersion, + toolCount: null, + catalogDigest: null, + }); + const installedSkillVersion = installedSkillIdentity?.version ?? undefined; + const skillAligned = + installedSkillIdentity?.version === PRODUCT_IDENTITY.skillVersion && + installedSkillIdentity.toolCount === CATALOG_IDENTITY.counts.mcp_tools && + installedSkillIdentity.catalogDigest === + CATALOG_IDENTITY.digests.combined_sha256; + if (!skillAligned) + checks.push({ + name: "skill:identity", + ok: false, + classification: "config_drift", + detail: + installedSkillIdentity === undefined + ? "Installed REA skill identity is missing." + : "Installed REA skill identity is stale.", + remediation: "Run rea setup to update the installed REA skill.", + }); const registrations = (await host.clientRegistrations?.()) ?? []; for (const registration of registrations) if (registration.state !== "aligned") @@ -263,17 +303,17 @@ export const runDoctor = async ( }, skill: { installed_version: installedSkillVersion ?? null, + installed_tool_count: installedSkillIdentity?.toolCount ?? null, + installed_catalog_digest: installedSkillIdentity?.catalogDigest ?? null, state: - installedSkillVersion === undefined - ? "unknown" - : installedSkillVersion === PRODUCT_IDENTITY.skillVersion + installedSkillIdentity === undefined + ? "missing" + : skillAligned ? "aligned" : "stale", - remediation: - installedSkillVersion === undefined || - installedSkillVersion === PRODUCT_IDENTITY.skillVersion - ? null - : "Run rea setup to update the installed REA skill.", + remediation: skillAligned + ? null + : "Run rea setup to update the installed REA skill.", }, registrations, }, @@ -407,6 +447,33 @@ export const systemDoctorHost = ( return undefined; } }, + async installedSkillIdentity() { + try { + const content = await readFile( + join( + homedir(), + ".agents/skills", + PRODUCT_IDENTITY.skillName, + "SKILL.md", + ), + "utf8", + ); + const version = + /^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(content)?.[1] ?? null; + const countText = /^\s{2}tool_count:\s*(\d+)\s*$/mu.exec(content)?.[1]; + const catalogDigest = + /^\s{2}catalog_digest:\s*"([a-f0-9]{64})"\s*$/mu.exec(content)?.[1] ?? + null; + return { + version, + toolCount: + countText === undefined ? null : Number.parseInt(countText, 10), + catalogDigest, + }; + } catch { + return undefined; + } + }, clientRegistrations: () => readClientRegistrationStatuses(homedir()), }); diff --git a/tests/doctor.test.ts b/tests/doctor.test.ts index e7ed68d2c..b949912b0 100644 --- a/tests/doctor.test.ts +++ b/tests/doctor.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from "vitest"; import { runDoctor, type DoctorHost } from "../src/application/Doctor.js"; +import { CATALOG_IDENTITY } from "../src/catalogIdentity.js"; +import { PRODUCT_IDENTITY } from "../src/identity.js"; const host = (overrides: Partial = {}): DoctorHost => ({ platform: "darwin", @@ -12,6 +14,12 @@ const host = (overrides: Partial = {}): DoctorHost => ({ linuxDemoRuntimeReady: () => Promise.resolve(true), brewHopperPath: () => Promise.resolve(undefined), manualHopperPaths: () => Promise.resolve([]), + installedSkillIdentity: () => + Promise.resolve({ + version: PRODUCT_IDENTITY.skillVersion, + toolCount: CATALOG_IDENTITY.counts.mcp_tools, + catalogDigest: CATALOG_IDENTITY.digests.combined_sha256, + }), ...overrides, }); @@ -85,7 +93,12 @@ describe("doctor", () => { host({ installationPaths: () => Promise.resolve(["/usr/local/bin/rea", "/home/user/bin/rea"]), - installedSkillVersion: () => Promise.resolve("10"), + installedSkillIdentity: () => + Promise.resolve({ + version: "10", + toolCount: null, + catalogDigest: null, + }), }), ); @@ -99,6 +112,25 @@ describe("doctor", () => { }, }); }); + it("reports a missing installed skill as unhealthy", async () => { + const result = await runDoctor( + undefined, + host({ installedSkillIdentity: () => Promise.resolve(undefined) }), + ); + + expect(result.healthy).toBe(false); + expect(result.identity?.skill).toMatchObject({ + state: "missing", + remediation: "Run rea setup to update the installed REA skill.", + }); + expect(result.checks).toContainEqual({ + name: "skill:identity", + ok: false, + classification: "config_drift", + detail: "Installed REA skill identity is missing.", + remediation: "Run rea setup to update the installed REA skill.", + }); + }); it("reports stale client registration paths without claiming live state", async () => { const result = await runDoctor( undefined, diff --git a/tests/ghidraDoctor.test.ts b/tests/ghidraDoctor.test.ts index 7dd2edc6c..fd798614e 100644 --- a/tests/ghidraDoctor.test.ts +++ b/tests/ghidraDoctor.test.ts @@ -7,6 +7,8 @@ import { type GhidraInstallationInspection, } from "../src/ghidra/GhidraInstallation.js"; import { projectGhidraDoctorInspection } from "../src/ghidra/GhidraDoctor.js"; +import { CATALOG_IDENTITY } from "../src/catalogIdentity.js"; +import { PRODUCT_IDENTITY } from "../src/identity.js"; const INSTALL = "/opt/ghidra_12.1.2_PUBLIC"; const installationHost = ( @@ -65,6 +67,12 @@ const doctorHost = ( manualHopperPaths: () => Promise.resolve([]), providerInspections: () => Promise.resolve([projectGhidraDoctorInspection(ghidra)]), + installedSkillIdentity: () => + Promise.resolve({ + version: PRODUCT_IDENTITY.skillVersion, + toolCount: CATALOG_IDENTITY.counts.mcp_tools, + catalogDigest: CATALOG_IDENTITY.digests.combined_sha256, + }), }); describe("Ghidra doctor integration", () => { diff --git a/tests/identity.test.ts b/tests/identity.test.ts index 98fca65bd..8243f482a 100644 --- a/tests/identity.test.ts +++ b/tests/identity.test.ts @@ -14,7 +14,8 @@ describe("package identity", () => { scripts: z.object({ start: z.string(), prepare: z.string(), - prepack: z.string().optional(), + prepack: z.string(), + prepublishOnly: z.string(), }), }) .parse( @@ -30,6 +31,11 @@ describe("package identity", () => { expect(PRODUCT_IDENTITY.mcpCommand).toBe(`npx -y ${packageJson.name} mcp`); expect(packageJson.scripts.start).toBe("node scripts/rea.mjs mcp"); expect(packageJson.scripts.prepare).toBe("npm run build && husky"); - expect(packageJson.scripts.prepack).toBeUndefined(); + expect(packageJson.scripts.prepack).toBe( + "npm run deps:check && node scripts/generate-skill-metadata.mjs --check", + ); + expect(packageJson.scripts.prepublishOnly).toBe( + "npm run deps:check && node scripts/generate-skill-metadata.mjs --check", + ); }); }); diff --git a/tests/readmeConsistency.test.ts b/tests/readmeConsistency.test.ts index 12af7244e..ce0a56c8d 100644 --- a/tests/readmeConsistency.test.ts +++ b/tests/readmeConsistency.test.ts @@ -6,7 +6,6 @@ import { ENHANCED_TOOL_CONTRACTS, OFFICIAL_TOOL_CONTRACTS, SESSION_TOOL_CONTRACTS, - TOOL_CONTRACTS, } from "../src/contracts/toolContracts.js"; import { NATIVE_TOOL_CONTRACTS } from "../src/contracts/nativeToolContracts.js"; import { ARTIFACT_TOOL_CONTRACTS } from "../src/contracts/artifactToolContracts.js"; @@ -39,30 +38,18 @@ const expectedToolCounts = [ const toolCountsFromReadme = (content: string, path: string): number[] => { const lines = content.split(/\r?\n/u); - const headingIndex = lines.findIndex((line) => - new RegExp(`^## .*${String(TOOL_CONTRACTS.length)}`, "u").test(line), - ); - if (headingIndex === -1) - throw new Error( - `Missing ${String(TOOL_CONTRACTS.length)}-tool heading in ${path}`, - ); - - const counts: number[] = []; - for (const line of lines.slice(headingIndex + 1)) { - if (line.startsWith("## ")) break; - if (!line.trim().startsWith("|") || /^\|\s*-/.test(line)) continue; - const cells = line.split("|").map((cell) => cell.trim()); - const countCell = cells[2]; - if (countCell === undefined) - throw new Error(`Malformed table row in ${path}`); - const count = Number(countCell); - if (!Number.isInteger(count)) { - if (counts.length === 0) continue; - throw new Error(`Non-numeric tool count in ${path}`); + for (const [index, line] of lines.entries()) { + if (!/^\|\s*-/u.test(line)) continue; + const counts: number[] = []; + for (const row of lines.slice(index + 1)) { + if (!row.trim().startsWith("|")) break; + const count = Number(row.split("|")[2]?.trim()); + if (!Number.isInteger(count)) break; + counts.push(count); } - counts.push(count); + if (counts.length === expectedToolCounts.length) return counts; } - return counts; + throw new Error(`Missing tool-family inventory table in ${path}`); }; describe("localized README product facts", () => { @@ -87,7 +74,7 @@ describe("localized README product facts", () => { expect(content).toContain(client.displayName); if (path === "README_ar.md") expect(content).toContain("Windows غير مدعوم حاليًا"); - expect(content).toContain(`MCP_tools-${String(TOOL_CONTRACTS.length)}`); + expect(content).toContain("MCP-tool_catalog"); expect(toolCountsFromReadme(content, path)).toEqual(expectedToolCounts); }, );