ReqifExchangeCodec.java

package com.taxonomy.exchange;

import com.taxonomy.extension.api.integration.IntegrationContracts.*;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static com.taxonomy.exchange.ExchangeXml.*;

/** ReqIF 1.2's normative 2011 XML vocabulary. Original type systems and safe extensions remain exchange evidence. */
public final class ReqifExchangeCodec {
    public static final String PROFILE = "reqif-1.2";
    public static final String VERSION = "1";
    public static final String NS = "http://www.omg.org/spec/ReqIF/20110401/reqif.xsd";
    public static final String XHTML = "http://www.w3.org/1999/xhtml";
    private static final String FIXED_TIME = "2000-01-01T00:00:00Z";

    public ExchangeDocument read(byte[] content, String externalVersion, boolean completeScope) {
        Document doc = parse(content);
        if (!NS.equals(doc.getDocumentElement().getNamespaceURI()) || !"REQ-IF".equals(doc.getDocumentElement().getLocalName()))
            throw invalid("WRONG_FORMAT", "Expected a ReqIF document");
        validate(doc, PROFILE);
        List<MappingLoss> losses = new ArrayList<>();
        Map<String, Element> definitions = identities(doc);
        checkReferences(doc, definitions);
        List<Artifact> artifacts = new ArrayList<>();
        for (Element object : all(doc, NS, "SPEC-OBJECT")) {
            Fields fields = fields(object, definitions);
            String id = required(object, "IDENTIFIER");
            String titleKey = findField(fields, List.of("ReqIF.ChapterName", "ReqIF.Name", "Title", "Name"));
            String textKey = findField(fields, List.of("ReqIF.Text", "ReqIF.Description", "Text", "Description"));
            if (textKey == null) {
                List<String> rich = fields.extensions.entrySet().stream().filter(e -> e.getKey().startsWith("kind:") && e.getValue().equals("XHTML"))
                        .map(e -> e.getKey().substring(5)).sorted().toList();
                if (rich.size() == 1) textKey = rich.getFirst();
                else if (rich.size() > 1) losses.add(loss(id, "text", "AMBIGUOUS_TEXT_ATTRIBUTE", LossDisposition.UNSUPPORTED,
                        "Several XHTML attributes have no recognized text mapping; select a mapping profile before applying"));
            }
            if (textKey != null && textKey.equals(titleKey)) {
                titleKey = null;
                losses.add(loss(id, "title", "TITLE_USES_LONG_NAME", LossDisposition.TRANSFORMED,
                        "The sole rich-text attribute supplies the body; LONG-NAME independently supplies the title"));
            }
            String title = titleKey == null ? object.getAttribute("LONG-NAME") : plain(fields.values.get(titleKey), fields.extensions.get("kind:" + titleKey));
            String body = textKey == null ? "" : plain(fields.values.get(textKey), fields.extensions.get("kind:" + textKey));
            if (title.isBlank()) title = id;
            if (body.isBlank()) losses.add(loss(id, "text", "EMPTY_REQUIREMENT_TEXT", LossDisposition.UNSUPPORTED,
                    "This object has no mapped requirement text; its source and attributes remain available for review"));
            if (titleKey != null) fields.extensions.put("titleAttribute", titleKey);
            if (textKey != null) fields.extensions.put("textAttribute", textKey);
            fields.extensions.put("lastChange", object.getAttribute("LAST-CHANGE"));
            fields.extensions.put("longName", object.getAttribute("LONG-NAME"));
            fields.extensions.put("xml", xml(object));
            artifacts.add(new Artifact(id, ArtifactKind.REQUIREMENT, ref(object, "TYPE"), title, body, fields.values, fields.extensions));
        }
        for (Element specification : all(doc, NS, "SPECIFICATION")) {
            Fields fields = fields(specification, definitions);
            Element evidence = (Element) specification.cloneNode(true);
            Element hierarchy = child(evidence, "CHILDREN"); if (hierarchy != null) evidence.removeChild(hierarchy);
            fields.extensions.put("xml", xml(evidence));
            artifacts.add(new Artifact(required(specification, "IDENTIFIER"), ArtifactKind.SPECIFICATION, ref(specification, "TYPE"),
                    specification.getAttribute("LONG-NAME"), specification.getAttribute("DESC"), fields.values, fields.extensions));
        }
        if (artifacts.size() > MAX_ARTIFACTS) throw invalid("ITEM_LIMIT", "ReqIF exceeds the supported item count");
        List<Relation> relations = new ArrayList<>();
        for (Element relation : all(doc, NS, "SPEC-RELATION")) {
            Fields fields = fields(relation, definitions);
            fields.extensions.put("xml", xml(relation));
            fields.extensions.put("longName", relation.getAttribute("LONG-NAME"));
            relations.add(new Relation(required(relation, "IDENTIFIER"), ref(relation, "TYPE"), ref(relation, "SOURCE"), ref(relation, "TARGET"), fields.values, fields.extensions));
        }
        List<Placement> placements = new ArrayList<>();
        for (Element spec : all(doc, NS, "SPECIFICATION")) hierarchy(child(spec, "CHILDREN"), required(spec, "IDENTIFIER"), null, placements);
        for (Element extension : all(doc, NS, "REQ-IF-TOOL-EXTENSION"))
            losses.add(loss(null, "toolExtensions", "TOOL_EXTENSION_PRESERVED", LossDisposition.PRESERVED_EXTENSION,
                    "Safe tool-specific XML is retained without interpreting vendor behavior"));
        if (!all(doc, NS, "RELATION-GROUP").isEmpty())
            losses.add(loss(null, "relationGroups", "RELATION_GROUP_PRESERVED", LossDisposition.PRESERVED_EXTENSION,
                    "Relation groups are preserved in exchange evidence; no portfolio group authoring is implied"));
        for (Element object : all(doc, XHTML, "object"))
            losses.add(loss(null, "xhtml.object", "ATTACHMENT_NOT_FETCHED", LossDisposition.UNSUPPORTED,
                    "Embedded-object references are retained as inert source; binary payloads are not fetched or executed"));
        String safe = xml(doc);
        Map<String, String> metadata = new LinkedHashMap<>();
        metadata.put("sourceTool", text(all(doc, NS, "REQ-IF-HEADER").getFirst(), "SOURCE-TOOL-ID"));
        Element tools = child(doc.getDocumentElement(), "TOOL-EXTENSIONS"); if (tools != null) metadata.put("TOOL-EXTENSIONS", xml(tools));
        Element core = all(doc, NS, "REQ-IF-CONTENT").getFirst();
        for (String name : List.of("DATATYPES", "SPEC-TYPES", "SPEC-RELATION-GROUPS")) {
            Element node = child(core, name); if (node != null) metadata.put(name, xml(node));
        }
        return new ExchangeDocument(PROFILE, VERSION, externalVersion == null ? digest(content) : externalVersion, completeScope, safe,
                artifacts, relations, placements, metadata, losses);
    }

