# XML Utility — Issue Catalogue

Analysed source tree: `src/` (parser, validator, schema, transform, pipeline,
namespace, errors, cache, cli, codegen, xsd, io, plugins, utils).

---

## P0 — Critical Bugs / Crashes / Security Vulnerabilities

### P0-18 · `Buffer.isBuffer` Called in Browser/Deno/Bun Environments — Crash
**File:** `src/parser/XmlParser.ts`, line 132  
`Buffer.isBuffer(input as unknown)` is called unconditionally on every `parse()` invocation. `Buffer` is a Node.js-only global. In browsers, Deno, and Bun (without the Node compat layer), this throws `ReferenceError: Buffer is not defined` immediately, making the parser completely unusable in those runtimes despite `src/browser.ts`, `src/deno.ts`, and `src/bun.ts` being exported entry points.

### P0-19 · `Buffer.byteLength` Called in `ParseBudget` — Same Cross-Runtime Crash
**File:** `src/utils/ParseBudget.ts`, line 48  
`Buffer.byteLength(input, 'utf8')` used for byte-limit check. Same Node-only API issue as P0-01. `ParseBudget` is exported and documented for general use; calling it in a non-Node runtime crashes immediately.

### P0-20 · `require('path')` Inside `SchemaCache.invalidate` — Dynamic `require` Crash in ESM
**File:** `src/cache/SchemaCache.ts`, lines 330–338  
`require('path').resolve(key)` is called inside an otherwise-async class method. In pure ESM environments (which the package also ships as), `require` is not defined, causing a `ReferenceError`. `resolve` is already imported at the top of the file; this residual `require` is unreachable dead code in some builds but fatal in others.

### P0-21 · `XmlDocument.appendChild` Casts Parent to `XmlElement` — Type Corruption
**File:** `src/parser/XmlNodes.ts`, line 318  
`node.parent = this as unknown as XmlElement;` — An `XmlDocument` is cast as an `XmlElement` and assigned to `node.parent`. Downstream code that traverses `.parent` and assumes it is an `XmlElement` (e.g. calling `.tagName`, `.attributes`, `.childElements`) will receive a document node and crash with "Cannot read properties of undefined".

### P0-22 · Entity Expansion Limit Applied AFTER String Construction — OOM Risk
**File:** `src/utils/xmlUtils.ts`, lines 37–62  
`decodeEntities` builds the fully-expanded result string via `String.replace`, then checks `result.length > MAX_EXPANSION` after the fact. A Billion Laughs attack can produce a result string that exhausts heap memory before the guard is ever reached. The check must be applied incrementally or the input must be pre-scanned.

### P0-23 · `_parseDtdEntities` Regex Does Not Prevent Recursive Entity Expansion
**File:** `src/parser/XmlParser.ts`, lines 295–321  
Entity values extracted from the DTD internal subset are stored in `opts.entityMap` and then passed to `decodeEntities`. If an entity value itself contains `&anotherEntity;` references, those are expanded by the same `decodeEntities` call during text parsing, enabling multi-level expansion. The code comment says "Nested entity references within values are NOT expanded (no recursion)" but the runtime flow does expand them, creating a Quadratic/Billion-Laughs risk.

### P0-24 · `consumeUntilStr` Returns All Remaining Input When `stop` Is Not Found
**File:** `src/utils/StringReader.ts`, lines 99–106  
When `stop` is not present in the remaining input, `indexOf` returns `-1`, and `end` is set to `this._input.length`, consuming the entire rest of the document silently. For malformed XML (e.g. an unclosed comment `<!--`), this causes the parser to silently consume everything without throwing an error, resulting in corrupted parse state and a wrong/missing AST rather than a parse error.

### P0-25 · `SaxParser.on()` Double-Initialisation Bug — First Handler Dropped
**File:** `src/parser/SaxParser.ts`, lines 130–134  
```typescript
if (!this.handlers.has(type)) this.handlers.set(type, []);
const list = this.handlers.get(type) ?? [];
if (!this.handlers.has(type)) this.handlers.set(type, list);
```
The second `if (!this.handlers.has(type))` is always `false` at that point (the first block already set it). The `list` variable could be the locally-created `[]` that was never stored when the first `get` returned an existing array. This logic is confusing and has an edge-case where a handler could be lost. The code structure misleads maintainers into thinking double-registration is guarded.

### P0-26 · `XmlStreamParser` Silently Drops Errors After `_flush`
**File:** `src/io/XmlStreamParser.ts`, lines 57–69  
`_flush` calls `callback(err)` on parse failure, which passes the error to the Transform stream machinery. However, the `parseXmlStream` wrapper only listens to `'error'` on the `parser` object (the Transform). If a consumer pipes directly without error handling and the underlying `source` stream has already ended when the error fires, the error event may go unhandled and crash the Node.js process.

### P0-27 · `validateStream` Accumulates All Chunks in Memory — Not True Streaming
**File:** `src/validator/StreamingValidator.ts`, lines 712–732  
`validateStream(stream, ...)` concatenates all chunks into a `Buffer[]` array before parsing. For a file larger than available RAM this causes an OOM crash. The function is documented as "Validate a Node.js Readable stream" implying streaming behaviour, but it is full buffering. This is a correctness/safety issue because callers relying on the streaming contract for large files will OOM.

### P0-28 · `ParseBudget.parse` Wall-Time Check Is Post-Parse — Does Not Abort In-Progress Parse
**File:** `src/utils/ParseBudget.ts`, lines 66–73  
The wall-time violation is checked only after `parseXml` returns. Since the parser is synchronous and single-threaded, a pathological input that takes minutes will never be interrupted. The guard is illusory; users setting `wallTimeMs: 5000` will see the process block for the full parse duration, then (ironically) receive a timeout error even though parsing already completed.

### P0-29 · `decodeEntities` Uses `String.fromCodePoint` With Unrestricted Code Points — Possible Invalid Characters
**File:** `src/utils/xmlUtils.ts`, lines 41–46  
Numeric character references like `&#x0;` (NUL), `&#xD800;`–`&#xDFFF;` (surrogates), and `&#xFFFE;`/`&#xFFFF;` (non-characters) are converted via `String.fromCodePoint` without validation. XML 1.0 §2.2 forbids these code points. Inserting a NUL byte into an element's text content can cause downstream C-string processing bugs or security issues in consumers.

