app/lib/publications/update-notice.mjs
69 lines and 2 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| updateNoticeProblem | 30 to 49 | 11 | 100% | WatchCRAP 11 | No data: no mutants in this code |
| updateNoticeText | 58 to 74 | 4 | 100% | OkCRAP 4 | No data: no mutants in this code |
Imports
It imports no other module of this repo. Imported by app/lib/publications/validate.mjs, app/routes/publications.$slug.tsx.
Source
// No record carries a notice today; the path is built cold so a correction needs only a data change,
// and a test drives it with a real retracted DOI. The paper carries updated-by (the notice carries
// update-to), so the DOI here is the notice's.
export const UPDATE_TYPES = /** @type {const} */ ([
"retraction",
"correction",
"expression-of-concern",
]);
const LABEL = {
retraction: "Retracted",
correction: "Corrected",
"expression-of-concern": "Expression of concern",
};
/**
* @typedef {object} UpdateNotice
* @property {string} type one of UPDATE_TYPES
* @property {string} doi the NOTICE's DOI, not the paper's
* @property {string | null} [date] YYYY-MM-DD, as deposited
*/
/**
* A reason, not a boolean, so the gate can say which part of a notice is malformed.
*
* @param {unknown} value
* @returns {string | null} null when the notice is usable
*/
export function updateNoticeProblem(value) {
if (value === null || value === undefined) return null;
if (typeof value !== "object") return "not an object";
const notice = /** @type {Record<string, unknown>} */ (value);
if (!UPDATE_TYPES.includes(/** @type {any} */ (notice.type))) {
return `type ${JSON.stringify(notice.type)} is not one of ${UPDATE_TYPES.join(", ")}`;
}
// Shape only: checking existence would put a network call in a gate.
if (typeof notice.doi !== "string" || !/^10\.\d{4,9}\/\S+$/.test(notice.doi)) {
return `doi ${JSON.stringify(notice.doi)} is not a DOI name`;
}
if (
notice.date !== null &&
notice.date !== undefined &&
!(typeof notice.date === "string" && /^\d{4}-\d{2}-\d{2}$/.test(notice.date))
) {
return `date ${JSON.stringify(notice.date)} is not YYYY-MM-DD`;
}
return null;
}
/**
* Links to the notice's own DOI, the only honest citation for it. The deposited date is in the sentence
* so an undated retraction is not read as recent.
*
* @param {UpdateNotice} notice
* @returns {{ label: string, sentence: string, url: string }}
*/
export function updateNoticeText(notice) {
const problem = updateNoticeProblem(notice);
if (problem) {
// Every caller passed the gate, so data changed under a green build; a 500 beats a half-rendered notice.
throw new Error(`unusable update notice: ${problem}`);
}
const label = LABEL[/** @type {keyof typeof LABEL} */ (notice.type)];
const when = notice.date ? ` on ${notice.date}` : "";
return {
label,
sentence:
notice.type === "expression-of-concern"
? `The publisher has issued an expression of concern about this paper${when}.`
: `This paper was ${label.toLowerCase()}${when} by the publisher.`,
url: `https://doi.org/${notice.doi}`,
};
}