    public byte[] write(ExchangeDocument source) {
        validatePlacements(source);
        if ((source.source() == null || source.source().isBlank()) && !source.metadata().containsKey("DATATYPES")) {
            ExchangeDocument initial = read(ExchangeXml.write(generated(source)), source.externalVersion(), source.completeScope());
            List<Placement> placements = source.placements();
            if (placements.isEmpty()) {
                List<Placement> flat = new ArrayList<>();
                String specification = initial.artifacts().stream().filter(a -> a.kind() == ArtifactKind.SPECIFICATION).findFirst().orElseThrow().id();
                int position = 0;
                for (Artifact artifact : initial.artifacts()) if (artifact.kind() == ArtifactKind.REQUIREMENT)
                    flat.add(new Placement("hierarchy-" + artifact.id(), specification, null, artifact.id(), position++, Map.of("LAST-CHANGE", FIXED_TIME)));
                placements = flat;
            }
            return write(new ExchangeDocument(initial.profile(), initial.profileVersion(), initial.externalVersion(), initial.completeScope(), initial.source(),
                    initial.artifacts(), initial.relations(), placements, initial.metadata(), source.losses()));
        }
        Document doc = source.source() == null || source.source().isBlank() ? generated(source) : parse(source.source().getBytes(StandardCharsets.UTF_8));
        Element core = all(doc, NS, "REQ-IF-CONTENT").getFirst();
        if (source.metadata().containsKey("TOOL-EXTENSIONS")) {
            Element tools = child(doc.getDocumentElement(), "TOOL-EXTENSIONS"); if (tools != null) doc.getDocumentElement().removeChild(tools);
            doc.getDocumentElement().appendChild(doc.importNode(parse(source.metadata().get("TOOL-EXTENSIONS").getBytes(StandardCharsets.UTF_8)).getDocumentElement(), true));
        }
        for (String name : List.of("DATATYPES", "SPEC-TYPES", "SPEC-RELATION-GROUPS")) if (source.metadata().containsKey(name)) {
            Element previous = child(core, name);
            Node replacement = doc.importNode(parse(source.metadata().get(name).getBytes(StandardCharsets.UTF_8)).getDocumentElement(), true);
            if (previous != null) core.replaceChild(replacement, previous); else core.appendChild(replacement);
        }
        List<Artifact> nativeRequirements = source.artifacts().stream().filter(a -> a.kind() == ArtifactKind.REQUIREMENT && !a.extensions().containsKey("xml")).toList();
        Map<String, Element> generatedObjects = Map.of();
        if (!nativeRequirements.isEmpty()) {
            Document generated = generated(new ExchangeDocument(PROFILE, VERSION, source.externalVersion(), true, "", nativeRequirements,
                    List.of(), List.of(), Map.of(), List.of()));
            Map<String, Element> present = identities(doc);
            for (String group : List.of("DATATYPES", "SPEC-TYPES")) {
                Element target = child(core, group); if (target == null) target = append(core, NS, group);
                for (Element definition : children(all(generated, NS, group).getFirst())) {
                    Element previous = present.get(required(definition, "IDENTIFIER"));
                    if (previous == null) target.appendChild(doc.importNode(definition, true));
                    else if (!ExchangeXml.semantic(xml(previous)).equals(ExchangeXml.semantic(xml(definition))))
                        throw invalid("TYPE_IDENTITY_COLLISION", "A native export type collides with an existing external identity");
                }
            }
            generatedObjects = identities(generated);
        }
        Map<String, Element> ids = identities(doc);
        Map<String, Artifact> artifacts = new LinkedHashMap<>();
        source.artifacts().forEach(a -> { if (artifacts.putIfAbsent(a.id(), a) != null) throw invalid("DUPLICATE_IDENTITY", "Duplicate exchange identity"); });
        Element objects = child(core, "SPEC-OBJECTS"); if (objects == null) objects = append(core, NS, "SPEC-OBJECTS");
        while (objects.hasChildNodes()) objects.removeChild(objects.getFirstChild());
        for (Artifact artifact : source.artifacts()) if (artifact.kind() == ArtifactKind.REQUIREMENT) {
            Element object;
            if (artifact.extensions().containsKey("xml")) {
                object = (Element) doc.importNode(parse(artifact.extensions().get("xml").getBytes(StandardCharsets.UTF_8)).getDocumentElement(), true);
                if (!NS.equals(object.getNamespaceURI()) || !"SPEC-OBJECT".equals(object.getLocalName())) throw invalid("EVIDENCE_TYPE", "Incompatible requirement evidence");
                objects.appendChild(object);
            } else {
                object = (Element) doc.importNode(generatedObjects.get(artifact.id()), true);
                objects.appendChild(object);
                continue;
            }
            updateObject(object, artifact, ids);
        }
        Element specifications = child(core, "SPECIFICATIONS");
        if (specifications == null) specifications = append(core, NS, "SPECIFICATIONS");
        while (specifications.hasChildNodes()) specifications.removeChild(specifications.getFirstChild());
        for (Artifact artifact : source.artifacts()) if (artifact.kind() == ArtifactKind.SPECIFICATION) {
            Element specification;
            if (artifact.extensions().containsKey("xml")) {
                specification = (Element) doc.importNode(parse(artifact.extensions().get("xml").getBytes(StandardCharsets.UTF_8)).getDocumentElement(), true);
                if (!NS.equals(specification.getNamespaceURI()) || !"SPECIFICATION".equals(specification.getLocalName())) throw invalid("EVIDENCE_TYPE", "Incompatible specification evidence");
                specifications.appendChild(specification);
            } else {
                specification = definition(specifications, "SPECIFICATION", artifact.id());
                setRef(specification, "TYPE", "SPECIFICATION-TYPE-REF", artifact.type());
            }
        }
        for (Element specification : all(doc, NS, "SPECIFICATION")) {
            Artifact artifact = artifacts.get(required(specification, "IDENTIFIER"));
            if (artifact != null) {
                specification.setAttribute("LONG-NAME", artifact.title());
                applyValues(specification, artifact.attributes(), artifact.extensions(), ids);
            }
            Element old = child(specification, "CHILDREN"); if (old != null) specification.removeChild(old);
            List<Placement> roots = source.placements().stream().filter(p -> p.containerId().equals(specification.getAttribute("IDENTIFIER")) && p.parentId() == null)
                    .sorted(Comparator.comparingInt(Placement::position).thenComparing(Placement::id)).toList();
            if (!roots.isEmpty()) {
                Element children = append(specification, NS, "CHILDREN");
                for (Placement root : roots) writeHierarchy(children, root, source.placements(), new HashSet<>());
            }
        }
        Map<String, Relation> relations = new LinkedHashMap<>(); source.relations().forEach(r -> relations.put(r.id(), r));
        Element relationGroup = child(core, "SPEC-RELATIONS"); if (relationGroup == null) relationGroup = append(core, NS, "SPEC-RELATIONS");
        while (relationGroup.hasChildNodes()) relationGroup.removeChild(relationGroup.getFirstChild());
        for (Relation relation : source.relations()) {
            Element node;
            if (relation.extensions().containsKey("xml")) {
                node = (Element) doc.importNode(parse(relation.extensions().get("xml").getBytes(StandardCharsets.UTF_8)).getDocumentElement(), true);
                if (!NS.equals(node.getNamespaceURI()) || !"SPEC-RELATION".equals(node.getLocalName())) throw invalid("EVIDENCE_TYPE", "Incompatible relation evidence");
                relationGroup.appendChild(node);
            } else node = definition(relationGroup, "SPEC-RELATION", relation.id());
            setRef(node, "TYPE", "SPEC-RELATION-TYPE-REF", relation.type());
            setRef(node, "SOURCE", "SPEC-OBJECT-REF", relation.source());
            setRef(node, "TARGET", "SPEC-OBJECT-REF", relation.target());
            applyValues(node, relation.attributes(), relation.extensions(), ids);
        }
        // Re-establish the normative content-group order after optional groups were inserted.
        for (String name : List.of("DATATYPES", "SPEC-TYPES", "SPEC-OBJECTS", "SPEC-RELATIONS", "SPECIFICATIONS", "SPEC-RELATION-GROUPS")) {
            Element node = child(core, name); if (node != null) core.appendChild(node);
        }
        validate(doc, PROFILE); checkReferences(doc, identities(doc));
        return ExchangeXml.write(doc);
    }