### P0-30 · XSD `xs:import` Without `schemaLocation` Silently Skipped — Schema Incomplete
**File:** `src/xsd/XsdParser.ts`, line 184  
`if (!loc) return;` — When `xs:import` specifies only a `namespace` (valid per XSD spec; the schema processor should use a pre-registered schema), the import is silently ignored. This means schemas that rely on namespace-only imports are silently incomplete, leading to false-positive "element not found" errors at validation time with no indication that the schema was partially loaded.

### P0-31 · `ValidationEngine._addError` Uses Wrong Error Code for Node-Count Limit
**File:** `src/parser/XmlParser.ts`, line 336 and `src/parser/XmlLexer.ts`, line 293  
Both `XmlParser` and `XmlLexer` throw `secError('SEC_MAX_TEXT_LENGTH', ...)` when the node count exceeds `maxNodeCount`. The correct code is `SEC_MAX_NODE_COUNT` (or at minimum a distinct code). Callers filtering on `SEC_MAX_TEXT_LENGTH` to handle text-size errors will incorrectly classify node-count exhaustion as a text error, masking the true cause.

### P0-32 · `XmlParser` Resets `_XML_NAME_RE.lastIndex` to 0 Before `consumePattern` — Race Condition in Concurrent Use
**File:** `src/parser/XmlParser.ts`, lines 559–561  
`_XML_NAME_RE` is a **module-level** sticky regex. `parseName` resets `lastIndex = 0` before calling `consumePattern`, which then sets `lastIndex = this._pos`. If two `XmlParser` instances are used concurrently (e.g. in a worker-thread pool without isolation), they share the module-level regex and can corrupt each other's `lastIndex`. `XmlLexer.ts` line 306 has the same issue.

---

## P1 — High Severity Bugs

### P1-11 · `childElements` Getter Iterates All Children on Every Call — O(n) Hot Path
**File:** `src/parser/XmlNodes.ts`, lines 200–202  
`get childElements()` performs a full `.filter()` over `this.children` on every invocation. Every call to `_validateElement`, `_validateAll`, `_validateSequence`, and content-model matching re-invokes this getter repeatedly, creating O(n×depth) work.

### P1-12 · `textContent` Getter Iterates and Re-Joins Children on Every Call
**File:** `src/parser/XmlNodes.ts`, lines 205–210  
`get textContent()` filters and maps `this.children` every call. Called once per element in validation (simple-type check) plus once per text-content assertion. For elements with many mixed-content children this is O(n) per access.

### P1-13 · `ValidationEngine._validateAttributes` Falls Back to Linear Attribute Scan
**File:** `src/validator/ValidationEngine.ts`, lines 499–500  
```typescript
const attrNode = elem.attributes.get(attrDecl.name)
  ?? [...elem.attributes.values()].find(a => a.localName === attrDecl.name);
```
On a cache miss the code spreads all attributes into an array and calls `.find()`. For an element with declared attributes this is O(declared × element-attrs). For large documents with wide elements this is a hot-path regression.

### P1-14 · `StreamingValidator._validateAttributes` Reports Unknown Attributes as Warnings in `strict` Mode
**File:** `src/validator/StreamingValidator.ts`, lines 393–398  
In `strict` mode, an undeclared attribute is added as `severity: 'warning'`, not `'error'`. The equivalent logic in `ValidationEngine` (line 563) correctly emits an error in strict mode. This inconsistency means streaming validation is more lenient than DOM validation for the same document/schema combination.

### P1-15 · `_mergeComplexTypeChain` Memoization Only at `depth === 0` — Sub-Chain Never Cached
**File:** `src/validator/ValidationEngine.ts`, lines 1057–1060  
The cache is written only when `depth === 0`. Sub-types encountered via recursion at `depth > 0` are recomputed on every top-level call that encounters the same sub-type through a different root. For schemas with diamond inheritance (multiple types extending the same base), the base-chain merge is recomputed for each derived type.

### P1-16 · `NsStack` in `SaxParser` `_run()` Is Never Reset Between `parse()` Calls
**File:** `src/parser/SaxParser.ts`, lines 120–121  
`this.ns = new NsStack()` is only set in the constructor. If a `SaxParser` instance is reused for a second `parse()` call (e.g. via `setInput` or direct re-call), the namespace stack from the previous run leaks into the new parse, causing incorrect namespace resolution.

### P1-17 · `XmlDiff` FNV-32 Hash Has High Collision Rate for Large Trees
**File:** `src/utils/XmlDiff.ts`, lines 30–37 and 67  
A 32-bit FNV hash is used as a structural fingerprint for entire subtrees. With 2³² ≈ 4 billion possible values, two structurally different subtrees have a ~1:4B chance of collision per pair, but for documents with millions of nodes the birthday paradox makes false-equal judgements likely (~50% probability around 65k unique subtrees). A false-equal fingerprint causes `diffXml` to report no change when one exists — a silent correctness error.

### P1-18 · `BatchValidator._runBatch` Does Not Propagate `failFast` Abort Atomically
**File:** `src/validator/BatchValidator.ts`, lines 181–201  
`aborted` is set to `true` inside an async callback, but `Promise.all` already has all batch promises in flight. The `if (aborted) return;` guard in iterating items from the next batch is checked only at the start of the next iteration block, meaning all items in the **current** `Promise.all` batch run to completion even after `failFast` triggers.

### P1-19 · `XPathEngine` `//` Descendant Axis Does Not Reset Position Counter
**File:** `src/parser/XPathEngine.ts` (descendant axis evaluation)  
The `//` (descendant-or-self) axis builds a flat list of all matching descendants but does not update position counters correctly for positional predicates like `//item[2]`. This returns the second global descendant named `item`, not the second child of each `//item` context node as XPath 1.0 specifies.

### P1-20 · `StringReader.consumeWhile` Does Not Update Line/Col Tracking
**File:** `src/utils/StringReader.ts`, lines 60–68  
`consumeWhile` advances `_pos` character by character but only calls `_advanceTracking(slice)` **after** the loop. If a predicate-based scan hits a newline, `_line`/`_col` are only updated at the end of the consumed region, meaning any error thrown **during** `consumeWhile` (e.g. in a custom predicate) would have stale line/col.

### P1-21 · `SchemaCache.getOrLoad` with `statFast` Does Two `stat` Calls Per Miss
**File:** `src/cache/SchemaCache.ts`, lines 149–213  
When `statFast: true` and the entry is not in cache, the code calls `stat(key)` at line 151 for the fast-path check, then calls `stat(key)` again at line 196 to persist the fingerprint. Two `stat` syscalls per cache miss doubles filesystem overhead for cold starts.

