783a267 feat: 实现 schema 分离并生成扁平 JSON 字典
 scripts/bundle_for_extension.js   | 142 ++++++++++++++++++++++++++------------
 test/bundle_for_extension.test.js | 106 ++++++++++++++++++++++++++++
 2 files changed, 203 insertions(+), 45 deletions(-)
diff --git a/scripts/bundle_for_extension.js b/scripts/bundle_for_extension.js
index 1212bf0..fd6cd4a 100644
--- a/scripts/bundle_for_extension.js
+++ b/scripts/bundle_for_extension.js
@@ -1,53 +1,105 @@
 const fs = require('fs');
 const path = require('path');
+const crypto = require('crypto');
+const matter = require('gray-matter');
 
-const commandsDir = path.join(__dirname, '../data/commands');
-const outputDir = path.join(__dirname, '../src/generated');
-
-if (!fs.existsSync(outputDir)) {
-    fs.mkdirSync(outputDir, { recursive: true });
+function getMd5(content) {
+    return crypto.createHash('md5').update(content, 'utf8').digest('hex');
 }
 
-// 准备生成的 TS 文件头
-let tsContent = `// ⚠️ 此文件由打包脚本自动生成，请勿直接修改。\n`;
-tsContent += `import * as vscode from 'vscode';\n\n`;
-
-let cmdItems = `export const vivadoCommands: vscode.CompletionItem[] = [\n`;
-let argDict = `export const vivadoArgs: Record<string, vscode.CompletionItem[]> = {\n`;
-
-// 遍历所有的单文件指令
-const files = fs.readdirSync(commandsDir).filter(f => f.endsWith('.json'));
-
-files.forEach(file => {
-    const filePath = path.join(commandsDir, file);
-    const cmdData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
-    
-    // 如果存在人工覆写的 snippet，就用人工的，否则用默认的
-    const insertText = cmdData.custom_snippet ? 
-        `new vscode.SnippetString(\`${cmdData.custom_snippet}\`)` : 
-        `new vscode.SnippetString('${cmdData.name} $0')`;
-
-    // 组装顶级命令
-    cmdItems += `    {\n`;
-    cmdItems += `        label: '${cmdData.name}',\n`;
-    cmdItems += `        kind: vscode.CompletionItemKind.Function,\n`;
-    cmdItems += `        detail: ${JSON.stringify(cmdData.short_description)},\n`;
-    cmdItems += `        insertText: ${insertText},\n`;
-    cmdItems += `        documentation: new vscode.MarkdownString(${JSON.stringify(cmdData.full_help)})\n`;
-    cmdItems += `    },\n`;
-
-    // 组装参数
-    if (cmdData.arguments && cmdData.arguments.length > 0) {
-        argDict += `    "${cmdData.name}": [\n`;
-        cmdData.arguments.forEach(arg => {
-            argDict += `        { label: '${arg.flag}', detail: ${JSON.stringify(arg.description)}, kind: vscode.CompletionItemKind.EnumMember },\n`;
-        });
-        argDict += `    ],\n`;
+function bundleExtensionData(
+    commandsDir = path.join(__dirname, '..', 'data', 'commands'),
+    generatedDir = path.join(__dirname, '..', 'src', 'generated'),
+    distDir = path.join(__dirname, '..', 'dist'),
+    reportPath = path.join(__dirname, '..', 'data', 'outdated_translations_report.md')
+) {
+    if (!fs.existsSync(generatedDir)) fs.mkdirSync(generatedDir, { recursive: true });
+    if (!fs.existsSync(distDir)) fs.mkdirSync(distDir, { recursive: true });
+
+    const schema = [];
+    const dictEn = {};
+    const dictZh = {};
+    const outdatedReport = [];
+
+    const dirs = fs.readdirSync(commandsDir);
+    for (const dir of dirs) {
+        const cmdDir = path.join(commandsDir, dir);
+        if (!fs.statSync(cmdDir).isDirectory()) continue;
+        
+        const enMdPath = path.join(cmdDir, 'en.md');
+        const zhMdPath = path.join(cmdDir, 'zh-cn.md');
+        
+        if (!fs.existsSync(enMdPath)) continue;
+        
+        const enFile = matter(fs.readFileSync(enMdPath, 'utf8'));
+        const enHash = getMd5(fs.readFileSync(enMdPath, 'utf8')); // Simple file hash for tracking
+        
+        // Add to schema
+        const cmdSchema = {
+            name: dir,
+            versions: enFile.data.versions || [],
+            arguments: (enFile.data.arguments || []).map(a => a.flag)
+        };
+        schema.push(cmdSchema);
+        
+        // Populate English dict
+        dictEn[`${dir}.short_description`] = enFile.data.short_description || "";
+        dictEn[`${dir}.full_help`] = enFile.content.trim();
+        if (enFile.data.arguments) {
+            enFile.data.arguments.forEach(arg => {
+                dictEn[`${dir}.args.${arg.flag}`] = arg.description;
+            });
+        }
+        
+        // Populate Chinese dict (with fallback logic)
+        let zhOutdated = false;
+        if (fs.existsSync(zhMdPath)) {
+            const zhFile = matter(fs.readFileSync(zhMdPath, 'utf8'));
+            if (zhFile.data._source_hash !== enHash) {
+                zhOutdated = true;
+                outdatedReport.push(`- **${dir}**: Hash mismatch (Source updated)`);
+            } else {
+                dictZh[`${dir}.short_description`] = zhFile.data.short_description || dictEn[`${dir}.short_description`];
+                dictZh[`${dir}.full_help`] = zhFile.content.trim() || dictEn[`${dir}.full_help`];
+                if (enFile.data.arguments) {
+                    enFile.data.arguments.forEach(arg => {
+                        const zhArg = zhFile.data.arguments?.find(a => a.flag === arg.flag);
+                        dictZh[`${dir}.args.${arg.flag}`] = zhArg ? zhArg.description : arg.description;
+                    });
+                }
+            }
+        } else {
+            outdatedReport.push(`- **${dir}**: Missing translation`);
+            zhOutdated = true;
+        }
+        
+        // Fallback if missing or outdated
+        if (zhOutdated) {
+            dictZh[`${dir}.short_description`] = dictEn[`${dir}.short_description`];
+            dictZh[`${dir}.full_help`] = dictEn[`${dir}.full_help`];
+            if (enFile.data.arguments) {
+                enFile.data.arguments.forEach(arg => {
+                    dictZh[`${dir}.args.${arg.flag}`] = arg.description;
+                });
+            }
+        }
     }
-});
 
-cmdItems += `];\n\n`;
-argDict += `};\n`;
+    fs.writeFileSync(path.join(generatedDir, 'vivadoSchema.ts'), `export const vivadoSchema = ${JSON.stringify(schema, null, 2)};\n`);
+    fs.writeFileSync(path.join(distDir, 'strings.en.json'), JSON.stringify(dictEn, null, 2));
+    fs.writeFileSync(path.join(distDir, 'strings.zh-cn.json'), JSON.stringify(dictZh, null, 2));
+
+    const reportDir = path.dirname(reportPath);
+    if (!fs.existsSync(reportDir)) fs.mkdirSync(reportDir, { recursive: true });
+    fs.writeFileSync(reportPath, `# Translation Report\n\n${outdatedReport.join('\n')}\n`);
+    console.log('Build completed.');
+}
+
+if (require.main === module) {
+    bundleExtensionData();
+}
 
-fs.writeFileSync(path.join(outputDir, 'vivadoCommands.ts'), tsContent + cmdItems + argDict);
-console.log(`✅ 成功将 ${files.length} 个指令打包为 src/generated/vivadoCommands.ts`);
\ No newline at end of file
+module.exports = {
+    getMd5,
+    bundleExtensionData
+};
\ No newline at end of file
diff --git a/test/bundle_for_extension.test.js b/test/bundle_for_extension.test.js
new file mode 100644
index 0000000..b52622e
--- /dev/null
+++ b/test/bundle_for_extension.test.js
@@ -0,0 +1,106 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('fs');
+const path = require('path');
+const os = require('os');
+const matter = require('gray-matter');
+
+const { bundleExtensionData, getMd5 } = require('../scripts/bundle_for_extension.js');
+
+test('bundleExtensionData generates schema, flat dictionaries, and outdated report correctly', () => {
+    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vivado-bundle-test-'));
+    const commandsDir = path.join(tmpDir, 'data', 'commands');
+    const generatedDir = path.join(tmpDir, 'src', 'generated');
+    const distDir = path.join(tmpDir, 'dist');
+    const reportPath = path.join(tmpDir, 'data', 'outdated_translations_report.md');
+
+    // Create test command 1 (matching hash for zh-cn)
+    const cmd1Dir = path.join(commandsDir, 'cmd_one');
+    fs.mkdirSync(cmd1Dir, { recursive: true });
+
+    const cmd1EnContent = matter.stringify("Full help for cmd_one", {
+        versions: ["2024.1", "2024.2"],
+        short_description: "Cmd one short desc",
+        arguments: [
+            { flag: "-opt1", description: "Option 1 description" },
+            { flag: "-opt2", description: "Option 2 description" }
+        ]
+    });
+    fs.writeFileSync(path.join(cmd1Dir, 'en.md'), cmd1EnContent, 'utf8');
+
+    const cmd1Hash = getMd5(fs.readFileSync(path.join(cmd1Dir, 'en.md'), 'utf8'));
+    const cmd1ZhContent = matter.stringify("cmd_one 详细帮助", {
+        _source_hash: cmd1Hash,
+        short_description: "cmd_one 简短描述",
+        arguments: [
+            { flag: "-opt1", description: "选项 1 描述" }
+            // opt2 description missing, should fallback to en for opt2
+        ]
+    });
+    fs.writeFileSync(path.join(cmd1Dir, 'zh-cn.md'), cmd1ZhContent, 'utf8');
+
+    // Create test command 2 (missing zh-cn)
+    const cmd2Dir = path.join(commandsDir, 'cmd_two');
+    fs.mkdirSync(cmd2Dir, { recursive: true });
+
+    const cmd2EnContent = matter.stringify("Full help for cmd_two", {
+        versions: ["2024.2"],
+        short_description: "Cmd two short desc",
+        arguments: [{ flag: "-flagA", description: "Flag A desc" }]
+    });
+    fs.writeFileSync(path.join(cmd2Dir, 'en.md'), cmd2EnContent, 'utf8');
+
+    // Create test command 3 (outdated zh-cn hash mismatch)
+    const cmd3Dir = path.join(commandsDir, 'cmd_three');
+    fs.mkdirSync(cmd3Dir, { recursive: true });
+
+    const cmd3EnContent = matter.stringify("Full help for cmd_three", {
+        versions: ["2024.2"],
+        short_description: "Cmd three short desc"
+    });
+    fs.writeFileSync(path.join(cmd3Dir, 'en.md'), cmd3EnContent, 'utf8');
+
+    const cmd3ZhContent = matter.stringify("cmd_three 旧帮助", {
+        _source_hash: "outdated_hash_12345",
+        short_description: "cmd_three 旧描述"
+    });
+    fs.writeFileSync(path.join(cmd3Dir, 'zh-cn.md'), cmd3ZhContent, 'utf8');
+
+    // Run bundle
+    bundleExtensionData(commandsDir, generatedDir, distDir, reportPath);
+
+    // Verify vivadoSchema.ts
+    const schemaTs = fs.readFileSync(path.join(generatedDir, 'vivadoSchema.ts'), 'utf8');
+    assert.match(schemaTs, /export const vivadoSchema = /);
+    const schemaJson = JSON.parse(schemaTs.replace('export const vivadoSchema = ', '').replace(/;\s*$/, ''));
+    
+    assert.equal(schemaJson.length, 3);
+    const cmd1Schema = schemaJson.find(c => c.name === 'cmd_one');
+    assert.deepEqual(cmd1Schema.versions, ["2024.1", "2024.2"]);
+    assert.deepEqual(cmd1Schema.arguments, ["-opt1", "-opt2"]);
+
+    // Verify strings.en.json
+    const dictEn = JSON.parse(fs.readFileSync(path.join(distDir, 'strings.en.json'), 'utf8'));
+    assert.equal(dictEn['cmd_one.short_description'], "Cmd one short desc");
+    assert.equal(dictEn['cmd_one.full_help'], "Full help for cmd_one");
+    assert.equal(dictEn['cmd_one.args.-opt1'], "Option 1 description");
+    assert.equal(dictEn['cmd_one.args.-opt2'], "Option 2 description");
+
+    // Verify strings.zh-cn.json
+    const dictZh = JSON.parse(fs.readFileSync(path.join(distDir, 'strings.zh-cn.json'), 'utf8'));
+    assert.equal(dictZh['cmd_one.short_description'], "cmd_one 简短描述");
+    assert.equal(dictZh['cmd_one.full_help'], "cmd_one 详细帮助");
+    assert.equal(dictZh['cmd_one.args.-opt1'], "选项 1 描述");
+    assert.equal(dictZh['cmd_one.args.-opt2'], "Option 2 description"); // fallbacked
+
+    // Fallbacks for missing/outdated zh-cn
+    assert.equal(dictZh['cmd_two.short_description'], "Cmd two short desc");
+    assert.equal(dictZh['cmd_three.short_description'], "Cmd three short desc");
+
+    // Verify outdated_translations_report.md
+    const report = fs.readFileSync(reportPath, 'utf8');
+    assert.match(report, /Missing translation/);
+    assert.match(report, /Hash mismatch/);
+
+    fs.rmSync(tmpDir, { recursive: true, force: true });
+});
