# ADR 0004 — Centralized Namespace Resolution Engine

| Field | Value |
|---|---|
| **Status** | Accepted |
| **Date** | 2026-04-14 (v1.4.0 — G35) |
| **Deciders** | Core team |
| **Supersedes** | v1.0–v1.3 (inline namespace resolution in XmlParser) |

---

## Context

XML Namespaces 1.0 is a separate specification layered on top of XML 1.0. Namespace resolution — mapping `prefix:localName` to `{namespaceURI}localName` — requires maintaining a scoped stack of prefix → URI bindings as the parser walks the element tree.

In v1.0–v1.3, namespace resolution was done inline within `XmlParser._buildDom()`. This caused several problems:

1. **Duplication** — `XmlSerializer`, `XsdParser`, and the SAX layer each had their own partial namespace logic, with subtle inconsistencies
2. **Missing constraints** — the `xml` prefix being bound to the wrong URI was not caught; re-declaring `xmlns` as a prefix was silently accepted
3. **No reuse** — `XPathEngine` couldn't share the namespace context from the parser; `SaxParser` had its own NsStack class
4. **Testing difficulty** — namespace resolution was entangled with DOM construction; hard to test in isolation

In v1.4, we extracted namespace resolution into a dedicated, reusable module.

---

## Decision

**Create a dedicated `NamespaceEngine` class (`src/namespace/NamespaceEngine.ts`) used by all modules that need namespace resolution.**

The engine provides:

### Scoped push/pop stack

```ts
const ns = new NamespaceEngine();

ns.pushScope([{ prefix: 'xs', uri: 'http://www.w3.org/2001/XMLSchema' }]);
const resolved = ns.resolveQName('xs:string');
// → { namespaceURI: '...XMLSchema', localName: 'string', prefix: 'xs', qname: 'xs:string' }
ns.popScope();
```

Each call to `pushScope()` creates a new frame that inherits all bindings from the parent frame. `popScope()` removes the top frame.

### XML Namespaces 1.0 constraints enforced

| Constraint | Rule |
|---|---|
| `xml` prefix | Must always map to `http://www.w3.org/XML/1998/namespace` |
| `xmlns` prefix | Reserved — cannot be re-declared as a namespace prefix |
| Default namespace | Empty prefix maps to declared default, or `""` if undeclared |
| Attribute namespace | Unprefixed attributes are always no-namespace, even with a default namespace in scope |

### Consistent key format

```ts
NamespaceEngine.buildKey('http://example.com', 'item')  // '{http://example.com}item'
NamespaceEngine.parseKey('{http://example.com}item')    // { ns, local }
```

All internal maps in `SchemaModel`, `CompiledSchema`, and `ValidationEngine` use this `{ns}local` key format for namespace-aware element lookup.

### Null-safe resolution

```ts
ns.tryResolveQName('unknown:foo')  // null — safe for optional resolution
```

### Prefix allocation for serialization

```ts
ns.getOrAllocatePrefix('http://example.com', 'tns')
// → 'tns' (or generates 'ns0', 'ns1', … if 'tns' is taken)
```

Used by `XmlSerializer` when re-writing namespace declarations on elements.

---

## Consequences

### Positive

- **Single implementation** — namespace logic in one place; fixes propagate everywhere
- **Spec compliance** — `xml`/`xmlns` constraints now enforced uniformly
- **Reusable** — `XmlParser`, `SaxParser`, `XsdParser`, `XmlSerializer`, `NamespaceEngine` (direct use) all share the same engine
- **Testable in isolation** — `NamespaceEngine` has its own test file; namespace bugs are trivial to reproduce
- **Consistent keys** — `{ns}local` format used uniformly throughout the validator avoids a whole class of lookup bugs

### Negative

- **Extra object allocation** — one `NamespaceEngine` instance per parse. Benchmarks show negligible impact (~1%).
- **Migration cost** — v1.4 required updating `XmlParser`, `SaxParser`, and `XsdParser` to use `NamespaceEngine` instead of their own inline stacks. All three modules were refactored in a single release.

### Backward compatibility

The `NamespaceEngine` is exported as a public API. Callers can use it directly for namespace-aware processing without parsing XML. No breaking changes to `parseXml()`, `parseSax()`, or `parseXsd()` — they now use `NamespaceEngine` internally, transparently.

---

## Alternatives Considered

| Alternative | Rejected Because |
|---|---|
| Keep inline logic in each module | Continues duplication; divergent constraint enforcement |
| Build namespace resolution into `XmlLexer` | Lexer should be namespace-agnostic; complicates the token model |
| Use a global namespace registry | Not thread-safe; breaks test isolation |
| Resolve namespaces lazily on demand | Complicates error reporting; makes it impossible to detect undeclared prefixes eagerly |