    private static void updateObject(Element object, Artifact artifact, Map<String, Element> definitions) {
        String titleAttribute = artifact.extensions().get("titleAttribute");
        String textAttribute = artifact.extensions().get("textAttribute");
        Map<String, String> values = new LinkedHashMap<>(artifact.attributes());
        if (titleAttribute == null) object.setAttribute("LONG-NAME", artifact.title());
        else if (!artifact.title().equals(plain(values.get(titleAttribute), artifact.extensions().get("kind:" + titleAttribute))))
            values.put(titleAttribute, valueFor(artifact.title(), artifact.extensions().get("kind:" + titleAttribute)));
        if (textAttribute != null && !artifact.text().equals(plain(values.get(textAttribute), artifact.extensions().get("kind:" + textAttribute))))
            values.put(textAttribute, valueFor(artifact.text(), artifact.extensions().get("kind:" + textAttribute)));
        applyValues(object, values, artifact.extensions(), definitions);
    }

    private static void applyValues(Element object, Map<String, String> values, Map<String, String> extensions, Map<String, Element> definitions) {
        Element container = child(object, "VALUES");
        if (container == null && !values.isEmpty()) container = append(object, NS, "VALUES");
        Map<String, Element> existing = new LinkedHashMap<>();
        for (Element value : children(container)) existing.put(ref(value, "DEFINITION"), value);
        existing.forEach((id, value) -> { if (!values.containsKey(id)) value.getParentNode().removeChild(value); });
        for (var entry : values.entrySet()) {
            Element definition = definitions.get(entry.getKey());
            if (definition == null) throw invalid("MISSING_DEFINITION", "Attribute definition is not in the selected type system");
            String kind = definition.getLocalName().replace("ATTRIBUTE-DEFINITION-", "");
            Element value = existing.get(entry.getKey());
            if (value == null) {
                value = append(container, NS, "ATTRIBUTE-VALUE-" + kind);
                text(append(value, NS, "DEFINITION"), NS, "ATTRIBUTE-DEFINITION-" + kind + "-REF", entry.getKey());
            }
            if (kind.equals("XHTML")) {
                Element old = child(value, "THE-VALUE"); if (old != null) value.removeChild(old);
                Document fragment = parse(entry.getValue().getBytes(StandardCharsets.UTF_8));
                append(value, NS, "THE-VALUE").appendChild(value.getOwnerDocument().importNode(fragment.getDocumentElement(), true));
            } else if (kind.equals("ENUMERATION")) {
                Element old = child(value, "VALUES"); if (old != null) value.removeChild(old);
                Element selected = append(value, NS, "VALUES");
                for (String id : entry.getValue().split("\n")) if (!id.isBlank()) text(selected, NS, "ENUM-VALUE-REF", id);
            } else value.setAttribute("THE-VALUE", entry.getValue());
        }
    }

