@ietf-tools/idnits
Version:
Library / CLI to inspect Internet-Draft documents for a variety of conditions to conform with IETF policies.
784 lines (701 loc) • 30.3 kB
JavaScript
import { ValidationWarning, ValidationError, ValidationComment } from '../helpers/error.mjs'
import { checkReferencesInDownrefs } from '../remote/downref.mjs'
import { MODES } from '../config/modes.mjs'
import { getStatusCategory } from '../config/rfc-status-hierarchy.mjs'
import { fetchRemoteDocInfoJson, fetchRemoteRfcInfo } from '../remote/common.mjs'
import { findAllDescendantsWith } from '../helpers/traversal.mjs'
import { isPlainObject, last, toPairs, trimStart } from 'lodash-es'
import { formatEnumeration, splitDraftName } from '../helpers/common.mjs'
/**
* Validate document references for RFCs and Drafts downrefs.
*
* @param {Object} doc - Document to validate
* @param {Object} [opts] - Additional options
* @param {number} [opts.mode=0] - Validation mode to use
* @param {boolean} [opts.offline=false] - Skip fetching remote data if true
* @returns {Array} - List of errors/warnings/comments
*/
export async function validateDownrefs (doc, { mode = MODES.NORMAL } = {}) {
const result = []
if (mode === MODES.SUBMISSION) {
return result
}
switch (doc.type) {
case 'txt': {
const { referenceSectionRfc, draftStatusReferences, referenceSectionDraftReferences, nonReferenceSectionDraftReferences } = doc.data.extractedElements
const statusCategory = getStatusCategory(doc.data.header.intendedStatus ?? doc.data.header.category)
const possibleDownrefs = referenceSectionDraftReferences.map(ref => ref.value).filter(refValue => !nonReferenceSectionDraftReferences.includes(refValue))
const rfcs = referenceSectionRfc.filter((extracted) => extracted.subsection === 'normative_references').map((extracted) => extracted.value).map((rfcNumber) => `RFC ${rfcNumber}`)
const drafts = normalizeDraftReferencesForDownref(draftStatusReferences.filter((extracted) => extracted.subsection === 'normative_references').map((extracted) => extracted.value))
for (const ref of [...rfcs, ...drafts, ...possibleDownrefs]) {
let refStatus = null
if (ref.startsWith('RFC')) {
const rfcNumber = trimStart(ref.split(' ')[1], '0')
const rfcInfo = await fetchRemoteRfcInfo(rfcNumber)
refStatus = getStatusCategory(rfcInfo?.status)
} else {
const draftInfo = await fetchRemoteDocInfoJson(ref)
refStatus = getStatusCategory(draftInfo?.intended_std_level || draftInfo?.std_level)
}
if (refStatus !== null && refStatus < statusCategory) {
const isDownref = await checkReferencesInDownrefs([ref])
const docType = doc.docKind === 'rfc' ? 'this RFC' : "this draft's intended status"
if (isDownref.length > 0) {
switch (mode) {
case MODES.NORMAL:
result.push(new ValidationWarning('DOWNREF_TO_LOWER_STATUS_IN_REGISTRY', `Reference to ${ref}, which has a lower status than ${docType} and is listed in the Downref Registry.`, {
ref: 'https://www.rfc-editor.org/info/bcp97'
}))
break
case MODES.FORGIVE_CHECKLIST:
result.push(new ValidationWarning('DOWNREF_TO_LOWER_STATUS_IN_REGISTRY', `Reference to ${ref}, which has a lower status than ${docType} and is listed in the Downref Registry.`, {
ref: 'https://www.rfc-editor.org/info/bcp97'
}))
break
}
} else {
switch (mode) {
case MODES.NORMAL:
result.push(new ValidationError('DOWNREF_TO_LOWER_STATUS', `Reference to ${ref}, which has a lower status than ${docType}. This reference is not in the Downref Registry.`, {
ref: 'https://www.rfc-editor.org/info/bcp97'
}))
break
case MODES.FORGIVE_CHECKLIST:
result.push(new ValidationWarning('DOWNREF_TO_LOWER_STATUS', `Reference to ${ref}, which has a lower status than ${docType}. This reference is not in the Downref Registry.`, {
ref: 'https://www.rfc-editor.org/info/bcp97'
}))
break
}
}
} else if (!refStatus) {
switch (mode) {
case MODES.NORMAL:
result.push(new ValidationError('POSSIBLE_DOWNREF', `Reference to ${ref} is possible downref`, {
ref: 'https://www.rfc-editor.org/info/bcp97'
}))
break
case MODES.FORGIVE_CHECKLIST:
result.push(new ValidationWarning('POSSIBLE_DOWNREF', `Reference to ${ref} is possible downref`, {
ref: 'https://www.rfc-editor.org/info/bcp97'
}))
break
}
}
}
break
}
case 'xml': {
const referencesSections = doc.data.rfc?.back?.references?.references
if (!referencesSections) { return result }
const normalizedReferences = extractReferencesFromXml(referencesSections, /normative references/i)
const docStatus = getStatusCategory(doc.data.rfc._attr.category ?? doc.data.rfc._attr.status)
for (const ref of normalizedReferences) {
let refStatus = null
if (/^RFC\s*\d+$/i.test(ref)) {
const rfcNumber = ref.match(/\d+/)[0]
const rfcInfo = await fetchRemoteRfcInfo(rfcNumber)
refStatus = getStatusCategory(rfcInfo?.status)
} else if (/draft/i.test(ref)) {
const draftInfo = await fetchRemoteDocInfoJson(ref)
refStatus = getStatusCategory(draftInfo?.intended_std_level || draftInfo?.std_level)
}
if (refStatus !== null && refStatus < docStatus) {
const isDownref = await checkReferencesInDownrefs([ref])
const docType = doc.docKind === 'rfc' ? 'this RFC' : "this draft's intended status"
if (isDownref.length > 0) {
switch (mode) {
case MODES.NORMAL:
result.push(
new ValidationWarning(
'DOWNREF_TO_LOWER_STATUS_IN_REGISTRY',
`Reference to ${ref}, which has a lower status than ${docType} and is listed in the Downref Registry.`,
{ ref: 'https://www.rfc-editor.org/info/bcp97' }
)
)
break
case MODES.FORGIVE_CHECKLIST:
result.push(
new ValidationWarning(
'DOWNREF_TO_LOWER_STATUS_IN_REGISTRY',
`Reference to ${ref}, which has a lower status than ${docType} and is listed in the Downref Registry.`,
{ ref: 'https://www.rfc-editor.org/info/bcp97' }
)
)
break
}
} else {
switch (mode) {
case MODES.NORMAL:
result.push(
new ValidationError(
'DOWNREF_TO_LOWER_STATUS',
`Reference to ${ref}, which has a lower status than ${docType}. This reference is not in the Downref Registry.`,
{ ref: 'https://www.rfc-editor.org/info/bcp97' }
)
)
break
case MODES.FORGIVE_CHECKLIST:
result.push(
new ValidationWarning(
'DOWNREF_TO_LOWER_STATUS',
`Reference to ${ref}, which has a lower status than ${docType}. This reference is not in the Downref Registry.`,
{ ref: 'https://www.rfc-editor.org/info/bcp97' }
)
)
break
}
}
} else if (refStatus === null) {
switch (mode) {
case MODES.NORMAL:
result.push(
new ValidationError(
'POSSIBLE_DOWNREF',
`${ref} status can't be fetched. Reference is possible downref`,
{ ref: 'https://www.rfc-editor.org/info/bcp97' }
)
)
break
case MODES.FORGIVE_CHECKLIST:
result.push(
new ValidationWarning(
'POSSIBLE_DOWNREF',
`${ref} status can't be fetched. Reference is possible downref`,
{ ref: 'https://www.rfc-editor.org/info/bcp97' }
)
)
break
}
}
}
break
}
}
return result
}
/**
* Extracts references from one or more XML document reference sections.
*
* @param {Array<Object>} referencesSections - XML document <references> sections
* @param {RegExp|RegExp[]} [subsections] - Один або масив RegExp для фільтрації по назві підсекції.
* Якщо не вказано, витягуємо з усіх.
* @returns {Array<String>} - Масив нормалізованих референсів (RFC і draft-ім’я).
*/
function extractReferencesFromXml (referencesSections, subsections, normalizeDraftRefs = true) {
let sectionsToScan
if (!subsections) {
sectionsToScan = referencesSections
} else {
const regs = Array.isArray(subsections) ? subsections : [subsections]
sectionsToScan = referencesSections.filter(section => {
const nameText = typeof section.name === 'string'
? section.name
: isPlainObject(section.name)
? section.name['#text']
: ''
if (!nameText) return false
const lower = nameText.toLowerCase()
return regs.some(rx => rx.test(lower))
})
}
const allRefs = sectionsToScan.flatMap(sec => sec.reference || [])
if (allRefs.length === 0) {
return []
}
return getXmlReferences(allRefs, normalizeDraftRefs)
}
/**
* function to get xml references from xml document
*
* @param {Array} references
* @returns
*/
function getXmlReferences (references, normilizeDraftRefs = true) {
const { drafts, rfcs } = references.reduce((acc, ref) => {
const sis = Array.isArray(ref.seriesInfo)
? ref.seriesInfo
: ref.seriesInfo
? [ref.seriesInfo]
: []
// draft
const di = sis.find(si => si._attr?.name === 'Internet-Draft')
if (di?._attr?.value) {
acc.drafts.add(di._attr.value)
}
// rfc
const ri = sis.find(si => si._attr?.name === 'RFC')
if (ri?._attr?.value) {
acc.rfcs.add(`RFC ${ri._attr.value}`)
}
return acc
}, { drafts: new Set(), rfcs: new Set() })
const draftList = normilizeDraftRefs ? normalizeDraftReferences(Array.from(drafts)) : Array.from(drafts)
const rfcList = Array.from(rfcs)
return [...draftList, ...rfcList]
}
/**
* Normalize references by removing brackets, versions, and checking for drafts.
*
* @param {Array} references - Array of textual references.
* @returns {Array} - Array of normalized references containing "draft".
*/
function normalizeDraftReferences (references) {
return references
.map((ref) => {
let normalized = ref.replace(/^\[|\]$/g, '')
normalized = normalized
.replace(/-\d{2}$/, '')
.replace(/^I-D\./i, '')
return normalized
})
.filter((ref) => ref.toLowerCase().includes('draft'))
}
/**
*
* @param {Array} references - Array of textual references.
* @returns {Array} - Array of normalized references containing "draft".
*/
function normalizeDraftReferencesForDownref (references) {
return references
.map((ref) => {
let normalized = ref.replace(/^\[|\]$/g, '')
normalized = normalized.replace(/-\d{2}$/, '')
return normalized
})
}
/**
* Normalize XML references to drafts and RFCs.
*
* @param {Array} references - Array of reference strings.
* @returns {Array} - Normalized references including only drafts and RFCs.
*/
function normalizeXmlReferences (references) {
const normalizedReferences = []
references.forEach((ref) => {
if (/^RFC\d+$/i.test(ref)) {
const rfcNumber = ref.match(/\d+/)[0]
normalizedReferences.push(`RFC ${rfcNumber}`)
} else if (/draft/i.test(ref)) {
const draftName = ref.trim().replace(/^\[|\]$/g, '')
.replace(/-\d{2}$/, '')
.replace(/^I-D\./i, '')
normalizedReferences.push(draftName)
}
})
return normalizedReferences
}
/**
* Validates normative references within a document by checking the status of referenced RFCs.
*
* This function processes both text (`txt`) and XML (`xml`) documents, identifying RFCs within
* normative references and validating their status using remote data fetched from the RFC editor.
*
* - For TXT documents, it looks for normative references in the `referenceSectionRfc` field.
* - For XML documents, it extracts references from the back references section.
*
* Steps:
* 1. Extract normative references for both TXT and XML documents.
* 2. Fetch metadata for each RFC using `fetchRemoteRfcInfo`.
* 3. Validate the fetched status:
* - If no status is defined or the RFC cannot be fetched, a `UNDEFINED_STATUS` comment is added.
* - If the status is unrecognized, an `UNKNOWN_STATUS` comment is added.
* 4. Return a list of validation comments highlighting issues.
*
* @param {Object} doc - The document to validate.
* @param {Object} [opts] - Additional options.
* @param {number} [opts.mode=MODES.NORMAL] - Validation mode (e.g., NORMAL, SUBMISSION).
* @returns {Promise<Array>} - A list of validation results, including warnings or comments.
*/
export async function validateNormativeReferences (doc, { mode = MODES.NORMAL } = {}) {
const result = []
const RFC_NUMBER_REG = /^\d+$/
if (mode === MODES.SUBMISSION) {
return result
}
switch (doc.type) {
case 'txt': {
const normativeReferences = doc.data.extractedElements.referenceSectionRfc
.filter((el) => el.subsection === 'normative_references' && RFC_NUMBER_REG.test(el.value))
.map((el) => trimStart(el.value, '0'))
for (const rfcNum of normativeReferences) {
const rfcInfo = await fetchRemoteRfcInfo(rfcNum)
if (!rfcInfo || !rfcInfo.status) {
result.push(new ValidationComment('UNDEFINED_STATUS', `RFC ${rfcNum} does not have a defined status or could not be fetched.`, {
ref: `https://www.rfc-editor.org/info/rfc${rfcNum}`
}))
continue
}
const statusCategory = getStatusCategory(rfcInfo.status)
if (statusCategory === null) {
result.push(new ValidationComment('UNKNOWN_STATUS', `RFC ${rfcNum} has an unrecognized status: "${rfcInfo.status}".`, {
ref: `https://www.rfc-editor.org/info/rfc${rfcNum}`
}))
}
if (rfcInfo.obsoleted_by?.length > 0) {
const obsoletedByList = rfcInfo.obsoleted_by.map(rfc => `RFC${rfc}`).join(', ')
const message = `RFC ${rfcNum} is obsolete and has been replaced by: ${obsoletedByList}.`
if (mode === MODES.NORMAL) {
result.push(new ValidationError(
'OBSOLETE_REFERENCE',
message,
{
ref: 'https://authors.ietf.org/en/required-content#references'
}
))
} else if (mode === MODES.FORGIVE_CHECKLIST) {
result.push(new ValidationWarning(
'OBSOLETE_REFERENCE',
message,
{
ref: 'https://authors.ietf.org/en/required-content#references'
}
))
}
}
}
break
}
case 'xml': {
const referencesSections = doc.data.rfc?.back?.references?.references
if (!referencesSections) { return result }
const normativeReferences = extractReferencesFromXml(referencesSections, /normative references/i)
const normilizedReferences = normativeReferences.filter((ref) => ref.startsWith('RFC')).map((ref) => ref.match(/\d+/)[0])
for (const rfcNum of normilizedReferences) {
const rfcInfo = await fetchRemoteRfcInfo(rfcNum)
if (!rfcInfo || !rfcInfo.status) {
result.push(new ValidationComment('UNDEFINED_STATUS', `RFC ${rfcNum} does not have a defined status or could not be fetched.`, {
ref: `https://www.rfc-editor.org/info/rfc${rfcNum}`
}))
continue
}
const statusCategory = getStatusCategory(rfcInfo.status)
if (statusCategory === null) {
result.push(new ValidationComment('UNKNOWN_STATUS', `RFC ${rfcNum} has an unrecognized status: "${rfcInfo.status}".`, {
ref: `https://www.rfc-editor.org/info/rfc${rfcNum}`
}))
}
if (rfcInfo.obsoleted_by.length > 0) {
const obsoletedByList = rfcInfo.obsoleted_by.map(rfc => `RFC${rfc}`).join(', ')
const message = `RFC ${rfcNum} is obsolete and has been replaced by: ${obsoletedByList}.`
if (mode === MODES.NORMAL) {
result.push(new ValidationError(
'OBSOLETE_REFERENCE',
message,
{
ref: 'https://authors.ietf.org/en/required-content#references'
}
))
} else if (mode === MODES.FORGIVE_CHECKLIST) {
result.push(new ValidationWarning(
'OBSOLETE_REFERENCE',
message,
{
ref: 'https://authors.ietf.org/en/required-content#references'
}
))
}
}
}
break
}
}
return result
}
/**
* Validates unclassified references within a document.
*
* This function checks for issues in unclassified references from the document's
* extracted elements. Specifically, it identifies if:
* 1. The reference is obsolete (i.e., replaced by other documents).
* 2. The reference does not have a defined status or cannot be fetched.
*
* - For each unclassified reference, the function fetches its metadata using
* `fetchRemoteRfcInfo`.
* - If the reference is obsolete, it generates a validation error or warning
* based on the provided mode.
* - If the reference has no status or cannot be fetched, a validation comment is generated.
*
* Modes:
* - `NORMAL`: Produces `ValidationError` for obsolete references.
* - `FORGIVE_CHECKLIST`: Produces `ValidationWarning` for obsolete references.
*
* @param {Object} doc - The document object to validate.
* @param {Object} [opts] - Additional options.
* @param {number} [opts.mode=MODES.NORMAL] - Validation mode (`NORMAL` or `FORGIVE_CHECKLIST`).
* @returns {Promise<Array>} - A list of validation results, including errors, warnings, or comments.
*
*/
export async function validateUnclassifiedReferences (doc, { mode = MODES.NORMAL } = {}) {
const result = []
if (mode === MODES.SUBMISSION) {
return result
}
switch (doc.type) {
case 'txt': {
const unclassifiedReferences = doc.data.extractedElements.referenceSectionRfc
.filter(el => el.subsection === 'unclassified_references')
.map(el => el.value)
for (const ref of unclassifiedReferences) {
const rfcInfo = await fetchRemoteRfcInfo(ref)
if (!rfcInfo || !rfcInfo.status) {
result.push(new ValidationComment(
'UNDEFINED_STATUS',
`The unclassified reference ${ref} does not have a defined status or could not be fetched.`,
{ ref: `https://www.rfc-editor.org/info/rfc${ref}` }
))
continue
}
if (rfcInfo.obsoleted_by.length > 0) {
const obsoletedByList = rfcInfo.obsoleted_by.map(rfc => `RFC${rfc}`).join(', ')
const message = `RFC ${ref} is obsolete and has been replaced by: ${obsoletedByList}.`
if (mode === MODES.NORMAL) {
result.push(new ValidationError(
'OBSOLETE_REFERENCE',
message,
{ ref: 'https://authors.ietf.org/en/required-content#references' }
))
} else if (mode === MODES.FORGIVE_CHECKLIST) {
result.push(new ValidationWarning(
'OBSOLETE_REFERENCE',
message,
{ ref: 'https://authors.ietf.org/en/required-content#references' }
))
}
}
}
break
}
case 'xml': {
const referencesSections = doc.data.rfc?.back?.references?.references
if (!referencesSections) { return result }
const references = referencesSections
.filter(section => !/normative references|informative references/i.test(section.name))
.flatMap(section => findAllDescendantsWith(section, (value, key) => key === '_attr' && value.anchor))
.flatMap(match => Array.isArray(match.value.anchor) ? match.value.anchor : [match.value.anchor])
.filter(Boolean)
const unclassifiedReferences = normalizeXmlReferences(references)
.filter(ref => ref.startsWith('RFC'))
.map(ref => ref.match(/\d+/)[0])
for (const rfcNum of unclassifiedReferences) {
const rfcInfo = await fetchRemoteRfcInfo(rfcNum)
if (!rfcInfo?.status) {
result.push(new ValidationComment(
'UNDEFINED_STATUS',
`RFC ${rfcNum} does not have a defined status or could not be fetched`,
{ ref: `https://www.rfc-editor.org/info/rfc${rfcNum}` }
))
continue
}
if (rfcInfo.obsoleted_by.length) {
const obsoletedByList = rfcInfo.obsoleted_by.map(rfc => `RFC${rfc}`).join(', ')
const message = `RFC ${rfcNum} is obsolete and has been replaced by: ${obsoletedByList}.`
result.push(
mode === MODES.NORMAL
? new ValidationError('OBSOLETE_REFERENCE', message, { ref: 'https://authors.ietf.org/en/required-content#references' })
: new ValidationWarning('OBSOLETE_REFERENCE', message, { ref: 'https://authors.ietf.org/en/required-content#references' })
)
}
}
break
}
}
return result
}
/**
* Validates informative references within a document.
*
* This function checks for issues in informative references from the document's
* extracted elements. Specifically, it identifies if:
* 1. The reference is obsolete (i.e., replaced by other documents).
* 2. The reference does not have a defined status or cannot be fetched.
*
*
* @param {Object} doc - The document object to validate.
* @param {Object} [opts] - Additional options.
* @param {number} [opts.mode=MODES.NORMAL] - Validation mode (`NORMAL` or `FORGIVE_CHECKLIST`).
* @returns {Promise<Array>} - A list of validation results, including errors, warnings, or comments.
*/
export async function validateInformativeReferences (doc, { mode = MODES.NORMAL } = {}) {
const result = []
if (mode === MODES.SUBMISSION) {
return result
}
const informativeReferences = doc.data.extractedElements.referenceSectionRfc
.filter(el => el.subsection === 'informative_references')
.map(el => el.value)
for (const ref of informativeReferences) {
const rfcInfo = await fetchRemoteRfcInfo(ref)
if (!rfcInfo || !rfcInfo.status) {
result.push(new ValidationComment(
'UNDEFINED_STATUS',
`The informative reference RFC ${ref} does not have a defined status or could not be fetched.`,
{ ref: `https://www.rfc-editor.org/info/rfc${ref}` }
))
continue
}
if (rfcInfo.obsoleted_by.length > 0) {
const obsoletedByList = rfcInfo.obsoleted_by.map(rfc => `RFC${rfc}`).join(', ')
const message = `RFC ${ref} is obsolete and has been replaced by: ${obsoletedByList}.`
if (mode === MODES.NORMAL) {
result.push(new ValidationError(
'OBSOLETE_REFERENCE',
message,
{ ref: 'https://authors.ietf.org/en/required-content#references' }
))
} else if (mode === MODES.FORGIVE_CHECKLIST) {
result.push(new ValidationWarning(
'OBSOLETE_REFERENCE',
message,
{ ref: 'https://authors.ietf.org/en/required-content#references' }
))
}
}
}
return result
}
/**
* Validate draft references in a document to ensure their states are valid.
*
* This function checks all draft references in a document, ensuring:
* - The referenced draft exists and its state can be fetched.
* - A draft is not already published as an RFC (and thus should not be referenced as a draft).
*
* @param {Object} doc - The document to validate. Should contain `type` and `data` properties.
* @param {Object} [opts] - Additional options for validation.
* @param {number} [opts.mode] - The validation mode (e.g., SUBMISSION mode skips validation).
*
* @returns {Array} A list of `ValidationWarning` objects containing information about invalid references.
*/
export async function validatePublishedDraftReferences (doc, { mode } = {}) {
const result = []
if (mode === MODES.SUBMISSION) {
return result
}
switch (doc.type) {
case 'txt': {
const referenceSectionDraftReferences = doc.data.extractedElements.draftStatusReferences
const drafts = normalizeDraftReferences(referenceSectionDraftReferences.map((el) => el.value))
for (let i = 0; i < drafts.length; i++) {
const draftInfo = await fetchRemoteDocInfoJson(drafts[i])
if (!draftInfo || !draftInfo.state) {
result.push(new ValidationWarning(
'UNDEFINED_STATE',
`The draft reference ${drafts[i]} does not have a defined state or could not be fetched.`,
{ ref: `https://datatracker.ietf.org/doc/${drafts[i]}` }
))
continue
} else if (draftInfo.state.toLowerCase() === 'rfc') {
result.push(new ValidationWarning(
'INVALID_STATE_FOR_DRAFT',
`The draft reference ${drafts[i]} is already published as an RFC and should not be referenced as a draft.`,
{ ref: `https://datatracker.ietf.org/doc/${drafts[i]}` }
))
}
}
break
}
case 'xml': {
const referencesSections = doc.data.rfc?.back?.references?.references
if (!referencesSections) { return result }
const definedReferences = findAllDescendantsWith(referencesSections, (value, key) => key === '_attr' && value.anchor)
.flatMap((match) => (Array.isArray(match.value.anchor) ? match.value.anchor : [match.value.anchor]))
.filter(Boolean)
const normilizedReferences = normalizeXmlReferences(definedReferences)
const drafts = normilizedReferences.filter((el) => el.startsWith('draft-'))
for (let i = 0; i < drafts.length; i++) {
const draftInfo = await fetchRemoteDocInfoJson(drafts[i])
if (!draftInfo || !draftInfo.state) {
result.push(new ValidationWarning(
'UNDEFINED_STATE',
`The draft reference ${drafts[i]} does not have a defined state or could not be fetched.`,
{ ref: `https://datatracker.ietf.org/doc/${drafts[i]}` }
))
continue
} else if (draftInfo.state.toLowerCase() === 'rfc') {
result.push(new ValidationWarning(
'INVALID_STATE_FOR_DRAFT',
`The draft reference ${drafts[i]} is already published as an RFC and should not be referenced as a draft.`,
{ ref: `https://datatracker.ietf.org/doc/${drafts[i]}` }
))
}
}
break
}
}
return result
}
/**
*
* Validate references for stale. If document has newer version, it will be marked as stale and
* output validation warning.
*
* @param {Object} doc - The document to validate. Should contain `type` and `data` properties.
* @param {Object} [opts] - Additional options for validation.
* @param {number} [opts.mode] - The validation mode (e.g., SUBMISSION mode skips validation).
*/
export async function validateReferenceForStale (doc, { mode } = {}) {
const result = []
if (mode === MODES.SUBMISSION) {
return result
}
let parsedDrafts = []
switch (doc.type) {
case 'txt': {
const referenceSectionDraftReferences = doc.data.extractedElements.draftStatusReferences
parsedDrafts = referenceSectionDraftReferences.map(r => {
const { baseName, version } = splitDraftName(r.value)
return {
baseName,
version,
subsection: r.subsection
}
})
break
}
case 'xml': {
const referencesSections = doc.data.rfc?.back?.references?.references
if (!referencesSections) { return result }
const references = extractReferencesFromXml(referencesSections, /.*/gmi, false)
const drafts = references.filter((el) => el.startsWith('draft-'))
parsedDrafts = drafts.map(r => {
const { baseName, version } = splitDraftName(r)
return {
baseName,
version
}
})
break
}
}
for (let i = 0; i < parsedDrafts.length; i++) {
const draftName = parsedDrafts[i].baseName
const draftVersion = parsedDrafts[i].version
const draftInfo = await fetchRemoteDocInfoJson(draftName)
const lastVersion = last(draftInfo?.rev_history)
if (!draftInfo || !draftInfo.rev_history || !lastVersion) {
result.push(new ValidationWarning(
'UNDEFINED_STATE',
`The draft reference ${draftName} does not have a defined state or could not be fetched.`,
{ ref: `https://datatracker.ietf.org/doc/${draftName}` }
))
continue
} else if (lastVersion.name !== draftName && lastVersion.rev !== draftVersion) {
const newerDrafts = {}
let curPublishedDate = null
for (const entry of draftInfo.rev_history) {
if (entry.name === draftName && entry.rev === draftVersion) {
curPublishedDate = entry.published
} else if (curPublishedDate && entry.published > curPublishedDate) {
newerDrafts[entry.name] = entry.rev
}
}
const newerDraftsArr = toPairs(newerDrafts).map(d => `${d[0]} (version ${d[1]})`)
result.push(new ValidationWarning(
'OUTDATED_DRAFT',
`The draft reference ${draftName} (version ${draftVersion}) is stale. It was replaced by ${formatEnumeration(newerDraftsArr)}.`,
{ ref: `https://datatracker.ietf.org/doc/${draftName}` }
))
}
}
return result
}