chore(ai): generalize element-tool name regex to accept _v\d+

Five regex sites across pen-ai-skills + pen-mcp + apps/web were
anchored at `_v0$`, blocking the _v1 family from being recognized
as element tools:

  - packages/pen-ai-skills/src/corpus/output-parser.ts
    ELEMENT_TOOL_NAME_RE (filters tool_call outputs in A/B
    scorer)
  - apps/web/src/services/ai/design-parser.ts:106 (embedded
    orchestrator dispatch)
  - packages/pen-mcp/src/__tests__/design-prompt-elements.test.ts
    (×2 — stale-integration guard for elements.md)
  - packages/pen-mcp/src/__tests__/element-tool-registry-parity.test.ts
    ("every tool name matches convention" — renamed to _vN)
  - apps/web/src/services/ai/__tests__/element-tools-dispatcher.test.ts
    (drift guard for SUPPORTED_EMBEDDED_ELEMENT_TOOLS)

All now accept /^add_[a-z_]+_v\d+$/. Registry-parity test's
expectedBuilder mapping already handled both v0 (strip suffix →
buildModalShell) and v1+ (preserve → buildModalShellV1) via the
existing `.replace(/_v0$/, '')` — no change there.

Prerequisite for landing add_modal_shell_v1 as a first-class MCP
tool in the next commit.
This commit is contained in:
Fini 2026-04-22 10:55:00 +08:00
parent b07b970daa
commit 315ded51ad
5 changed files with 12 additions and 9 deletions

View file

@ -153,8 +153,8 @@ describe('dispatchElementToolCalls (multi-tag batch)', () => {
* advertised-but-not-executable output).
*/
describe('SUPPORTED_EMBEDDED_ELEMENT_TOOLS drift guard', () => {
it('covers every add_*_v0 name pen-mcp exposes', () => {
const penMcpAddNames = [...ELEMENT_TOOL_NAMES].filter((n) => /^add_.+_v0$/.test(n));
it('covers every add_*_vN name pen-mcp exposes', () => {
const penMcpAddNames = [...ELEMENT_TOOL_NAMES].filter((n) => /^add_.+_v\d+$/.test(n));
const shimSet = new Set(SUPPORTED_EMBEDDED_ELEMENT_TOOLS);
const missing = penMcpAddNames.filter((n) => !shimSet.has(n));
expect(

View file

@ -103,7 +103,7 @@ export function tryParseAllElementToolOutputs(raw: string): DesignOutputShape[]
parsed.arguments && typeof parsed.arguments === 'object'
? (parsed.arguments as Record<string, unknown>)
: {};
if (/^add_[a-z_]+_v0$/.test(parsed.name)) {
if (/^add_[a-z_]+_v\d+$/.test(parsed.name)) {
shapes.push({ kind: 'element-tool', name: parsed.name, arguments: args, raw: match[0] });
} else if (parsed.name === 'batch_design') {
const dsl = typeof args.operations === 'string' ? args.operations : '';

View file

@ -15,7 +15,10 @@ import type { ParsedOutput } from './types';
*/
const OP_TOOL_RE_G = /<op_tool>\s*([\s\S]*?)\s*<\/op_tool>/g;
const ELEMENT_TOOL_NAME_RE = /^add_[a-z_]+_v0$/;
// Accepts any numeric version suffix (_v0, _v1, …). The family
// currently ships v0 (byte-parity, hex literals) and v1 (theme-
// aware with $color-* refs). Future _v2+ lands here automatically.
const ELEMENT_TOOL_NAME_RE = /^add_[a-z_]+_v\d+$/;
/**
* Reasoning-model chain-of-thought wrapper. MiniMax M-series,

View file

@ -37,10 +37,10 @@ describe('get_design_prompt — elements section', () => {
const def = DESIGN_TOOL_DEFINITIONS.find((t) => t.name === 'get_design_prompt');
const description = def?.description ?? '';
const elementTools = DESIGN_TOOL_DEFINITIONS.map((t) => t.name).filter((n) =>
/^add_.*_v0$/.test(n),
/^add_.*_v\d+$/.test(n),
);
// (1) any element-tool name that DOES appear must be in the registry
const namedInDescription = description.match(/add_[a-z_]+_v0/g) ?? [];
const namedInDescription = description.match(/add_[a-z_]+_v\d+/g) ?? [];
for (const named of namedInDescription) {
expect(elementTools, `description references ${named} which is not in registry`).toContain(
named,
@ -76,7 +76,7 @@ describe('get_design_prompt — elements section', () => {
// such tool MUST be mentioned in the elements skill content.
const content = buildDesignPrompt('elements');
const elementTools = DESIGN_TOOL_DEFINITIONS.map((t) => t.name).filter((n) =>
/^add_.*_v0$/.test(n),
/^add_.*_v\d+$/.test(n),
);
expect(elementTools.length).toBeGreaterThan(0);
for (const n of elementTools) {

View file

@ -52,9 +52,9 @@ describe('pen-mcp element-tool registry parity', () => {
expect(ELEMENT_TOOL_DEFINITIONS.length).toBeGreaterThanOrEqual(42);
});
it('Every tool name matches the add_X_v0 convention', () => {
it('Every tool name matches the add_X_vN convention (accepts _v0, _v1, …)', () => {
for (const def of ELEMENT_TOOL_DEFINITIONS) {
expect(def.name, `tool definition: ${def.name}`).toMatch(/^add_[a-z_]+_v0$/);
expect(def.name, `tool definition: ${def.name}`).toMatch(/^add_[a-z_]+_v\d+$/);
}
});