Skip to main content

Trace Campaign Asset Lineage from JSON in Node.js

Updated 5 min readMarkdown

A campaign can produce a square listing image, a social still, and a video opening from one approved pack shot. The video may inherit its starting frame from the social branch. A flat list of filenames hides that dependency. This Node.js example checks a small manifest and prints the chain each asset claims.

1. Give each asset a stable ID

Use one root for the approved source and record the immediate parent of every derivative. Assign an ID when the asset enters review. Keep that ID across file renames and use a new ID for a new revision that other assets may reference.

[
  {"id":"story-open","parent":"social-still"},
  {"id":"pack-source","parent":null},
  {"id":"listing-hero","parent":"pack-source"},
  {"id":"social-still","parent":"pack-source"}
]

Save the fixture as assets.json. It describes a fictional campaign. The story opening comes first in the file and its parent comes last. The manifest can preserve review order because the resolver indexes all IDs before tracing any branch.

Save the resolver as trace-assets.mjs. It expects lowercase IDs with digits and hyphens after the first letter. A root has an explicit null parent. Every other record names a parent by ID.

export function traceAssets(rows) {
  if (!Array.isArray(rows) || rows.length === 0) {
    throw new Error("Expected a nonempty array of asset records");
  }

  const byId = new Map();
  const validId = (id) => typeof id === "string" && /^[a-z][a-z0-9-]*$/.test(id);
  for (const row of rows) {
    if (!row || !validId(row.id) || (row.parent !== null && !validId(row.parent))) {
      throw new Error("Each record needs an ID and a parent ID or null");
    }
    if (byId.has(row.id)) throw new Error(`Duplicate ID: ${row.id}`);
    byId.set(row.id, row);
  }

  const roots = rows.filter((row) => row.parent === null);
  if (roots.length !== 1) throw new Error("Expected exactly one root");

  return rows.map(({ id }) => {
    const path = [];
    const seen = new Set();
    let current = id;
    while (current !== null) {
      if (seen.has(current)) throw new Error(`Cycle reached from ${id}: ${current}`);
      const row = byId.get(current);
      if (!row) throw new Error(`Missing parent while tracing ${id}: ${current}`);
      seen.add(current);
      path.push(current);
      current = row.parent;
    }
    return { id, lineage: path.reverse() };
  });
}

During a trace, the set tracks IDs seen on the current chain. A repeated ID signals a loop. A missing ID signals a reference the reviewer cannot resolve. Each successful path is reversed so the source appears first.

3. Print the chain at handoff

Save this wrapper as report.mjs and run node report.mjs assets.json from the same directory. It validates the entire manifest before printing any paths. Invalid JSON, unreadable input and structural errors use exit code 2.

import { readFileSync } from "node:fs";
import { traceAssets } from "./trace-assets.mjs";

try {
  const file = process.argv[2];
  if (!file) throw new Error("Usage: node report.mjs assets.json");
  const report = traceAssets(JSON.parse(readFileSync(file, "utf8")));
  for (const { lineage } of report) console.log(lineage.join(" -> "));
} catch (error) {
  console.error(error.message);
  process.exitCode = 2;
}

The first line is pack-source -> social-still -> story-open. The listing hero follows a separate path from pack-source. If the social still is retired, the report gives the reviewer a concrete starting point for checking the story opening.

Save the checks as test.mjs and run node test.mjs. The cases cover out-of-order rows, a root-only manifest, missing and duplicate IDs, bad root counts, and both self-loops and two-asset cycles.

import assert from "node:assert/strict";
import { traceAssets } from "./trace-assets.mjs";

const root = { id: "source", parent: null };
const child = { id: "still", parent: "source" };
const video = { id: "video", parent: "still" };
assert.deepEqual(traceAssets([video, root, child]), [
  { id: "video", lineage: ["source", "still", "video"] },
  { id: "source", lineage: ["source"] },
  { id: "still", lineage: ["source", "still"] },
]);
assert.deepEqual(traceAssets([root]), [{ id: "source", lineage: ["source"] }]);
assert.throws(() => traceAssets([]), /nonempty/);
assert.throws(() => traceAssets(null), /nonempty/);
assert.throws(() => traceAssets([root, root]), /Duplicate/);
assert.throws(() => traceAssets([root, { ...child, parent: "gone" }]), /Missing parent/);
assert.throws(() => traceAssets([root, { ...child, parent: "still" }]), /Cycle/);
assert.throws(() => traceAssets([root, { ...child, parent: "video" }, video]), /Cycle/);
assert.throws(() => traceAssets([child, video]), /one root/);
assert.throws(() => traceAssets([root, { id: "second", parent: null }]), /one root/);
assert.throws(() => traceAssets([root, { id: "still" }]), /parent ID/);
assert.throws(() => traceAssets([root, { id: "Still A", parent: "source" }]), /ID/);
console.log("12 checks passed");

5. Keep structure and approval separate

A complete chain means the declared links are resolvable. Verify that the referenced file is the approved revision and inspect the crop, label and product details for the destination. Keep those review decisions with the asset IDs so a later revision can be evaluated separately.

6. Choose the right level of detail

This example models a tree with one parent per asset. Use it for small campaigns with one approved source. When a composite uses two equally important inputs, a manifest that records both dependencies will preserve more of the review history. The time spent walking the tree is proportional to the total length of the paths printed.

The code and example were checked with synthetic records in Node.js 24.20.0. The example uses fictional IDs and describes file relationships only.

AI assistance was used in drafting and editing this article.

Source workflow. How to Turn One Product Photo Into a Full Ad Campaign

Technical reference. Node.js strict assertion mode

1 view

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