review.fixIds.plan
Plan automatic fixes for empty or duplicate IDs in all Re:VIEW manuscript files to ensure proper document structure and prevent common validation errors.
Instructions
Plan auto-fixes for empty/duplicate IDs across all .re files.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| cwd | Yes |
Implementation Reference
- src/index.ts:488-507 (handler)Main handler for the 'review.fixIds.plan' tool. It retrieves the list of .re files from catalog.yml, gathers all existing IDs used across files, plans fixes for empty or duplicate IDs in target blocks and captions for each file, and returns a JSON response with the count and list of planned fixes.
case "review.fixIds.plan": { const files = await pickFilesFromCatalog(args.cwd as string); const used = await gatherUsedIds(args.cwd as string, files); const fixes: any[] = []; for (const f of files) { const p = path.join(args.cwd as string, f); const txt = await fs.readFile(p, "utf-8"); const prefix = slugifyBase(f); const plan = planFixIdsForFile(f, txt, used, prefix); fixes.push(...plan); } return { content: [ { type: "text", text: JSON.stringify({ count: fixes.length, fixes }) } ] }; } - src/index.ts:284-290 (schema)Input schema definition for the tool, specifying the required 'cwd' parameter.
name: "review.fixIds.plan", description: "Plan auto-fixes for empty/duplicate IDs across all .re files.", inputSchema: { type: "object", properties: { cwd: { type: "string" } }, required: ["cwd"] } - src/index.ts:144-216 (helper)Helper function that processes a single file to plan ID fixes for target blocks (list, emlist, etc.) and captions. Detects empty or duplicate IDs, generates unique prefixed IDs, and creates before/after snippets for fixes.
function planFixIdsForFile(file: string, text: string, usedIds: Set<string>, prefixBase: string) { const fixes: any[] = []; const lines = text.split(/\r?\n/); for (let i=0;i<lines.length;i++) { const m = lines[i].match(RE_BLOCK_OPEN); if (!m) continue; const name = m[1]; const bracket = m[2] ?? ""; if (!isIdTargetBlock(name)) continue; let idVal: string | null = null; if (bracket) { const b = bracket.match(RE_BRACKET); if (b) { const attrs = b[1]; const kv = attrs.match(RE_ID_KV); idVal = kv ? kv[2].trim() : null; } } const mkId = (base: string) => { let n = 1, cand = `${base}-${String(n).padStart(3,"0")}`; while (usedIds.has(cand)) { n++; cand = `${base}-${String(n).padStart(3,"0")}`; } usedIds.add(cand); return cand; }; if (!idVal || idVal === "") { const cand = mkId(`${prefixBase}-${name}`); const before = lines[i]; let after: string; if (bracket) { after = before.replace(RE_BRACKET, (_all, inner) => { const sep = String(inner).trim().length ? `${inner}, id=${cand}` : `id=${cand}`; return `[${sep}]`; }); } else { after = before.replace(/^\/\/([A-Za-z0-9_]+)/, `//$1[id=${cand}]`); } fixes.push({ file, lineStart: i+1, lineEnd: i+1, before, after, reason: "empty" }); } else if (usedIds.has(idVal)) { const cand = mkId(`${prefixBase}-${name}`); const before = lines[i]; const after = before.replace(RE_ID_KV, (_a, q) => `${q ? `id=${q}${cand}${q}` : `id=${cand}`}`); fixes.push({ file, lineStart: i+1, lineEnd: i+1, before, after, reason: "duplicate" }); } else { usedIds.add(idVal); } } // captions for (let i=0;i<lines.length;i++) { const m = lines[i].match(RE_CAPTION_ID); if (!m) continue; const id = (m[1] || "").trim(); const mkId = (base: string) => { let n = 1, cand = `${base}-${String(n).padStart(3,"0")}`; while (usedIds.has(cand)) { n++; cand = `${base}-${String(n).padStart(3,"0")}`; } usedIds.add(cand); return cand; }; if (!id || usedIds.has(id)) { const cand = mkId(`${prefixBase}-cap`); const before = lines[i]; const after = before.replace(RE_CAPTION_ID, (_all) => `\\reviewlistcaption[${cand}]{`); fixes.push({ file, lineStart: i+1, lineEnd: i+1, before, after, reason: id ? "duplicate" : "empty" }); } else { usedIds.add(id); } } return fixes; } - src/index.ts:117-142 (helper)Helper function that scans all specified .re files to collect a Set of all currently used IDs from block attributes and captions.
async function gatherUsedIds(cwd: string, files: string[]) { const used = new Set<string>(); for (const f of files) { const txt = await fs.readFile(path.join(cwd, f), "utf-8"); // blocks for (const line of txt.split(/\r?\n/)) { const m = line.match(RE_BLOCK_OPEN); if (!m) continue; const bracket = m[2] ?? ""; if (bracket) { const b = bracket.match(RE_BRACKET); if (b) { const attrs = b[1]; const kv = attrs.match(RE_ID_KV); if (kv) used.add(kv[2].trim()); } } } // captions for (const m of txt.matchAll(RE_CAPTION_ID)) { const id = (m[1] || "").trim(); if (id) used.add(id); } } return used; } - src/index.ts:81-92 (helper)Helper function that parses catalog.yml to extract paths of .re files from PREDEF, CHAPS, and APPENDIX sections.
function pickFilesFromCatalog(cwd: string, catalogPath="catalog.yml"): Promise<string[]> { return fs.readFile(path.join(cwd, catalogPath), "utf-8") .then(txt => { const y = YAML.parse(txt); const sections = ["PREDEF","CHAPS","APPENDIX"]; const files: string[] = []; for (const sec of sections) { if (Array.isArray(y?.[sec])) files.push(...y[sec]); } return files; }); }