### P1-22 · `ValidationPipeline` Stage 5 (type-validate) Is a No-Op with Misleading Stage Record
**File:** `src/pipeline/ValidationPipeline.ts`, lines 222–228  
The `'type-validate'` stage is recorded as a separate pipeline stage with its own `StageResult`, but the stage body is a documented `// no-op`. Consumers of `PipelineResult.stages` who filter by `stage === 'type-validate'` expecting type-only errors will see an empty `issues` array because all type errors are already merged into `pr.validation` from Stage 4.

### P1-23 · `XsltTransformer` Does Not Support `xsl:call-template` — Silent Empty Output
**File:** `src/transform/XsltTransformer.ts`  
`xsl:call-template` is not implemented (documented limitation). However, many real-world stylesheets use `xsl:call-template` extensively. When encountered, the instruction is silently skipped (treated as an unknown literal element), producing empty or wrong output without any warning or error. A "not supported" error would be more appropriate.

### P1-24 · `XmlLexer` Does Not Track `lastIndex` Reset for Module-Level `_XML_NAME_RE`
**File:** `src/parser/XmlLexer.ts`, line 306  
Unlike `XmlParser.parseName()` (which explicitly resets `lastIndex = 0`), `XmlLexer._lexName()` calls `this.r.consumePattern(_XML_NAME_RE)` without resetting `_XML_NAME_RE.lastIndex`. The code depends on `consumePattern` executing immediately with no interleave. A defensive `_XML_NAME_RE.lastIndex = 0` is absent, making the code fragile.

### P1-25 · `IdentityConstraintEngine` Identity Checks Are Run Twice When Using `ValidationPipeline`
**File:** `src/pipeline/ValidationPipeline.ts`, lines 229–236 and `src/validator/ValidationEngine.ts`, lines 350–358  
`ValidationEngine.validate()` already invokes `_checkIdentityConstraint` for every element. The pipeline then **also** runs `IdentityConstraintEngine.evaluate(doc)` in Stage 6. Identity constraint violations are added to `pr.validation` twice, resulting in duplicate error messages in the final `ValidationResult`.

### P1-26 · `validateFragment` Wraps `parseXml` in a Redundant Destructuring
**File:** `src/validator/ValidationEngine.ts`, line 1204  
`const { parseXml } = { parseXml: _parseXml };` — This is a no-op destructuring left over from a refactor. It adds a needless object allocation per call.

### P1-27 · `DfaEngine.buildDfa` For Nested `xs:all` Groups Is Not Properly Handled
**File:** `src/validator/DfaEngine.ts`  
The DFA builder handles `sequence`, `choice`, and `all` at the top level. When an `xs:all` group is nested inside an outer `xs:sequence` or `xs:choice` (valid in XSD 1.1), `normalise()` recursively builds a nested `DfaModel`, but `runDfa` for `all` compositor uses the `particleIndex` map built only for flat-top particles. Nested `all` particles are matched via the fallback `_runAll` path but their occurrence tracking is not propagated to the parent runner correctly.

### P1-28 · `SaxParser.events()` Generator Wraps Entire `_run()` in Try/Catch — Swallows Generator Protocol Errors
**File:** `src/parser/SaxParser.ts`, lines 149–160  
The `try/catch` around `yield* this._run()` converts any `Error` thrown from within `_run` into a `{ type: 'error', error: xe }` event and then `return`s. After returning, the generator is exhausted. If a consumer's `for...of` loop encounters this synthetic error event and does not `break`, the `endDocument` event is never yielded, breaking consumers that rely on `endDocument` to finalize state.

### P1-29 · `XmlToJson` `coerceNumbers` Coerces `"NaN"` — Corrupted JSON
**File:** `src/transform/XmlToJson.ts` (number coercion logic)  
When `coerceNumbers: true`, the text value `"NaN"` is passed to `Number("NaN")` which returns `NaN` — a value that does not round-trip through `JSON.stringify` (it becomes `null`). Similarly, `-0` becomes `0`. Consumers expecting faithful JSON serialisation of their original values receive corrupted data.

### P1-30 · `_evalField` in `ValidationEngine` Does Not Handle Multi-Level `child::` XPath Paths
**File:** `src/validator/ValidationEngine.ts`, lines 1026–1031  
`_evalField` supports only `.` (text content), `@attr` (attribute), and single bare child names. XSD identity constraints with multi-step field paths like `child::a/child::b` or `./item/price` silently return `null`, making all tuples for such constraints `\0MISSING` and suppressing keyref validation without error.

### P1-31 · `SaxParser` Does Not Check for Unclosed Tags at `EOF`
**File:** `src/parser/SaxParser.ts`, lines 316–318  
When `EOF` is reached and `tagStack` is non-empty, no error is emitted. The generator simply returns. Consumers receive a valid-looking `endDocument` event for a document with unclosed elements, silently masking well-formedness violations.

### P1-32 · `StreamingValidator` Does Not Validate `xsi:nil` Behaviour
**File:** `src/validator/StreamingValidator.ts`  
`xsi:nil="true"` is checked by `ValidationEngine` (DOM path) but `StreamingValidator` has no handling for `xsi:nil`. A nillable element with `xsi:nil="true"` and child content should error; with non-nillable declaration it should also error. Neither check exists in the streaming path.

### P1-33 · `XmlParser` Does Not Validate XML 1.0 Character Range
**File:** `src/parser/XmlParser.ts`  
XML 1.0 §2.2 restricts allowed characters to `#x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]`. The parser accepts and stores any JavaScript string character including control characters `\x01`–`\x08`, `\x0B`, `\x0C`, `\x0E`–`\x1F`. These characters are illegal in XML 1.0 and should produce a parse error.

### P1-34 · `_handleRedefine` Does Not Apply Override Correctly — Merged and Redefined
**File:** `src/xsd/XsdParser.ts`, lines 208–238  
`_handleRedefine` merges the sub-schema first (`this.schema.merge(subSchema)`) then applies overrides. But `xs:redefine` semantics require the redefined types to **reference themselves** (self-reference in the restriction/extension). The current code simply replaces the type rather than implementing this self-reference, so complex `xs:redefine` redefinitions silently produce incorrect schemas.

### P1-35 · `PluginRegistry.runSchemaValidators` Is Not Called by Any Public API
**File:** `src/plugins/PluginRegistry.ts`  
`runSchemaValidators(doc, schema, result)` is defined but no built-in API (`validate`, `ValidationPipeline.run`, `BatchValidator`) calls it. Plugin-registered schema validators are silently never executed unless callers wire the call manually, which is not documented and not obvious.