    private record Fields(Map<String, String> values, Map<String, String> extensions) {}
    private static Fields fields(Element object, Map<String, Element> ids) {
        Map<String, String> values = new LinkedHashMap<>(); Map<String, String> extensions = new LinkedHashMap<>();
        Element type = ids.get(ref(object, "TYPE"));
        if (type == null) throw invalid("TYPE_REFERENCE", "ReqIF type reference is missing");
        for (Element definition : children(child(type, "SPEC-ATTRIBUTES"))) {
            String id = required(definition, "IDENTIFIER");
            extensions.put("definition:" + id, definition.getAttribute("LONG-NAME"));
            extensions.put("kind:" + id, definition.getLocalName().replace("ATTRIBUTE-DEFINITION-", ""));
            extensions.put("datatype:" + id, ref(definition, "TYPE"));
            List<Element> defaults = children(child(definition, "DEFAULT-VALUE"));
            if (!defaults.isEmpty()) values.put(id, value(defaults.getFirst()));
        }
        for (Element value : children(child(object, "VALUES"))) {
            String id = ref(value, "DEFINITION");
            if (!extensions.containsKey("kind:" + id)) throw invalid("ATTRIBUTE_TYPE", "Attribute does not belong to the object's selected type");
            values.put(id, value(value));
        }
        return new Fields(values, extensions);
    }

