fix(mcp): get_design_prompt reads design.md per-document (no global leak)
The earlier doc-backed fix left one leak: handleGetDesignMd/handleSetDesignMd still called setDesignMdForPrompt(spec), which wrote into a process-level module variable `_designMdContent` that get_design_prompt's "style" + "design-md" sections read. Switching between documents kept the prior file's policy; get_design_prompt itself had no filePath parameter so it couldn't even identify the current document. Fix: - Delete `_designMdContent` / setDesignMdForPrompt / getDesignMdForPrompt. - `buildDesignPrompt(section, designMdPolicy?)` takes policy as an explicit stateless argument. - Export `designMdSpecToPromptPolicy(spec)` — pure converter. - Add `filePath` to get_design_prompt's schema. The route handler opens the addressed document, derives the policy from `doc.designMd`, and threads it into buildDesignPrompt. Add `design-md` to the section enum (previously only returned via the "style" override). - design-md.ts handlers no longer touch the old setter. Verified by a two-file live smoke: set design.md on A → get_design_prompt on B returns "No design.md loaded" with no A-specific tokens.
This commit is contained in:
parent
2aabe494f5
commit
b153b37cfe
|
|
@ -3,11 +3,15 @@
|
|||
exports[`D0 parity spike — additive & gated tool registration > pre-D0 production design tool DEFINITIONS are unchanged (snapshot) > pre-d0-design-tool-definitions 1`] = `
|
||||
[
|
||||
{
|
||||
"description": "Get design knowledge prompt. Use "section" to retrieve a focused subset instead of the full prompt. Sections: schema (PenNode types), layout (flexbox rules), roles (semantic roles), text (typography/CJK/copywriting), style (visual style policy), icons (icon names), examples (design examples), guidelines (design tips), planning (layered workflow guide), elements (N-tool element-tool family reference — decision tree, PREFER/FALLBACK rules, composition pattern; the section itself enumerates the current tools). Omit section for the full prompt.",
|
||||
"description": "Get design knowledge prompt. Use "section" to retrieve a focused subset instead of the full prompt. Sections: schema (PenNode types), layout (flexbox rules), roles (semantic roles), text (typography/CJK/copywriting), style (visual style policy — reads the active document's design.md when present), icons (icon names), examples (design examples), guidelines (design tips), planning (layered workflow guide), elements (N-tool element-tool family reference), design-md (raw style policy derived from the active document's design.md, or a "no design.md" notice). Omit section for the full prompt.",
|
||||
"inputSchema": {
|
||||
"properties": {
|
||||
"filePath": {
|
||||
"description": "Path to .op file, or omit to use the live canvas. The prompt's 'style' / 'design-md' sections are derived from THIS document's design.md so the reply never leaks another file's design system.",
|
||||
"type": "string",
|
||||
},
|
||||
"section": {
|
||||
"description": "Which section of design knowledge to retrieve. Default: all. Use "planning" for layered generation workflow; "elements" for N-tool element tool reference.",
|
||||
"description": "Which section of design knowledge to retrieve. Default: all. Use "planning" for layered generation workflow; "elements" for N-tool element tool reference; "design-md" for the active document's design system.",
|
||||
"enum": [
|
||||
"all",
|
||||
"schema",
|
||||
|
|
@ -20,6 +24,7 @@ exports[`D0 parity spike — additive & gated tool registration > pre-D0 product
|
|||
"guidelines",
|
||||
"planning",
|
||||
"elements",
|
||||
"design-md",
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import { handleBatchDesign } from '../tools/batch-design';
|
||||
import { buildDesignPrompt, listPromptSections } from '../tools/design-prompt';
|
||||
import {
|
||||
buildDesignPrompt,
|
||||
listPromptSections,
|
||||
designMdSpecToPromptPolicy,
|
||||
} from '../tools/design-prompt';
|
||||
import { openDocument, resolveDocPath } from '../document-manager';
|
||||
import { handleDesignSkeleton } from '../tools/design-skeleton';
|
||||
import { handleDesignContent } from '../tools/design-content';
|
||||
import { handleDesignRefine } from '../tools/design-refine';
|
||||
|
|
@ -22,10 +27,10 @@ const CORE_DESIGN_TOOL_DEFINITIONS = [
|
|||
description:
|
||||
'Get design knowledge prompt. Use "section" to retrieve a focused subset instead of the full prompt. ' +
|
||||
'Sections: schema (PenNode types), layout (flexbox rules), roles (semantic roles), text (typography/CJK/copywriting), ' +
|
||||
'style (visual style policy), icons (icon names), examples (design examples), guidelines (design tips), ' +
|
||||
'planning (layered workflow guide), elements (N-tool element-tool family reference — decision tree, ' +
|
||||
'PREFER/FALLBACK rules, composition pattern; the section itself enumerates the current tools). ' +
|
||||
'Omit section for the full prompt.',
|
||||
"style (visual style policy — reads the active document's design.md when present), icons (icon names), " +
|
||||
'examples (design examples), guidelines (design tips), planning (layered workflow guide), ' +
|
||||
'elements (N-tool element-tool family reference), design-md (raw style policy derived from the active ' +
|
||||
'document\'s design.md, or a "no design.md" notice). Omit section for the full prompt.',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
|
|
@ -43,9 +48,15 @@ const CORE_DESIGN_TOOL_DEFINITIONS = [
|
|||
'guidelines',
|
||||
'planning',
|
||||
'elements',
|
||||
'design-md',
|
||||
],
|
||||
description:
|
||||
'Which section of design knowledge to retrieve. Default: all. Use "planning" for layered generation workflow; "elements" for N-tool element tool reference.',
|
||||
'Which section of design knowledge to retrieve. Default: all. Use "planning" for layered generation workflow; "elements" for N-tool element tool reference; "design-md" for the active document\'s design system.',
|
||||
},
|
||||
filePath: {
|
||||
type: 'string',
|
||||
description:
|
||||
"Path to .op file, or omit to use the live canvas. The prompt's 'style' / 'design-md' sections are derived from THIS document's design.md so the reply never leaks another file's design system.",
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
|
|
@ -118,16 +129,26 @@ export async function handleDesignToolCall(
|
|||
): Promise<string> {
|
||||
const a = args as any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
switch (name) {
|
||||
case 'get_design_prompt':
|
||||
case 'get_design_prompt': {
|
||||
// Derive design.md policy from THIS document so the prompt cannot
|
||||
// leak another document's design system (per-document isolation).
|
||||
let designMdPolicy: string | null = null;
|
||||
try {
|
||||
const doc = await openDocument(resolveDocPath(a.filePath as string | undefined));
|
||||
designMdPolicy = designMdSpecToPromptPolicy(doc.designMd);
|
||||
} catch {
|
||||
// live canvas / file unavailable — prompt still works without policy
|
||||
}
|
||||
return JSON.stringify(
|
||||
{
|
||||
section: (a.section as string | undefined) ?? 'all',
|
||||
availableSections: listPromptSections(),
|
||||
designPrompt: buildDesignPrompt(a.section as string | undefined),
|
||||
designPrompt: buildDesignPrompt(a.section as string | undefined, designMdPolicy),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
}
|
||||
case 'batch_design':
|
||||
return JSON.stringify(await handleBatchDesign(a), null, 2);
|
||||
case 'design_skeleton':
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import {
|
|||
extractDesignMdFromDocument,
|
||||
} from '../utils/design-md-parser';
|
||||
import type { DesignMdSpec } from '@zseven-w/pen-types';
|
||||
import { setDesignMdForPrompt } from './design-prompt';
|
||||
|
||||
/**
|
||||
* design.md is now stored on the PenDocument (`doc.designMd`). It travels
|
||||
|
|
@ -46,7 +45,6 @@ export async function handleGetDesignMd(
|
|||
const doc = await openDocument(filePath);
|
||||
|
||||
if (doc.designMd) {
|
||||
setDesignMdForPrompt(doc.designMd);
|
||||
return {
|
||||
hasDesignMd: true,
|
||||
spec: doc.designMd,
|
||||
|
|
@ -85,7 +83,6 @@ export async function handleSetDesignMd(
|
|||
|
||||
doc.designMd = spec;
|
||||
await saveDocument(filePath, doc);
|
||||
setDesignMdForPrompt(spec);
|
||||
|
||||
return { success: true, spec };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@ const SECTION_MAP: Record<PromptSection, () => string> = {
|
|||
guidelines: () => DESIGN_GUIDELINES,
|
||||
planning: () => PLANNING_GUIDE,
|
||||
elements: () => getSkillContent('elements'),
|
||||
'design-md': () => _designMdContent ?? 'No design.md loaded in the current document.',
|
||||
'design-md': () => 'No design.md loaded in the current document.',
|
||||
copywriting: () => getSkillContent('copywriting'),
|
||||
overflow: () => getSkillContent('overflow'),
|
||||
cjk: () => getSkillContent('cjk'),
|
||||
|
|
@ -285,17 +285,13 @@ const SECTION_MAP: Record<PromptSection, () => string> = {
|
|||
'codegen-react-native': () => getSkillContent('codegen-react-native'),
|
||||
};
|
||||
|
||||
// Design.md content injected via setDesignMdForPrompt()
|
||||
let _designMdContent: string | null = null;
|
||||
|
||||
/** Set the design.md content to be returned by the 'design-md' section. */
|
||||
export function setDesignMdForPrompt(spec: DesignMdSpec | undefined): void {
|
||||
_designMdContent = spec ? buildDesignMdStylePolicy(spec) : null;
|
||||
}
|
||||
|
||||
/** Get the design.md style policy, or null if not loaded. */
|
||||
export function getDesignMdForPrompt(): string | null {
|
||||
return _designMdContent;
|
||||
/**
|
||||
* Derive the design.md style policy string for a spec, or null if no spec.
|
||||
* Stateless — callers thread the result through `buildDesignPrompt` so
|
||||
* there is no process-level global that could leak between documents.
|
||||
*/
|
||||
export function designMdSpecToPromptPolicy(spec: DesignMdSpec | null | undefined): string | null {
|
||||
return spec ? buildDesignMdStylePolicy(spec) : null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -308,12 +304,21 @@ export function getDesignMdForPrompt(): string | null {
|
|||
* When `section` is provided, returns only that focused subset of design
|
||||
* knowledge. This allows external LLMs to load context incrementally
|
||||
* instead of consuming the full prompt at once.
|
||||
*
|
||||
* `designMdPolicy` (caller-supplied, derived from the active document's
|
||||
* `doc.designMd`) replaces the generic "style" section content and fills
|
||||
* the "design-md" section. Pass null when the caller's document has no
|
||||
* design.md — the prompt falls back to the generic style rules.
|
||||
*/
|
||||
export function buildDesignPrompt(section?: string): string {
|
||||
export function buildDesignPrompt(section?: string, designMdPolicy?: string | null): string {
|
||||
if (section) {
|
||||
// When design-md is loaded, 'style' section returns it instead of default
|
||||
if (section === 'style' && _designMdContent) {
|
||||
return `DESIGN SYSTEM (from design.md):\n${_designMdContent}`;
|
||||
if (designMdPolicy) {
|
||||
if (section === 'style') {
|
||||
return `DESIGN SYSTEM (from design.md):\n${designMdPolicy}`;
|
||||
}
|
||||
if (section === 'design-md') {
|
||||
return designMdPolicy;
|
||||
}
|
||||
}
|
||||
if (section in SECTION_MAP) {
|
||||
return SECTION_MAP[section as PromptSection]();
|
||||
|
|
|
|||
Loading…
Reference in a new issue