### P1-36 · `ValidationEngine._assertElement` Constructs a New `AssertionEvaluator` Per Assertion Per Element
**File:** `src/validator/ValidationEngine.ts`, line 483  
`new AssertionEvaluator(elem)` is invoked for every assertion on every element in the document. The `AssertionEvaluator` constructor likely pre-computes some element state. In documents with many elements that have `xs:assert`, this creates significant GC pressure.

### P1-37 · `XmlCanonical` Not Listed in `src/index.ts` Exports
**File:** `src/utils/XmlCanonical.ts`, `src/index.ts`  
`XmlCanonical` is present in `src/utils/` but may not be exported from `src/index.ts`. If missing from exports it is dead code that users cannot access despite documentation references.

### P1-38 · CLI `--watch` Mode Watches Files Using Node `fs.watch` — No Debounce
**File:** `src/cli/cli.ts`, line 41 (`watch as fsWatch`)  
`fs.watch` on macOS fires multiple events per single save (once for metadata, once for content). Without debouncing, `--watch` mode re-validates 2–3 times per file save, flooding output.

### P1-39 · `XsdParser` Does Not Detect Circular Type References — Stack Overflow Risk
**File:** `src/xsd/XsdParser.ts`  
`_parseComplexType` calls `_parseParticle` which calls `_parseGroup` which can reference named groups. If an XSD defines a circular group reference (`groupA` references `groupB` which references `groupA`), the parser recurses infinitely until a stack overflow. The `_mergeComplexTypeChain` has a `depth > 20` guard but that is post-parse; the parser itself has no cycle detection.

### P1-40 · `ValidationResult.errors` / `warnings` Getters Are O(n) Per Call
**File:** `src/validator/ValidationResult.ts`, lines 81–91  
`get errors()` and `get warnings()` each call `this.issues.filter(...)` every invocation. In batch validation or pipeline consumers that call these getters multiple times (e.g. `result.errors.length`, then `result.errors.forEach`), the list is re-filtered twice per access. Cached counts (`_errorCount`, `_warningCount`) are tracked but the getters still do full filter instead of returning pre-computed lists.

---

## Performance Issues

### NPERF-21 · `XmlParser` Input Normalisation Creates a Full String Copy on Every Parse
**File:** `src/parser/XmlParser.ts`, line 142  
`input.replace(/\r\n/g, '\n').replace(/\r/g, '\n')` — Two chained regex replacements each allocate and return a new string. For a 50 MB XML file this allocates ~100 MB of temporary strings. A single-pass normalisation (one replacement) or an in-place scan would halve or eliminate this allocation.

### NPERF-22 · `childElements` and `textContent` Getters Re-Filter on Every Access
**File:** `src/parser/XmlNodes.ts`, lines 200–210  
As identified in P1-01/P1-02, these getters do O(n) work on every call. The structural DOM is immutable once parsed, so these results could be computed once and cached as `_childElementsCache` and `_textContentCache` at construction time or lazily on first access.

### NPERF-23 · `sha256Short` Computes Full SHA-256 Even Though Only 16 Characters Are Used
**File:** `src/utils/sha256.ts`, lines 161–163  
`sha256Short` computes the full 32-byte SHA-256 hash via `sha256Hex`, then slices the first 16 hex characters. The remaining 48 characters of hex output are computed and immediately discarded. A cheaper 64-bit hash (FNV-1a, xxHash, or SipHash) would be equally collision-resistant for cache keys and 10–20× faster.

### NPERF-24 · `_advanceTracking` Re-Scans Every Consumed Character for Newlines
**File:** `src/utils/StringReader.ts`, lines 126–131  
Every `consume`, `consumeUntilStr`, `skipWhitespace`, and `expect` call triggers `_advanceTracking(slice)` which iterates `slice.length` characters again to update line/col. For large text nodes (e.g. a 1 MB text node) the tracking loop runs 1 million iterations just to count newlines. An `indexOf('\n', pos)` loop would be more efficient.

### NPERF-25 · `ValidationEngine._validateAll` Rebuilds `seen` Map on Every `xs:all` Element
**File:** `src/validator/ValidationEngine.ts`, lines 718–724  
For every validated element with an `xs:all` content model, a fresh `Map<string, XmlElement[]>` is built by iterating all children. For documents with many identical schema types this Map is rebuilt identically per instance. The children list cannot easily be cached but the Map construction could at minimum be pooled.

