Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ import { PREFLIGHT_LIMITS } from "../signals/preflight-limits";
import { SCENARIO_MAX_BRANCH_REF_CHARS, SCENARIO_MAX_LINKED_ISSUE_NUMBERS, SCENARIO_MAX_REPO_FULL_NAME_CHARS } from "../scenarios/input-model";
import { loadUpstreamStatus } from "../upstream/ruleset";
import { simulateOpenPrPressure, type OpenPrPressureInput } from "../services/open-pr-pressure-scenarios";
import { buildFindingTaxonomyDocument, FINDING_TAXONOMY_URI } from "../review/finding-taxonomy";

type AppContext = Context<{ Bindings: Env }>;
type ToolPayload = {
Expand Down Expand Up @@ -2007,6 +2008,26 @@ export class GittensoryMcp {
}),
);

// #2225 — read-only taxonomy discovery for AI review finding categories + severity ladder.
server.registerResource(
"gittensory_finding_taxonomy",
FINDING_TAXONOMY_URI,
{
title: "Gittensory Finding Taxonomy",
description: "Canonical AI review finding categories and severity levels for discovery without hard-coding.",
mimeType: "application/json",
},
async () => ({
contents: [
{
uri: FINDING_TAXONOMY_URI,
mimeType: "application/json",
text: JSON.stringify(buildFindingTaxonomyDocument(), null, 2),
},
],
}),
);

return server;
}

Expand Down
18 changes: 18 additions & 0 deletions src/review/finding-taxonomy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { FINDING_CATEGORIES } from "./finding-category-classify";
import { REVIEW_FINDING_SEVERITY_LADDER } from "../signals/focus-manifest";

/** MCP resource URI for the canonical review finding taxonomy (#2225). */
export const FINDING_TAXONOMY_URI = "gittensory://finding-taxonomy" as const;

export interface FindingTaxonomyDocument {
categories: readonly (typeof FINDING_CATEGORIES)[number][];
severities: readonly (typeof REVIEW_FINDING_SEVERITY_LADDER)[number][];
}

/** Static, machine-readable taxonomy for AI review findings — categories + severity ladder. */
export function buildFindingTaxonomyDocument(): FindingTaxonomyDocument {
return {
categories: [...FINDING_CATEGORIES],
severities: [...REVIEW_FINDING_SEVERITY_LADDER],
};
}
28 changes: 28 additions & 0 deletions test/unit/finding-taxonomy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";
import { FINDING_CATEGORIES } from "../../src/review/finding-category-classify";
import { buildFindingTaxonomyDocument, FINDING_TAXONOMY_URI } from "../../src/review/finding-taxonomy";
import { REVIEW_FINDING_SEVERITY_LADDER } from "../../src/signals/focus-manifest";

describe("finding taxonomy document", () => {
it("exposes the canonical category enum and severity ladder", () => {
const doc = buildFindingTaxonomyDocument();
expect(doc.categories).toEqual([...FINDING_CATEGORIES]);
expect(doc.severities).toEqual([...REVIEW_FINDING_SEVERITY_LADDER]);
});

it("includes every category and severity value exactly once", () => {
const doc = buildFindingTaxonomyDocument();
for (const category of FINDING_CATEGORIES) {
expect(doc.categories).toContain(category);
}
for (const severity of REVIEW_FINDING_SEVERITY_LADDER) {
expect(doc.severities).toContain(severity);
}
expect(new Set(doc.categories).size).toBe(FINDING_CATEGORIES.length);
expect(new Set(doc.severities).size).toBe(REVIEW_FINDING_SEVERITY_LADDER.length);
});

it("uses the stable MCP resource URI", () => {
expect(FINDING_TAXONOMY_URI).toBe("gittensory://finding-taxonomy");
});
});
43 changes: 43 additions & 0 deletions test/unit/mcp-finding-taxonomy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { describe, expect, it } from "vitest";
import { GittensoryMcp } from "../../src/mcp/server";
import { FINDING_CATEGORIES } from "../../src/review/finding-category-classify";
import { FINDING_TAXONOMY_URI } from "../../src/review/finding-taxonomy";
import { REVIEW_FINDING_SEVERITY_LADDER } from "../../src/signals/focus-manifest";
import { createTestEnv } from "../helpers/d1";

async function connectTestClient() {
const mcpServer = new GittensoryMcp(createTestEnv()).createServer();
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await mcpServer.connect(serverTransport);
const client = new Client({ name: "gittensory-finding-taxonomy-test", version: "0.1.0" }, { capabilities: {} });
await client.connect(clientTransport);
return { client, mcpServer };
}

describe("MCP finding-taxonomy resource (#2225)", () => {
it("discovers the finding-taxonomy resource", async () => {
const { client } = await connectTestClient();
const { resources } = await client.listResources();
expect(resources.map((r) => r.uri)).toContain(FINDING_TAXONOMY_URI);
});

it("returns the canonical categories and severities as JSON", async () => {
const { client } = await connectTestClient();
const result = await client.readResource({ uri: FINDING_TAXONOMY_URI });
expect(result.contents).toHaveLength(1);
const content = result.contents[0];
expect(content?.mimeType).toBe("application/json");
if (!content || !("text" in content)) throw new Error("expected text content");
const body = JSON.parse(content.text ?? "") as { categories: string[]; severities: string[] };
expect(body.categories).toEqual([...FINDING_CATEGORIES]);
expect(body.severities).toEqual([...REVIEW_FINDING_SEVERITY_LADDER]);
for (const category of FINDING_CATEGORIES) {
expect(body.categories).toContain(category);
}
for (const severity of REVIEW_FINDING_SEVERITY_LADDER) {
expect(body.severities).toContain(severity);
}
});
});