pbakaus/impeccable · error

font-match: no spec at {spec_path}; run comp-spec.mjs first

Error message

font-match: no spec at {spec_path}; run comp-spec.mjs first

What it means

`font-match.mjs` operates on the JSON spec produced by `comp-spec`. Before doing any region work it mutably loads the spec from the resolved `--spec` path (default SPEC_PATH); if the file is missing or empty (`spec.as_mut()` is None), it emits this message and exits 1, explicitly telling you to run `comp-spec` first. It is an ordering/dependency guard between the two verbs.

Source

Thrown at crates/comp-verbs/src/font_match.rs:434

    }
    let _ = std::fs::write(out, util::json_pretty(spec));
}

// ---- CLI -------------------------------------------------------------------

/// `impeccable font-match --measure <id> | --rank <id> ...`
pub fn run(argv: &[String], io: &mut Io, renderer: &mut dyn FontRenderer) -> i32 {
    let spec_path = arg_or(argv, "spec", SPEC_PATH).to_string();
    let mut spec = load_spec(&resolve(io, &spec_path));
    let measure_id = arg(argv, "measure");
    let rank_id = arg(argv, "rank");
    let id = measure_id.or(rank_id);
    let Some(id) = id else {
        io.err("usage: font-match.mjs --measure <text-region-id> | --rank <text-region-id> [--candidates \"Family:700,Family2:400,...\"] [--text \"...\"] [--transform uppercase] [--category sans,serif,display,handwriting,mono]\n");
        return 1;
    };
    let Some(spec_val) = spec.as_mut() else {
        io.err(&format!("font-match: no spec at {spec_path}; run comp-spec.mjs first\n"));
        return 1;
    };
    let region = spec_val
        .get("regions")
        .and_then(Value::as_array)
        .and_then(|a| a.iter().find(|r| r.get("id").and_then(Value::as_str) == Some(id)))
        .cloned();
    let Some(region) = region else {
        let ids = spec_val
            .get("regions")
            .and_then(Value::as_array)
            .map(|a| a.iter().filter_map(|r| r.get("id").and_then(Value::as_str)).collect::<Vec<_>>().join(", "))
            .unwrap_or_default();
        io.err(&format!("font-match: no region {id}; ids: {ids}\n"));
        return 1;
    };
    let comp_file = spec_val.get("comp").and_then(Value::as_str).unwrap_or("").to_string();
    let comp = match png_io::load_raster(&resolve(io, &comp_file)) {

View on GitHub (pinned to 2bc2879276)

Solutions

  1. Run `comp-spec` (with `--grid`/`--regions` or `--auto`) first to generate the spec file.
  2. Verify the spec path: pass `--spec <path>` explicitly or run from the directory containing the default spec file.
  3. Check the spec file exists and is non-empty valid JSON (`cat <spec-path>`).
  4. If comp-spec previously failed, fix its error (e.g. bad --regions input) so the spec actually gets written.

Example fix

// before
node font-match.mjs --measure hero-title
// font-match: no spec at comp-spec.json; run comp-spec.mjs first
// after
node comp-spec.mjs --comp shot.png --auto
node font-match.mjs --measure hero-title
Defensive patterns

Strategy: validation

Validate before calling

import { existsSync, statSync } from 'node:fs';
const specPath = specArg ?? 'comp-spec.json';
if (!existsSync(specPath) || statSync(specPath).size === 0) {
  throw new Error(`${specPath} missing or empty — run comp-spec first`);
}

Type guard

function hasSpec(path) {
  try { return existsSync(path) && statSync(path).size > 0; } catch { return false; }
}

Try / catch

try {
  runFontMatch(args);
} catch (e) {
  if (String(e.stderr).includes('no spec at')) {
    runCompSpec();       // generate spec, then retry once
    runFontMatch(args);
  } else throw e;
}

Prevention

When it happens

Trigger: Running `font-match.mjs --measure <id>` before `comp-spec` has ever been run in the directory; the spec file was deleted or moved; `--spec` points at a wrong path; the spec file exists but contains no JSON object (e.g. empty or `null`).

Common situations: Fresh checkout or new project folder where only font-match is invoked; a clean-build/clean script removed generated spec files; running font-match from a different working directory so the default relative spec path resolves elsewhere; a previous comp-spec run failed (see error 50) so the spec was never written.

Understand the failure class

Background: "Config file not found": what it means and how to fix it in docker-sync, Maven, Vagrant, Turborepo and other tools — this error's family across 60 libraries.

Related errors


AI-assisted analysis of pbakaus/impeccable@2bc2879276 (2026-09-08). Data as JSON: /api/errors/a9bfee616aace6c8. Report an issue: GitHub.