### NPERF-26 · `StreamingKeyrefTracker.onElement` Calls `Array.find` Per Element For Keyrefs
**File:** `src/validator/StreamingValidator.ts`, lines 638–641  
`this._keyrefs.find(e => e.constraint.name === c.name)` is O(#keyrefs) per element. For schemas with many keyref constraints this is O(elements × keyrefs). An index `Map<string, entry>` (as done for `_constraintsByElement`) would make this O(1).

### NPERF-27 · `XsltTransformer._applyTemplates` Scans All Templates Linearly Per Node
**File:** `src/transform/XsltTransformer.ts`  
Template matching uses a linear search through all registered templates for every node in the source document. For a stylesheet with many templates and a large source document this is O(nodes × templates). A priority-indexed or hash-keyed dispatch table would reduce template lookup to O(1) for element name matches.

### NPERF-28 · `XmlElement.toString()` Allocates Multiple Intermediate Arrays and Strings
**File:** `src/parser/XmlNodes.ts`, lines 256–295  
`toString` for each element allocates: `[...this.attributes.values()].map(...)` (new array), `.join(' ')` (new string), `this.children.map(...)` (new array), `.join('\n')` (new string). For serialising a large document these intermediate allocations create significant GC pressure. A single-pass `StringBuilder`-style approach with array pre-allocation would be more efficient.

### NPERF-29 · Module-Level `_patternCache` in `TypeValidator` Is a Shared Global — No Isolation
**File:** `src/validator/TypeValidator.ts`, lines 126–138  
`_patternCache` is module-level. While it has an LRU-lite eviction at 512 entries, it is shared across all `TypeValidator` instances and all threads/workers. In a server that processes many schemas concurrently, stale entries from deleted schemas cannot be selectively evicted.

### NPERF-30 · `ValidationResult.byCategory()` Iterates All Issues Every Call
**File:** `src/validator/ValidationResult.ts`, line 95  
`byCategory` filters `this.issues` on every call. In IDE integrations that query by category repeatedly (e.g. per-keystroke), this is O(n) per call. A `Map<category, issues[]>` maintained alongside `issues` would make this O(1).

### NPERF-31 · `XmlParser.parseName()` Validates First Character With a Slow Regex `.test()` Per Call
**File:** `src/parser/XmlParser.ts`, line 555  
The first-character validation is performed via regex `.test(first)` on every XML name parse. A fast char-code comparison for the ASCII case (the vast majority) would be significantly faster than regex dispatch.

### NPERF-32 · `_checkUniqueTuples` Uses `JSON.stringify` for Tuple Keys
**File:** `src/validator/ValidationEngine.ts`, lines 969–980  
`JSON.stringify(tuple)` is called for every tuple in an identity constraint set. JSON serialisation is slow and allocates strings. A simple `tuple.join('\x00')` (already used in `StreamingKeyrefTracker`) would be much faster and avoids JSON overhead.

### NPERF-33 · `XmlDiff._fingerprint` Calls `[...el.attributes.entries()]` — Spreads Attribute Map
**File:** `src/utils/XmlDiff.ts`, line 50  
`[...el.attributes.entries()]` creates a new array of `[key, value]` pairs per element per fingerprint computation. For documents with many attributes, spreading the `Map` iterator on every diff call is expensive. A direct `Map` iteration without spread would avoid the allocation.

### NPERF-34 · `validateStreamingGenerator` Runs Full Synchronous Validation Then Yields — Not Streaming
**File:** `src/validator/StreamingValidator.ts`, lines 749–760  
`validateStreamingGenerator` is an `async function*` that calls the synchronous `validateStreaming` to completion, then yields each issue. This means the generator provides no backpressure and no incremental progress — it blocks the event loop for the full validation duration before yielding any issue.

### NPERF-35 · `BatchValidator._runBatch` Uses Synchronous Parse/Validate — No True CPU Parallelism
**File:** `src/validator/BatchValidator.ts`, lines 184–195  
Multiple files in a `concurrency=4` batch are parsed and validated synchronously in sequence within a single `Promise.all` batch because the parsing/validation is sync. True concurrency requires worker threads; the current design only parallelises I/O (the `readFile` step), not CPU-bound parsing.

### NPERF-36 · `NamespaceScope._flatCache` Is Rebuilt After Every `push` — Repeated Cold-Start Cost
**File:** `src/namespace/NamespaceEngine.ts`, line 62  
`push()` calls `this._flatCache = null` to invalidate the cache. On the next `resolve()` call for the new scope, the cache is rebuilt by walking the parent chain. For deeply nested documents where every new element pushes a scope, the flat cache is rebuilt once per `resolve` call on the new scope, producing O(depth) work per `resolve` at the warm-up phase.

### NPERF-37 · `XsdParser._anonCounter` Is Per-Parser — Sub-Parsers Have Independent Counters
**File:** `src/xsd/XsdParser.ts`, lines 117 and 253–258  
Each sub-parser created for `xs:import`/`xs:include` starts its `_anonCounter` at 0. If two sub-schemas both define anonymous types, their counters produce identical keys (e.g. `__anon_0__`). This forces the merged schema to overwrite one anonymous type with another, silently losing type definitions.

### NPERF-38 · `XmlParser.parseChildren` Checks Eight `peekStr`/`peek` Conditions Per Character
**File:** `src/parser/XmlParser.ts`, lines 444–475  
The inner loop calls `peekStr(2)`, `peekStr(9)`, `peekStr(4)`, `peekStr(2)` (again), `peek()` in sequence on every iteration. Each `peekStr(n)` calls `substring(pos, pos+n)`, allocating a new string. A single character peek for `'<'` followed by integer comparison on the next char would reduce this to 1–2 allocations.

### NPERF-39 · `sha256Bytes` Allocates a Full Padded Message Buffer for Every Hash
**File:** `src/utils/sha256.ts`, lines 51–52  
`new Uint8Array(paddedLen)` allocates a zero-filled typed array for every call to `sha256Short`. For the hot path in `SchemaCache.getOrParse` (called on every validation when content-hash tracking is enabled), this is a repeated 64-byte+ allocation. A pooled/reused scratch buffer would eliminate repeated allocation.

### NPERF-40 · `ValidationEngine` Calls `Date.now()` Per Element When `profile: false`
**File:** `src/validator/ValidationEngine.ts`, line 269  
`_emitProfile(path, t0)` is called unconditionally at line 361, which calls `Date.now()` again and allocates a `ProfileEvent` object that is pushed into `this.profileEvents`. When `profile: false` this is wasted work that could be short-circuited entirely.

---

## P2 — Medium Severity Bugs

### NP2-21 · `encodeEntities` Does Not Encode Single Quote — Incorrect in Single-Quoted Attributes
**File:** `src/utils/xmlUtils.ts`, lines 69–75  
`encodeEntities` encodes `&`, `<`, `>`, `"` but not `'`. If an attribute is serialised with single-quote delimiters and its value contains `'`, the output is invalid XML.

### NP2-22 · `XmlElement.find()` Does Not Support Wildcards or Namespaces
**File:** `src/parser/XmlNodes.ts`, lines 226–234  
`find('body/section/title')` only matches by `localName` (via `getChild`). Namespace-aware paths (`ns:body/ns:section`) and wildcards (`*/title`) are not supported. This makes the helper useless for namespace-qualified documents, which is the majority of XML documents validated by this tool.

### NP2-23 · `SchemaCache.keys()` Strips Hash Suffix Using Fixed Length Check (16 chars) — Fragile
**File:** `src/cache/SchemaCache.ts`, lines 406–412  
The hash-stripping logic uses `k.length - atIdx - 1 === 16` to detect a hash suffix. If `sha256Short` is called with a different `len` argument, or if a key happens to end with a 16-character segment that is not a hash, keys are incorrectly mangled.

### NP2-24 · `NamespaceEngine` Is Not Used by `XmlParser` — Duplicate Namespace Code
**File:** `src/namespace/NamespaceEngine.ts` and `src/parser/XmlParser.ts` (inner `NsContext` class)  
`XmlParser` implements its own `NsContext` class (lines 83–99) with a stack of `Map` copies. `NamespaceEngine` was implemented as a separate, more capable scoped system, but `XmlParser` was never updated to use it. Two implementations of namespace scoping diverge: `NamespaceEngine` validates `xmlns`/`xml` prefix rules; `NsContext` does not. Bugs fixed in one are not reflected in the other.

### NP2-25 · `XmlSerializer` Not Reviewed for Namespace Round-Trip Correctness
**File:** `src/parser/XmlSerializer.ts`  
After parsing, namespace declarations (`xmlns:prefix="uri"`) are stored as regular attributes on elements. If the serialiser emits them in insertion order but the original document had a different order, or if a prefix was redeclared in a child scope, the serialised output may be semantically identical but textually different, breaking round-trip tests or canonical XML expectations.

### NP2-26 · `SchemaPreflight.ts` Not Clearly Integrated Into Pipeline
**File:** `src/validator/SchemaPreflight.ts`  
`SchemaPreflight` exists as a standalone class but is not invoked by `ValidationPipeline` or `ValidationEngine`. Schema-level pre-validation (detecting undefined type references, circular dependencies) is therefore only performed if callers explicitly instantiate and call `SchemaPreflight`. Users of the high-level API receive no preflight errors.

### NP2-27 · `XmlCanonical` Not Integrated With Transforms — `transformXml` Output Is Not Canonical
**File:** `src/utils/XmlCanonical.ts` and `src/transform/XsltTransformer.ts`  
C14N (Canonical XML) is implemented but `XsltTransformer.transform()` and `serializeXml()` do not offer a `canonical: true` option or call the `XmlCanonical` module. Callers who want canonical output must manually post-process the serialised string.

### NP2-28 · `XsdToTypeScript` Codegen Does Not Handle `xs:union` Types
**File:** `src/codegen/XsdToTypeScript.ts`  
Union simple types (`xs:union memberTypes="..."`) are not reflected in the generated TypeScript. The generated type will be `string` for all union members, losing the type information that would allow TypeScript consumers to narrow values to the allowed set of literals.

### NP2-29 · `XsdToJsonSchema` Codegen Does Not Handle `xs:list` Types
**File:** `src/codegen/XsdToJsonSchema.ts`  
`xs:list` types (space-delimited lists of values) should generate `{"type":"array","items":{...}}` in JSON Schema, but they are likely emitted as `{"type":"string"}`, losing the list structure.

### NP2-30 · `parseArgs` in CLI Does Not Validate `--max-errors` and `--max-warnings` Number Range
**File:** `src/cli/cli.ts`, lines 257–261  
`parseInt(args[++i], 10) || 0` silently converts any non-numeric value (e.g. `--max-errors foo`) to `0`. No warning is emitted to the user. A `NaN` from `parseInt` silently becomes 0 (meaning "show all"), which is the opposite of the user's intent when they provided an invalid number.

### NP2-31 · `SchemaInference` Not Validated Against Real-World Schema Patterns
**File:** `src/utils/SchemaInference.ts`  
`SchemaInference` statistically infers XSD from sample XML documents. Inferred schemas for mixed-content models and `xs:choice` with variable membership are likely incorrect because inference algorithms require many samples to distinguish optional vs required vs repeated elements. No minimum-sample-size warning is emitted.

### NP2-32 · `XmlDiff` Does Not Detect Namespace URI Changes When Prefix Is the Same
**File:** `src/utils/XmlDiff.ts`  
The fingerprint uses `el.tagName` (the prefixed name) to hash the element. If the same prefix is rebound to a different URI in two documents (e.g., `xmlns:a="ns1"` vs `xmlns:a="ns2"`), the fingerprints are identical even though the elements are semantically different. `diffXml` reports no change.

### NP2-33 · `BatchValidator.validateString` Can Fail Silently If Schema Path Is a File Path
**File:** `src/validator/BatchValidator.ts`, lines 136–146  
`validateString` (synchronous) checks `this.cachedSchema` and falls back to `parseXsd(this.schemaSource)` only if `schemaSource` is set. If the `BatchValidator` was constructed with a file path (not raw XSD source), `schemaSource` is `null` and the method throws `'Schema not loaded'` without attempting to load. Callers who constructed with a path and call `validateString` receive confusing errors.

### NP2-34 · `XmlParser` Ignores Stray Text Before Root Element
**File:** `src/parser/XmlParser.ts`, lines 180–184  
Text content before the root element is silently discarded (`this.r.next()`). Per XML 1.0, any non-whitespace before the root element (after the prolog) is a well-formedness error. The parser should throw `PARSE_EXPECTED_TOKEN` rather than silently advance.

### NP2-35 · `ValidationEngine._getXsiAttr` Fast-Path Checks `xsi:` Prefix Literally
**File:** `src/validator/ValidationEngine.ts`, lines 1137–1139  
`elem.attributes.get('xsi:localName')` assumes the XSI namespace is always bound to the `xsi` prefix. A document that binds the XSI namespace to a different prefix (e.g. `xmlns:myxsi="http://www.w3.org/2001/XMLSchema-instance"`) will not be found by the fast path. The linear scan fallback is correct, but the misleading fast path adds fragility.

### NP2-36 · `XmlToJson` With `coerceBooleans: true` Treats `"True"` and `"TRUE"` Inconsistently With XSD
**File:** `src/transform/XmlToJson.ts`  
If boolean coercion uses strict `=== 'true'` comparison, then `"True"` and `"TRUE"` (which XSD `xs:boolean` accepts per lexical mapping) are not coerced and are left as strings, missing the semantic intent of the option.

### NP2-37 · `XmlStreamParser` Does Not Emit `'data'` for Node Streams Compatibility
**File:** `src/io/XmlStreamParser.ts`, lines 43–69  
Consumers who only listen to `'document'` without also listening to `'error'` have no way to detect failures if `_flush` calls `callback(err)` without first emitting `'error'`. The error can go unhandled.

### NP2-38 · CLI `--code-frame` Calculates Pointer Column Using `col - 1` — Off-by-One
**File:** `src/cli/cli.ts`, line 382  
`' '.repeat(width + 5 + Math.max(0, col - 1)) + '^'` — If the `width` of line numbers changes for files with many lines (i.e., more digits), the pointer shifts left/right, misaligning the caret.

### NP2-39 · `XsdParser` Does Not Handle `xs:element` With Both `ref` and `name` Attributes
**File:** `src/xsd/XsdParser.ts`, lines 308–327  
XSD forbids an `xs:element` from having both `ref` and `name`. The parser silently takes the `ref` path if `refAttr` is non-empty regardless of `name`. A `SCHEMA_PARSE_ERROR` should be thrown for such malformed schemas.

### NP2-40 · `_checkIntRange` in `TypeValidator` Does Not Use BigInt for `xs:long` Range
**File:** `src/validator/TypeValidator.ts` (`_checkIntRange` method)  
`xs:long` range is −9,223,372,036,854,775,808 to +9,223,372,036,854,775,807, which exceeds JavaScript's safe integer range (`Number.MAX_SAFE_INTEGER` = 9,007,199,254,740,991). Checking range with `parseInt`/`Number` silently truncates the value, accepting out-of-range `xs:long` values as valid.

### NP2-41 · `NamespaceEngine.NamespaceScope` `_flatCache` Is Never Populated
**File:** `src/namespace/NamespaceEngine.ts`, lines 86–99  
`resolve()` checks `if (this._flatCache) return this._flatCache.get(prefix)`, but `_flatCache` is never populated (it is only initialised to `null`). The cache check always misses and the chain-walk always runs. The flat-cache acceleration is effectively dead code.

### NP2-42 · `SchemaCache` `validateFile` Does Not Cache the Parsed `XmlDocument`
**File:** `src/cache/SchemaCache.ts`, lines 218–229  
`validateFile` reads the XML file every time it is called. If the same XML file is validated multiple times (e.g. in watch mode), there is no caching of the parsed `XmlDocument`. For large XML files the parse cost is paid on every call.

---

## P3 — Low Severity / Nice-to-Have Fixes

### NP3-21 · `XmlError.toString` Does Not Include `stage` or `category` in Output
**File:** `src/errors/XmlError.ts`, lines 225–230  
`toString()` omits `this.stage` and `this.category`. Error messages logged to console lack pipeline context (e.g. "parse" vs "validate"), making debugging harder.

### NP3-22 · `printHelp` Version Number Is Hard-Coded Fallback `'1.1.0'`
**File:** `src/cli/cli.ts`, line 76  
`_cachedVersion = '1.1.0'` is the catch fallback. When the `package.json` is not found (e.g. in a self-contained dist bundle), the CLI reports version `1.1.0` regardless of the actual version.

### NP3-23 · `XmlComment.toString()` Does Not Validate `--` in Value
**File:** `src/parser/XmlNodes.ts`, lines 100–103  
`XmlComment.toString()` returns `<!--${this.value}-->` without checking whether `this.value` contains `--`. If a comment node is constructed programmatically with `--` in its value, `toString()` produces invalid XML.

### NP3-24 · `XmlProcessingInstruction.toString()` Does Not Validate `?>` in Data
**File:** `src/parser/XmlNodes.ts`, lines 118–121  
`XmlProcessingInstruction.toString()` does not check `data` for `?>`, which would terminate the PI early and produce invalid XML.

### NP3-25 · `XmlDoctype.toString()` Could Double-Wrap if `content` Includes `DOCTYPE`
**File:** `src/parser/XmlNodes.ts`, lines 133–136  
`parseDoctype` stores everything after `<!DOCTYPE` in `content`, and `toString()` returns `<!DOCTYPE ${this.content}>`. If `content` somehow begins with `DOCTYPE` (malformed parser capture), the output double-emits the keyword.

### NP3-26 · `XmlDocument.toString()` Always Emits XML Declaration Even If None Was Present
**File:** `src/parser/XmlNodes.ts`, lines 327–333  
`toString()` always prefixes with `<?xml version="..." encoding="..."?>`. If the original document had no XML declaration, round-tripping through parse→toString adds one, breaking byte-level idempotence.

### NP3-27 · `SaxParser.off()` Linear Splice Is O(handlers) Per Removal
**File:** `src/parser/SaxParser.ts`, lines 137–144  
`list.splice(idx, 1)` is O(n) where n is the number of registered handlers for the event type. For event-heavy integrations that register/deregister frequently, a Set-based handler registry would be more efficient.

### NP3-28 · `xsdPatternToJs` Falls Back to `^[\s\S]*$` on Regex Compile Error — Silent Permissive Match
**File:** `src/validator/TypeValidator.ts`, lines 118–121  
When an XSD pattern cannot be compiled to a valid JavaScript regex, the fallback `/^[\s\S]*$/` matches any string, silently validating all values as conformant. A warning or a `fail(...)` return would be more appropriate.

### NP3-29 · `getVersion()` in CLI Uses `__dirname` Which Is Not Available in ESM Modules
**File:** `src/cli/cli.ts`, lines 72–73  
`resolve(__dirname, '../../..', 'package.json')` — `__dirname` is a CommonJS-only variable. In the ESM build of the CLI, this will be `undefined`, causing the path resolution to silently resolve to the wrong location or throw.

### NP3-30 · `ParseBudget` Uses `SEC_MAX_TEXT_LENGTH` For Both Byte and Time Limit Violations
**File:** `src/utils/ParseBudget.ts`, lines 50 and 70  
Both the byte-limit and wall-time-limit violations throw `secError('SEC_MAX_TEXT_LENGTH', ...)`. Callers cannot distinguish between the two from the error code alone. Dedicated codes (`SEC_MAX_BYTES` and `SEC_MAX_WALL_TIME`) would allow targeted handling.

### NP3-31 · `XmlToJson.xmlToJson()` Does Not Handle `XmlProcessingInstruction` Nodes
**File:** `src/transform/XmlToJson.ts`  
Processing instruction nodes are silently skipped. If a caller needs to round-trip PI nodes through JSON, they are lost without any warning.

### NP3-32 · `JsonToXml` Does Not Validate That Generated Tag Names Are Legal XML Names
**File:** `src/transform/JsonToXml.ts`  
JSON keys can contain characters illegal in XML names (spaces, `#`, digits-as-first-char). `JsonToXml` likely uses JSON keys as element names directly without validation, producing invalid XML.

### NP3-33 · `XmlDiff` Does Not Support Namespace-Aware Comparison Mode
**File:** `src/utils/XmlDiff.ts`  
`diffXml` compares elements by `tagName` (prefixed name) not `{namespaceURI}localName`. Two semantically identical documents that use different namespace prefixes would diff as entirely modified because `<ns1:item>` ≠ `<ns2:item>` lexically.

### NP3-34 · `ValidationResult.merge()` Does Not Deduplicate Identity-Constraint Issues
**File:** `src/validator/ValidationResult.ts`  
Since `ValidationEngine` and `IdentityConstraintEngine` both run in the pipeline (P1-15), `merge()` is called with duplicate identity issues. The deduplication set `_seenErrors` dedups on the message string, meaning slightly different path or severity strings bypass deduplication and duplicates slip through.

### NP3-35 · `XmlParser` Does Not Enforce That `standalone="yes"` Documents Have No External Markups
**File:** `src/parser/XmlParser.ts`, lines 219–223  
When `standalone="yes"` is declared, XML 1.0 §2.9 requires that no externally-declared markup affects the document. The parser records the `standalone` flag but performs no follow-up enforcement. This is a minor spec conformance gap.

### NP3-36 · `XsdParser` Does Not Store `xs:annotation`/`xs:documentation` — Lost Documentation
**File:** `src/xsd/XsdParser.ts`  
`xs:documentation` inside `xs:annotation` elements is silently discarded. Schema documentation strings are useful for codegen tools (e.g. generating TypeDoc/JSDoc comments in `XsdToTypeScript`). The `SchemaModel` has no `documentation` field.

### NP3-37 · CLI `--schema-dir` Option Is Not Used When Resolving `xs:include`
**File:** `src/cli/cli.ts`  
`--schema-dir` is passed as the base for the schema loader, but only for `xs:import`. Inline `xs:include` relative to the XSD file's own directory may use a different loader path. The interaction between `--schema-dir` and relative `xs:include` paths is not tested or clearly documented.

### NP3-38 · `XmlCanonical` Output Is Not Verified Against W3C C14N Test Suite
**File:** `src/utils/XmlCanonical.ts`  
C14N (Canonical XML 1.0) is a precisely spec'd algorithm with a published test suite. Without documented test coverage against the XMLDSIG C14N test vectors, the implementation may silently deviate from the spec for edge cases (namespace propagation, attribute ordering, empty element handling).

### NP3-39 · `XsdToTypeScript` Does Not Emit `readonly` for Fixed-Value Elements
**File:** `src/codegen/XsdToTypeScript.ts`  
Elements with `fixed="value"` in XSD should generate a TypeScript property with a literal type (e.g., `status: 'active'`) or at minimum a `readonly` modifier. Without this, TypeScript consumers can assign non-fixed values without a compile error.

### NP3-40 · `DfaEngine` States Array Can Grow Unbounded for `xs:choice` With Many Alternates
**File:** `src/validator/DfaEngine.ts`  
For `xs:choice` content models with a very large number of alternates (e.g., 1000 element names), `buildDfa` constructs a `states` and `transitions` array with no size limit applied, meaning a pathological XSD could cause unbounded memory use during schema compilation.

### NP3-41 · `XmlElement.walk()` Does Not Provide Node Type Filter — Forces All Visitors to Branch
**File:** `src/parser/XmlNodes.ts`, lines 251–254  
`walk(visitor)` visits every node regardless of type. Most callers visit only elements or only text nodes and immediately check `nodeType`. A `walkElements(visitor)` overload or a `filter` parameter would improve usability.

### NP3-42 · `XsltTransformer` Does Not Log or Expose Template Compilation Warnings
**File:** `src/transform/XsltTransformer.ts`  
Unknown XSL elements are silently treated as literal result elements and passed through to output. A stylesheet with a typo like `<xsl:if-not>` instead of `<xsl:if>` produces unexpected output silently. A warning list on the transformer would help debugging.

### NP3-43 · `SchemaModel.merge()` Does Not Detect Conflicting Type Definitions
**File:** `src/schema/SchemaModel.ts`  
When two imported schemas define the same type name, `merge` silently overwrites the first with the second. No `SCHEMA_CONFLICTING_DECL` error is emitted even though the error code is defined in `XmlError.ts`. The schema is silently corrupted.

### NP3-44 · `XmlParser` Prolog Loop Does Not Enforce At-Most-One DOCTYPE
**File:** `src/parser/XmlParser.ts`, lines 169–185  
The prolog loop allows multiple `<!DOCTYPE` declarations. XML 1.0 §2.8 permits at most one `DOCTYPE` declaration. Duplicate DOCTYPEs should raise a `PARSE_DUPLICATE_DOCTYPE` error.

### NP3-45 · `ValidationIssue.offset` Field Is Never Populated
**File:** `src/validator/ValidationResult.ts`, line 30 and `src/validator/ValidationEngine.ts`  
`ValidationIssue.offset` (0-based character offset, for code-frame tools) is defined but never set by `ValidationEngine._addError`. Code-frame tools that rely on `offset` rather than `line`/`col` receive `undefined`.

### NP3-46 · `XmlParser.parseXmlDeclaration` Does Not Validate `version` Value
**File:** `src/parser/XmlParser.ts`, lines 216–222  
The version extracted from `<?xml version="..."?>` is stored as-is. Any string is accepted. XML 1.0 §2.8 requires the version to be `"1.0"` (or `"1.1"` for XML 1.1). An unknown version like `version="2.0"` should produce a warning or error per spec.

### NP3-47 · `XmlElement.setAttribute` Does Not Re-Parse `prefix`/`localName` After Name Change
**File:** `src/parser/XmlNodes.ts`, lines 180–182  
If an existing attribute is retrieved and its `name` mutated directly (valid in JS), the cached `prefix`/`localName` become stale. Attribute `name` should be `readonly` or a setter should re-parse.

### NP3-48 · `XsdToJsonSchema` Does Not Map `xs:anyURI` to JSON Schema `"format":"uri"`
**File:** `src/codegen/XsdToJsonSchema.ts`  
`xs:anyURI` is likely emitted as `{"type":"string"}` without the `"format":"uri"` annotation. JSON Schema validators that enforce formats would not validate URI constraints.

### NP3-49 · No Exported Type for `ParseOptions` in Browser Entry Point
**File:** `src/browser.ts` and `src/index.ts`  
`ParseOptions` is defined in `XmlParser.ts` and re-exported from the main entry, but the browser entry point (`src/browser.ts`) may not export it. Browser-targeting TypeScript consumers receive no type hints for parser options.

### NP3-50 · `XmlDiff.diffXml` Uses a Per-Call `WeakMap` Cache — Cannot Be Warmed Across Calls
**File:** `src/utils/XmlDiff.ts`, lines 44–46  
The fingerprint cache is a fresh `WeakMap` per `diffXml()` call. While this prevents cross-call pollution, it means repeated diffs of the same document pair (e.g. in a watch-mode editor) recompute all fingerprints on every invocation. An optional external cache parameter would allow callers to warm it across calls.

---

*Generated: 2026-04-24. Total issues catalogued: 15 P0, 30 P1, 20 Performance, 22 P2, 30 P3.*

