Skip to main content

Check Garment Listing Image Coverage by Variant in Node.js

Updated 5 min readMarkdown

A garment listing can have seven images and still be missing a reviewed scene for one colorway. An image count also hides cases where two files have both been marked as the main shot. This Node.js example reports coverage by variant and role so each unresolved slot can be sent back to the reviewer.

1. Write the listing plan

Use a stable variant ID for each colorway and three roles for this example. The hero shows the overall shape, the scene shows the garment in context, and the fabric detail records its texture. Each asset has an ID, a review state and an origin.

{
  "variants": ["sand", "navy"],
  "assets": [
    {"id":"sand-h1","variant":"sand","role":"hero","review":"approved","origin":"generated"},
    {"id":"sand-s1","variant":"sand","role":"scene","review":"pending","origin":"generated"},
    {"id":"sand-d1","variant":"sand","role":"fabric-detail","review":"approved","origin":"generated"},
    {"id":"navy-h1","variant":"navy","role":"hero","review":"approved","origin":"generated"},
    {"id":"navy-h2","variant":"navy","role":"hero","review":"approved","origin":"photo"},
    {"id":"navy-s1","variant":"navy","role":"scene","review":"approved","origin":"generated"},
    {"id":"navy-d1","variant":"navy","role":"fabric-detail","review":"approved","origin":"photo"}
  ]
}

Save the fictional fixture as manifest.json. The sand scene has a pending image. Its fabric slot has an approved generated image, but this listing uses photography for fabric evidence. Navy has two approved hero candidates.

2. Group assets by variant and role

Save the validator as coverage.mjs. A nested map creates a bucket for each planned variant and role before processing any assets. The validator rejects unknown slots and duplicate asset IDs so an incorrect label cannot disappear into an unused bucket.

const roles = ["hero", "scene", "fabric-detail"];

export function checkCoverage(variants, assets) {
  const validId = (value) => typeof value === "string" &&
    /^[a-z][a-z0-9-]*$/.test(value);

  if (!Array.isArray(variants) || !variants.length || !variants.every(validId) ||
      new Set(variants).size !== variants.length || !Array.isArray(assets)) {
    throw new Error("Expected unique variant IDs and an asset array");
  }

  const slots = new Map(variants.map((variant) => [
    variant, new Map(roles.map((role) => [role, []])),
  ]));
  const ids = new Set();
  for (const asset of assets) {
    if (!asset || !validId(asset.id) || ids.has(asset.id)) {
      throw new Error("Every asset needs a unique ID");
    }
    ids.add(asset.id);

    const matches = slots.get(asset.variant)?.get(asset.role);
    if (!matches) throw new Error(`Unknown slot for ${asset.id}`);
    if (!["approved", "pending", "rejected"].includes(asset.review) ||
        !["photo", "generated"].includes(asset.origin)) {
      throw new Error(`Invalid review or origin for ${asset.id}`);
    }
    matches.push(asset);
  }

  return variants.flatMap((variant) => roles.map((role) => {
    const candidates = slots.get(variant).get(role);
    const approved = candidates.filter((asset) => asset.review === "approved");
    const eligible = approved.filter((asset) =>
      role !== "fabric-detail" || asset.origin === "photo");
    return {
      variant, role,
      status: ["missing", "ready"][eligible.length] ?? "ambiguous",
      eligibleIds: eligible.map((asset) => asset.id),
      excludedApprovedIds: approved.filter((asset) =>
        role === "fabric-detail" && asset.origin === "generated").map((asset) => asset.id),
    };
  }));
}

Only approved candidates can fill a slot. This example excludes generated fabric details even if someone approved them for another purpose. One eligible candidate is ready, two or more are ambiguous, and zero is missing. The report keeps excluded approved IDs visible.

3. Run the coverage report

Save the wrapper as review.mjs and run node review.mjs manifest.json. It prints the report in variant and role order. Exit code 0 means every slot has one eligible candidate. Exit code 1 means review work remains. Exit code 2 means the input could not be read or validated.

