Skip to main content
Glama

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
NameRequiredDescriptionDefault
cwdYes

Implementation Reference

  • 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 }) 
          }
        ]
      };
    }
  • 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"] 
    }
  • 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;
    }
  • 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;
    }
  • 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;
        });
    }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dsgarage/ReviewMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server