    private static String value(Element value) {
        if (value.getLocalName().equals("ATTRIBUTE-VALUE-XHTML")) {
            List<Element> content = children(child(value, "THE-VALUE"));
            return content.isEmpty() ? "<div xmlns=\"" + XHTML + "\"/>" : xml(content.getFirst());
        }
        if (value.getLocalName().equals("ATTRIBUTE-VALUE-ENUMERATION"))
            return String.join("\n", children(child(value, "VALUES")).stream().map(Node::getTextContent).sorted().toList());
        return value.getAttribute("THE-VALUE");
    }

    private static String findField(Fields fields, List<String> names) {
        for (String name : names) for (var entry : fields.extensions.entrySet())
            if (entry.getKey().startsWith("definition:") && entry.getValue().equalsIgnoreCase(name)) return entry.getKey().substring(11);
        return null;
    }
    private static String plain(String value, String kind) {
        if (value == null) return "";
        return "XHTML".equals(kind) ? parse(value.getBytes(StandardCharsets.UTF_8)).getDocumentElement().getTextContent() : value;
    }
    private static String valueFor(String value, String kind) {
        if (!"XHTML".equals(kind)) return value;
        Document doc = parse(("<div xmlns=\"" + XHTML + "\"/>").getBytes(StandardCharsets.UTF_8));
        text(doc.getDocumentElement(), XHTML, "p", value); return xml(doc.getDocumentElement());
    }
    private static String ref(Element parent, String wrapper) {
        List<Element> references = children(child(parent, wrapper));
        return references.isEmpty() ? "" : references.getFirst().getTextContent().strip();
    }
    private static void setRef(Element parent, String wrapper, String tag, String id) {
        Element node = child(parent, wrapper);
        if (node == null) node = append(parent, NS, wrapper); else while (node.hasChildNodes()) node.removeChild(node.getFirstChild());
        text(node, NS, tag, id);
    }
    private static Map<String, Element> identities(Document doc) {
        Map<String, Element> result = new LinkedHashMap<>();
        for (Element element : all(doc, NS, "*")) if (element.hasAttribute("IDENTIFIER") && !element.getLocalName().equals("ALTERNATIVE-ID")) {
            if (result.putIfAbsent(required(element, "IDENTIFIER"), element) != null)
                throw invalid("DUPLICATE_IDENTITY", "ReqIF reuses a stable identity");
        }
        return result;
    }
    private static void checkReferences(Document doc, Map<String, Element> ids) {
        for (Element element : all(doc, NS, "*")) if (element.getLocalName().endsWith("-REF")) {
            Element target = ids.get(element.getTextContent().strip());
            String type = element.getLocalName().substring(0, element.getLocalName().length() - 4);
            if (target == null || !target.getLocalName().equals(type)) throw invalid("DANGLING_REFERENCE", "ReqIF contains a missing or incompatible typed reference");
        }
    }
    private static void hierarchy(Element children, String specification, String parent, List<Placement> result) {
        int position = 0;
        for (Element node : children(children)) {
            String id = required(node, "IDENTIFIER"); Map<String, String> attributes = new LinkedHashMap<>();
            for (String name : List.of("LAST-CHANGE", "LONG-NAME", "DESC", "IS-EDITABLE", "IS-TABLE-INTERNAL"))
                if (node.hasAttribute(name)) attributes.put(name, node.getAttribute(name));
            Element editable = child(node, "EDITABLE-ATTS"); if (editable != null) attributes.put("editableXml", xml(editable));
            result.add(new Placement(id, specification, parent, ref(node, "OBJECT"), position++, attributes));
            hierarchy(child(node, "CHILDREN"), specification, id, result);
        }
    }
    private static void writeHierarchy(Element parent, Placement placement, List<Placement> all, Set<String> visiting) {
        if (!visiting.add(placement.id()) || visiting.size() > 90) throw invalid("HIERARCHY_CYCLE", "Invalid requirement hierarchy");
        Element node = append(parent, NS, "SPEC-HIERARCHY"); node.setAttribute("IDENTIFIER", placement.id());
        node.setAttribute("LAST-CHANGE", placement.attributes().getOrDefault("LAST-CHANGE", FIXED_TIME));
        for (String name : List.of("LONG-NAME", "DESC", "IS-EDITABLE", "IS-TABLE-INTERNAL"))
            if (placement.attributes().containsKey(name)) node.setAttribute(name, placement.attributes().get(name));
        setRef(node, "OBJECT", "SPEC-OBJECT-REF", placement.artifactId());
        String editable = placement.attributes().get("editableXml");
        if (editable != null) node.appendChild(node.getOwnerDocument().importNode(parse(editable.getBytes(StandardCharsets.UTF_8)).getDocumentElement(), true));
        List<Placement> descendants = all.stream().filter(p -> placement.id().equals(p.parentId()))
                .sorted(Comparator.comparingInt(Placement::position).thenComparing(Placement::id)).toList();
        if (!descendants.isEmpty()) {
            Element group = append(node, NS, "CHILDREN");
            for (Placement child : descendants) writeHierarchy(group, child, all, visiting);
        }
        visiting.remove(placement.id());
    }

