When an image goes through several small wording changes, a folder full of similarly named exports quickly becomes difficult to review. Which candidate came from the untouched source? Which one was based on an earlier edit? Did someone accidentally describe a revision as its own parent? Those questions are about the revision record, not the pixels. A small JavaScript check can identify some metadata inconsistencies without pretending to approve the image itself.
This is a project-associated, AI-assisted technical article. The complete module and tests below were executed locally with twelve passing Node test cases. The examples are synthetic. The code does not read image files, perform OCR, detect manipulation, assess permissions, or establish that a screenshot is authentic. A clean result only means the supplied revision records pass this particular consistency contract.
Model a revision as an explicit relationship
Each record has an identifier, a parent identifier, and a note. A null parent identifies an independent root. A candidate points to the record it was derived from rather than merely repeating a filename. This makes branches possible: two alternative wording choices can both point to the same source. A later review can then distinguish alternatives without assuming that the highest revision number is automatically the approved version.
The identifier is intentionally restricted to a short lowercase string containing letters, digits, underscores, or hyphens. It is a record key, not a filesystem path or an external URL. The note explains the role of the record in ordinary language. Neither field proves that a corresponding file exists. Your surrounding workflow still needs to associate a record with the correct artifact and preserve the original rather than silently overwriting it.
Choose a small contract before writing the check
The function rejects a non-array input and bounds the list to one thousand records. It reports malformed rows, invalid or duplicate identifiers, missing parent references, blank notes, and cycles. Multiple independent roots are allowed because a review packet may contain several authorized examples. This is not intended as a universal asset management schema; it is a deliberately narrow starting point that can be understood in one sitting.
Duplicate identifiers deserve special treatment. The first occurrence is kept in the lookup map, and later occurrences are reported and ignored for relationship traversal. That choice avoids silently replacing an earlier record with a later one. A caller should treat any returned issue as a reason to inspect the packet, not as an invitation to automatically repair the data. Guessing which duplicate was intended can destroy the very history the record is supposed to preserve.
Detect cycles without approving the content
For every indexed record, the checker walks through parent references until it reaches a root or an unknown key. A small set tracks the keys visited during that walk. Revisiting a key indicates a cycle, including the simple case of a record naming itself as its parent. The approach favors clarity over cleverness and is reasonable for the bounded list used in this demonstration.
The straightforward traversal can repeat work across records, so its worst case is quadratic in the number of records. The one-thousand-record limit makes that cost explicit, but it is not a promise about every deployment environment. If your workflow grows substantially, consider memoized traversal or a dedicated graph representation. Measure the actual workload instead of assuming this teaching example is an optimized production service.
Keep the function pure and its output modest
The implementation creates its own map and sets without modifying the input array or its records. One test verifies that behavior by comparing a structured clone before and after inspection. Preserving input matters when the same record packet is later displayed to a reviewer or saved as evidence of what was submitted. An inspection helper should not quietly rewrite the history it was asked to inspect.
Issue objects contain an index or identifier and a short code. They do not echo the entire note, which may contain context that a caller does not want in a generic diagnostic log. A production application still needs its own logging and privacy policy. This example demonstrates a restrained output shape; it does not establish that every possible use of the function is safe or that the supplied notes contain no sensitive information.
The complete module
The first file exports only the inspection function. It has no network requests, filesystem operations, browser automation, or package dependencies. A successful return does not grant permission to publish an image, and an error does not show that an image is malicious. The output concerns only the consistency of the metadata relationships represented by this function.
// Metadata consistency only. Does not read files or approve image edits.
export function inspectChain(records) {
if (!Array.isArray(records)) throw new TypeError('records must be an array');
if (records.length > 1000) throw new RangeError('at most 1000 records');
const issues = [];
const byId = new Map();
for (const [index, row] of records.entries()) {
if (!row || typeof row !== 'object' || Array.isArray(row)) {
issues.push({ index, code: 'invalid_record' });
continue;
}
if (typeof row.id !== 'string' || !/^[a-z0-9_-]{1,64}$/.test(row.id)) {
issues.push({ index, code: 'invalid_id' });
continue;
}
if (byId.has(row.id)) {
issues.push({ index, code: 'duplicate_id' });
continue;
}
byId.set(row.id, row);
}
for (const [id, row] of byId) {
if (row.parent !== null && (typeof row.parent !== 'string' || !byId.has(row.parent))) {
issues.push({ id, code: 'missing_parent' });
}
if (typeof row.note !== 'string' || !row.note.trim()) {
issues.push({ id, code: 'missing_note' });
}
const seen = new Set();
let current = id;
while (byId.has(current)) {
if (seen.has(current)) {
issues.push({ id, code: 'cycle' });
break;
}
seen.add(current);
const parent = byId.get(current).parent;
if (parent === null) break;
current = parent;
}
}
return issues;
}
Test the ordinary cases and the awkward edges
The tests include a root, a branch, duplicate keys, a missing parent, self and two-node cycles, an empty note, an invalid identifier, malformed rows, non-array input, an oversized list, and multiple roots. There is also an explicit non-mutation check. These cases describe the contract more clearly than a single example that happens to pass. They make it easier to see which outcomes are intentional and which are not implemented.
Run the second file with node --test revision-chain.test.mjs from the directory containing both files. The local run used for this article passed all twelve test cases with zero failures. That is evidence for these synthetic cases, not a security audit or an exhaustive proof. Add regression tests when you change the schema or discover a meaningful edge case in your actual workflow.
Connect the record to a manual screenshot review
Suppose a team needs to edit screenshot wording in an instructional graphic after the editable source is lost. Preserve an untouched source, record the exact replacement phrase, and give each generated candidate its own revision record. If the actual application or source design is available, a fresh export may be the better choice. The revision graph should support that decision rather than force every task through pixel editing.
AI Image Text Editor is one browser resource for the image step: https://image-text-editor.online/ . Its public homepage describes JPG, PNG and WebP inputs up to ten megabytes, selected text regions, and up to eight selections in one task. New users can start with free credits; edits consume credits. The service uses nearby lettering and background as references, so spelling, alignment, and surrounding detail still need review. This module is not integrated with that service and does not benchmark its output.
Separate consistency, correctness, and authorization
A revision packet can be perfectly consistent while every image in it contains the wrong wording. A reviewer still needs to compare the actual candidate with the approved request and inspect nearby details. They should check the result at its final display size, where resizing or compression may reveal a readability problem that was not obvious in the editor. A polished preview is not evidence that a label is factually correct.
Permission is a separate question again. Use authorized or synthetic examples, avoid unnecessary sensitive uploads, and clearly label altered screenshots when readers could mistake them for original records. Visual text replacement is not secure redaction; metadata, filenames, surrounding context, and older copies can still reveal information. The modest benefit of this helper is organizational: it can make a review packet easier to understand while leaving the actual content and publishing decisions where they belong.
Complete test file
import test from 'node:test';
import assert from 'node:assert/strict';
import { inspectChain } from './revision-chain.mjs';
const root = () => ({ id: 'source', parent: null, note: 'Untouched synthetic source' });
const codes = rows => inspectChain(rows).map(x => x.code);
test('one root is valid metadata', () => assert.deepEqual(inspectChain([root()]), []));
test('a branch preserves one source', () => assert.deepEqual(inspectChain([root(),
{ id: 'candidate_1', parent: 'source', note: 'First label correction' },
{ id: 'candidate_2', parent: 'source', note: 'Alternative wording' }]), []));
test('input is not mutated', () => {
const data = [root()]; const copy = structuredClone(data); inspectChain(data); assert.deepEqual(data, copy);
});
test('duplicate identifiers are reported', () => assert.ok(codes([root(), root()]).includes('duplicate_id')));
test('missing parent is reported', () => assert.ok(codes([{ id: 'edit', parent: 'lost', note: 'Test' }]).includes('missing_parent')));
test('self cycle is reported', () => assert.ok(codes([{ id: 'edit', parent: 'edit', note: 'Test' }]).includes('cycle')));
test('two-node cycle is reported', () => assert.ok(codes([
{ id: 'a', parent: 'b', note: 'A' }, { id: 'b', parent: 'a', note: 'B' }]).includes('cycle')));
test('blank note is reported', () => assert.ok(codes([{ ...root(), note: ' ' }]).includes('missing_note')));
test('invalid id is reported', () => assert.ok(codes([{ ...root(), id: '../source' }]).includes('invalid_id')));
test('nonobject rows are reported', () => assert.ok(codes([null, []]).every(x => x === 'invalid_record')));
test('non-array input and oversized lists throw', () => {
assert.throws(() => inspectChain({}), TypeError); assert.throws(() => inspectChain(Array(1001)), RangeError);
});
test('multiple independent roots are allowed', () => assert.deepEqual(inspectChain([root(),
{ id: 'second', parent: null, note: 'Another authorized example' }]), []));
Top comments (0)