import { readFileSync } from "node:fs";
import { checkCoverage } from "./coverage.mjs";

try {
  const file = process.argv[2];
  if (!file) throw new Error("Usage: node review.mjs manifest.json");
  const { variants, assets } = JSON.parse(readFileSync(file, "utf8"));
  const report = checkCoverage(variants, assets);
  for (const row of report) {
    const ids = row.eligibleIds.join(", ") || "none";
    console.log(`${row.variant} ${row.role} ${row.status} (${ids})`);
    if (row.excludedApprovedIds.length) {
      console.log(`  approved but ineligible: ${row.excludedApprovedIds.join(", ")}`);
    }
  }
  process.exitCode = report.every((row) => row.status === "ready") ? 0 : 1;
} catch (error) {
  console.error(error.message);
  process.exitCode = 2;
}

The sample has three ready slots, two missing slots and one ambiguous slot. To produce a complete report, resolve the navy hero choice, approve a suitable sand scene, and add an approved sand fabric photo. Use a new asset ID for the new photo.

4. Exercise the failure cases

Save the assertions as test.mjs and run node test.mjs. The cases check that a pending or rejected image cannot fill a slot, a generated detail is listed separately, and misspelled roles or variants fail validation.

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { checkCoverage } from "./coverage.mjs";

const { variants, assets } = JSON.parse(
  readFileSync(new URL("./manifest.json", import.meta.url), "utf8"),
);
const report = checkCoverage(variants, assets);
assert.deepEqual(report.map((row) => row.status), [
  "ready", "missing", "missing", "ambiguous", "ready", "ready",
]);
assert.deepEqual(report[2].excludedApprovedIds, ["sand-d1"]);
assert.deepEqual(report[3].eligibleIds, ["navy-h1", "navy-h2"]);
assert.equal(checkCoverage(["sand"], [])[0].status, "missing");
assert.deepEqual(checkCoverage(variants, [
  ...assets, { ...assets[0], id: "sand-h2", review: "rejected" },
])[0].eligibleIds, ["sand-h1"]);
assert.throws(() => checkCoverage(["sand", "sand"], []), /unique variant/);
assert.throws(() => checkCoverage([], []), /variant/);
assert.throws(() => checkCoverage(variants, [...assets, assets[0]]), /unique ID/);
assert.throws(() => checkCoverage(variants, [
  { ...assets[0], role: "alternate" },
]), /Unknown slot/);
assert.throws(() => checkCoverage(variants, [
  { ...assets[0], variant: "teal" },
]), /Unknown slot/);
assert.throws(() => checkCoverage(variants, [
  { ...assets[0], review: "yes" },
]), /Invalid review/);
assert.throws(() => checkCoverage(variants, [
  { ...assets[0], origin: "unknown" },
]), /Invalid review or origin/);
console.log("12 checks passed");

5. Give reviewers the asset IDs

Use the printed IDs to find the files in your asset system. Compare a candidate with the source garment before approving it. For the ambiguous hero, record which revision should occupy the slot and move the other candidate out of the approved state for that role.

6. Match the policy to your catalog

The three roles and the fabric-photo requirement are decisions for this example. Adjust the plan when your catalog uses different evidence or roles. If you need an audit trail, export the review decisions alongside the manifest with timestamps and revision IDs.

The code was checked in Node.js 24.20.0 with synthetic manifests. The test covers coverage and input validation. Garment image generation and visual review were outside the test.

AI assistance was used in drafting and editing this article.

Source workflow. How Clothing Sellers Can Use AI Virtual Try-On for New-Arrival Photos

Technical reference. Node.js strict assertion mode

About the author

  • The PixMind team shares practical AI image and video workflows. PixMind brings text-to-image, reference-based creation, editing, and video tools into one workspace. Website: https://www.pixmind.io/

No comments yet. Be the first to comment.

More from this blog