    /** Deterministic initial profile for a project which has never been exchanged with this connection. */
    private static Document generated(ExchangeDocument source) {
        Document doc = parse(("<REQ-IF xmlns=\"" + NS + "\"/>").getBytes(StandardCharsets.UTF_8));
        Element root = doc.getDocumentElement();
        Element header = append(append(root, NS, "THE-HEADER"), NS, "REQ-IF-HEADER"); header.setAttribute("IDENTIFIER", "taxonomy-header");
        text(header, NS, "CREATION-TIME", FIXED_TIME); text(header, NS, "REQ-IF-TOOL-ID", "Taxonomy ReqIF 1.2 profile 1");
        text(header, NS, "REQ-IF-VERSION", "1.0"); text(header, NS, "SOURCE-TOOL-ID", "Taxonomy"); text(header, NS, "TITLE", source.metadata().getOrDefault("title", "Requirements"));
        Element content = append(append(root, NS, "CORE-CONTENT"), NS, "REQ-IF-CONTENT");
        Element datatypes = append(content, NS, "DATATYPES");
        definition(datatypes, "DATATYPE-DEFINITION-STRING", "taxonomy-string").setAttribute("MAX-LENGTH", "100000");
        definition(datatypes, "DATATYPE-DEFINITION-XHTML", "taxonomy-xhtml");
        Element types = append(content, NS, "SPEC-TYPES"); Element objectType = definition(types, "SPEC-OBJECT-TYPE", "taxonomy-object");
        Element definitions = append(objectType, NS, "SPEC-ATTRIBUTES");
        Element title = definition(definitions, "ATTRIBUTE-DEFINITION-STRING", "taxonomy-title"); title.setAttribute("LONG-NAME", "ReqIF.ChapterName");
        setRef(title, "TYPE", "DATATYPE-DEFINITION-STRING-REF", "taxonomy-string");
        Element body = definition(definitions, "ATTRIBUTE-DEFINITION-XHTML", "taxonomy-text"); body.setAttribute("LONG-NAME", "ReqIF.Text");
        setRef(body, "TYPE", "DATATYPE-DEFINITION-XHTML-REF", "taxonomy-xhtml");
        definition(types, "SPECIFICATION-TYPE", "taxonomy-specification-type");
        definition(types, "SPEC-RELATION-TYPE", "taxonomy-relation-type");
        Element objects = append(content, NS, "SPEC-OBJECTS");
        for (Artifact artifact : source.artifacts()) if (artifact.kind() == ArtifactKind.REQUIREMENT) {
            Element object = definition(objects, "SPEC-OBJECT", artifact.id()); object.setAttribute("LONG-NAME", artifact.title());
            setRef(object, "TYPE", "SPEC-OBJECT-TYPE-REF", "taxonomy-object");
            Element values = append(object, NS, "VALUES");
            Element nameValue = append(values, NS, "ATTRIBUTE-VALUE-STRING"); nameValue.setAttribute("THE-VALUE", artifact.title());
            setRef(nameValue, "DEFINITION", "ATTRIBUTE-DEFINITION-STRING-REF", "taxonomy-title");
            Element textValue = append(values, NS, "ATTRIBUTE-VALUE-XHTML");
            setRef(textValue, "DEFINITION", "ATTRIBUTE-DEFINITION-XHTML-REF", "taxonomy-text");
            Element value = append(textValue, NS, "THE-VALUE"); text(append(value, XHTML, "div"), XHTML, "p", artifact.text());
        }
        Element relations = append(content, NS, "SPEC-RELATIONS");
        for (Relation relation : source.relations()) {
            Element node = definition(relations, "SPEC-RELATION", relation.id());
            setRef(node, "TYPE", "SPEC-RELATION-TYPE-REF", "taxonomy-relation-type");
            setRef(node, "SOURCE", "SPEC-OBJECT-REF", relation.source()); setRef(node, "TARGET", "SPEC-OBJECT-REF", relation.target());
        }
        Element specs = append(content, NS, "SPECIFICATIONS");
        List<Artifact> specifications = source.artifacts().stream().filter(a -> a.kind() == ArtifactKind.SPECIFICATION).toList();
        if (specifications.isEmpty()) specifications = List.of(new Artifact("taxonomy-specification", ArtifactKind.SPECIFICATION, "taxonomy-specification-type", "Requirements", "", Map.of(), Map.of()));
        for (Artifact specification : specifications) {
            Element node = definition(specs, "SPECIFICATION", specification.id()); node.setAttribute("LONG-NAME", specification.title());
            setRef(node, "TYPE", "SPECIFICATION-TYPE-REF", "taxonomy-specification-type");
        }
        return doc;
    }
    private static Element definition(Element parent, String type, String id) {
        Element element = append(parent, NS, type); element.setAttribute("IDENTIFIER", id); element.setAttribute("LAST-CHANGE", FIXED_TIME); return element;
    }
    private static MappingLoss loss(String id, String field, String code, LossDisposition disposition, String detail) { return new MappingLoss(id, field, code, disposition, detail); }
    public static String digest(byte[] content) {
        try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(content)); }
        catch (java.security.NoSuchAlgorithmException e) { throw new IllegalStateException(e); }
    }
}