A relationship editor can have a beautiful canvas and still give reviewers a poor way to find unresolved claims. A line may have no source note, an imported assertion may never have been reviewed, or a person may have explicitly reported a conflict. None of those states is resolved by making the diagram look more complete. A small review queue can make the work visible without trying to decide which family account is true.
This tutorial implements a narrow metadata transformation in JavaScript and tests it with Node's built-in test runner. All identifiers and note labels are synthetic. The actual local run passed the eight fixtures included below. That result concerns this small function, not an external product, a complete application's security, or the accuracy of any family-history information.
The article is prepared on behalf of the Genogram Maker project with AI assistance. The code is an independent teaching example, not an official API client or a description of the project's internal schema. No production family records or hosted AI requests are used. Relationship diagrams and queue results do not diagnose conditions, prove biological connections, or replace qualified professional interpretation.
Start with a modest interface contract
Each input claim has a stable identifier, an array of evidence labels, a boolean human-review flag, and an optional boolean conflict flag. A label points to a separate note rather than containing its text. The function returns only the identifier and a list of reason codes for claims that need attention. This gives a user interface enough information to locate a working record without putting narrative details into the queue result.
The contract deliberately says nothing about a graph's actual family structure. There are no parent-count rules, inferred genders, ages, diagnoses, or assumptions about legal relationships. Such rules would require a much more carefully specified model. Here, a queue entry means that supplied metadata deserves a review step; it is not an assertion that a relationship is wrong or a person has provided unreliable information.
Identifiers are case-sensitive and are not normalized. If an application needs another policy, implement that at an explicit boundary and test it separately. Silently trimming or changing identifiers inside a review pass can merge records that were meant to remain distinct. A stable reference is useful only when the editor and the queue agree on which record it names.
Represent reasons without overwriting the draft
The implementation recognizes three reasons: missing evidence, human review pending, and a reported conflict. They can occur together, and the result retains their deterministic order. For example, marking a claim reviewed does not erase the fact that its evidence list is empty. The reviewer can see both metadata choices rather than receiving an artificial all-clear because a single checkbox was selected.
The conflict flag is particularly easy to overinterpret. This function does not discover contradictory notes. It merely preserves a conflict that another process or reviewer has already reported. Similarly, a nonempty evidence list does not prove that the notes exist or support the claim. A fabricated but well-formed label can pass this transformation. Keep those limitations visible in the interface rather than naming the result a truth score.
Do not automatically remove a line because it appears in the queue. That would turn an explanation of uncertainty into an unsolicited edit. Instead, let a reviewer open the associated record, consult the source material, and record a justified decision. Keep an unresolved claim unresolved when no new information supports a change; completing every field is not the same as improving the evidence.
Validate the boundary before creating queue entries
The function requires an array and rejects malformed claim metadata with a TypeError. It also rejects duplicate identifiers rather than silently merging their state. These errors are distinct from ordinary review reasons. An absent evidence reference may be a legitimate incomplete draft, while a string where an evidence array was expected means that the agreed data contract has not been supplied.
Boundary errors produce generic messages that do not include the submitted identifiers or note text. That reduces one accidental logging channel, but it is not a complete privacy design. The returned claim identifiers may still be identifying when another system maps them to people. Decide what your application logs and retains instead of assuming that short or synthetic-looking identifiers are anonymous.
The function creates fresh result objects and reason arrays without modifying the supplied claims or evidence lists. One test passes frozen input to make that expectation concrete. In a production editor, immutability can help keep a review calculation separate from the state that will later be saved. It does not by itself prevent another component, concurrent request, or unauthorized user from changing the underlying record.
Keep UI state and evidence decisions separate
Render reason codes with plain-language labels and a short explanation of what triggered them. A missing-evidence message should say that no labels were supplied, not that the claim has been disproved. A reported-conflict message should tell the reviewer where to look next. Avoid replacing those distinctions with a single colored badge whose meaning depends on a private explanation from the author.
When someone completes a review, persist the decision through the application's ordinary controlled save path, then recompute the queue from the new state. Do not clear a visible queue row merely because a modal closed or a request was dispatched. An interrupted save can otherwise leave the interface looking resolved while the authoritative record still says that review is pending.
Concurrent edits need an explicit policy as well. A boolean review flag cannot explain which version a person reviewed. A larger application may need revision identifiers, permissions, and a suitably minimized audit record. Those concerns are intentionally outside this example. Adding fields without specifying how they relate to a saved revision would create an appearance of traceability without providing it.
Use a visual workspace without inventing an integration
One browser workspace for drawing and manually reviewing these maps is the free genogram maker from Genogram Maker. It provides editable people and relationships, contextual notes, auto layout, and optional AI drafting. Its current free plan describes two projects, up to fifty people per project, five monthly AI credits after sign-in, and PNG export. AI creation or updates use one to five credits. Professional access adds PDF, SVG, GEDCOM, and editable JSON downloads; confirm current official plans before depending on a format or allowance.
The function below does not import those exports or connect to that service. An actual integration would first need a documented schema, an intentional mapping of semantics, permission to handle the information, and tests against the relevant boundary. A product screenshot cannot substitute for structured input. It shows a reading view, not the stable identifiers and review metadata this example expects.
The image is the official website's example editor view, not a private family record or an independent test result. The same review-queue principle can be applied beside a paper diagram or another editor. Its value comes from making unresolved metadata visible, not from a dependency on a particular drawing tool.
Execute the small test suite honestly
Save the two files below in the same directory and run the command in the final block. The eight fixtures check a clean reviewed claim, missing evidence, pending human review, a reported conflict, combined reasons, duplicate identifiers, frozen input, and a malformed evidence field. The observed local result was eight passes and zero failures. No claim of exhaustive coverage is intended.
Before adapting the example, add fixtures for your actual schema and permissions boundary. Consider malformed root values, empty identifiers, mixed-type evidence labels, absent review flags, and conflicts between revisions. Test the interface's behavior after a failed save and after a newer version arrives. Those cases are not demonstrated by the small set of passing assertions in this article.
Share the reading copy only after a separate review
An empty queue is not permission to publish a family diagram. Metadata can be complete while a claim remains inaccurate, contested, or unnecessary for the intended audience. Review the source notes, the symbol legend, the purpose of the document, and which details should remain private. Keep a reading image and an editable source as distinct artifacts when they serve different recipients.
For educational examples, entirely fictional data is often the simplest starting point. For personal or professional work, review current privacy practices and applicable organizational requirements before entering information into any online service. A browser interface alone establishes neither local-only processing nor confidentiality guarantees. This utility's modest job is to expose a few explicit review states while leaving evidence, consent, and sharing decisions with the appropriate people.
Implementation: review-queue.mjs
// A teaching example, not a factual or clinical validator.
export function reviewQueue(claims) {
if (!Array.isArray(claims)) throw new TypeError("Expected a claim array");
const ids = new Set();
const queue = [];
for (const claim of claims) {
if (!claim || typeof claim !== "object" || Array.isArray(claim) ||
typeof claim.id !== "string" || !claim.id.trim() ||
!Array.isArray(claim.evidenceLabels) ||
!claim.evidenceLabels.every(x => typeof x === "string" && x.trim()) ||
typeof claim.humanReviewed !== "boolean" ||
(claim.conflictReported !== undefined &&
typeof claim.conflictReported !== "boolean")) {
throw new TypeError("Malformed claim metadata");
}
if (ids.has(claim.id)) throw new TypeError("Duplicate claim id");
ids.add(claim.id);
const reasons = [];
if (claim.evidenceLabels.length === 0) reasons.push("missing_evidence");
if (!claim.humanReviewed) reasons.push("human_review_pending");
if (claim.conflictReported === true) reasons.push("conflict_reported");
if (reasons.length) queue.push({ claimId: claim.id, reasons });
}
return queue;
}
Tests: queue.test.mjs
import test from "node:test";
import assert from "node:assert/strict";
import { reviewQueue } from "./review-queue.mjs";
const claim = updates => ({
id: "c1", evidenceLabels: ["fictional-note-a"],
humanReviewed: true, conflictReported: false, ...updates
});
test("reviewed metadata with evidence yields no queue entry", () => {
assert.deepEqual(reviewQueue([claim()]), []);
});
test("missing evidence is visible even when marked reviewed", () => {
assert.deepEqual(reviewQueue([claim({ evidenceLabels: [] })]), [
{ claimId: "c1", reasons: ["missing_evidence"] }
]);
});
test("unreviewed metadata requires human review", () => {
assert.deepEqual(reviewQueue([claim({ humanReviewed: false })]), [
{ claimId: "c1", reasons: ["human_review_pending"] }
]);
});
test("a reported conflict stays visible", () => {
assert.deepEqual(reviewQueue([claim({ conflictReported: true })]), [
{ claimId: "c1", reasons: ["conflict_reported"] }
]);
});
test("multiple reasons have deterministic order", () => {
assert.deepEqual(reviewQueue([claim({
evidenceLabels: [], humanReviewed: false, conflictReported: true
})]), [{ claimId: "c1", reasons: [
"missing_evidence", "human_review_pending", "conflict_reported"
] }]);
});
test("duplicate ids are rejected rather than silently merged", () => {
assert.throws(() => reviewQueue([claim(), claim()]), TypeError);
});
test("frozen input remains unchanged", () => {
const value = Object.freeze(claim({
evidenceLabels: Object.freeze([]), humanReviewed: false
}));
const input = Object.freeze([value]);
const before = JSON.stringify(input);
reviewQueue(input);
assert.equal(JSON.stringify(input), before);
});
test("malformed evidence metadata is rejected", () => {
assert.throws(() => reviewQueue([claim({ evidenceLabels: "note-a" })]),
TypeError);
});
Run locally
node --test queue.test.mjs
Observed for the included synthetic fixtures: eight tests, eight passes, zero failures. This example performs no network requests and makes no factual or clinical determinations.

Top comments (0)