diff --git a/docs/analysis-issues.md b/docs/analysis-issues.md
index 9d7ab51..43926d3 100644
--- a/docs/analysis-issues.md
+++ b/docs/analysis-issues.md
@@ -322,3 +322,135 @@ hover 同时显示 `No matching definition of the expected declared type...`。
> `ModuleId` 等模块引用指向同一 GameObject 内的兄弟模块,但部分引用(如武器上的
> `AttachModuleId`)目标 GameObject 跨文件无法静态确定,本轮先统一不检查;待局部
> 作用域建模落地后再启用这些引用的解析与诊断。
+
+---
+
+## 十、问题分析(第五轮,2026-08-01):`xi:include` 的 `href`/`xpointer` 误报未知属性
+
+### 问题
+
+AttachTest `Allied Vehicle\Guardian Tank\GameObject.xml` 第 249 行附近:
+
+```xml
+
+```
+
+报两条 `Unknown attribute "href" / "xpointer" for `(`unknown-attribute`),
+hover 同时显示 `Unknown attribute for this element.`。
+
+这个元素属于 **W3C XInclude 命名空间**(`xmlns:xi="http://www.w3.org/2001/XInclude"`),
+并不是 EA `uri:ea.com:eala:asset` XSD 的一部分。同一行在第二轮“问题 C”处理过
+(嵌套 `xi:include` 的索引与导航),但那轮没有覆盖 unknown-attribute 诊断,属于遗留缺口。
+
+### 根因
+
+诊断的属性校验没有像元素校验那样排除外来命名空间:
+
+- 元素校验已有 `!el.name.startsWith("xi:")` 守卫(所以 `` 本身不报 unknown element);
+- 属性校验只跳过 `xmlns*` / `xai:` / `xi:` 前缀的属性名,而 `href`、`xpointer` 是不带
+ 前缀的普通属性名;
+- `` 解析类型为 null(XSD 模型不含该元素),knownAttrs 为空 → 任何属性
+ 都被判为 unknown。
+
+### 修复
+
+1. `schemaModel` 新增两个纯函数:
+ - `isXsdElementName`:`xi:` 前缀元素不属于 EA XSD 模型;
+ - `isXsdAttributeName`:EA XSD 属性不带命名空间前缀,带前缀(`xai:`、`xi:`、
+ `xlink:`、`xml:`、`xsi:`、`xmlns:*`)的都是命名空间机制,不做 schema 校验。
+2. `diagnostics`:`xi:` 前缀元素整体跳过 schema 校验(元素与属性都不再误报);
+ 前缀属性名统一跳过。
+3. `hover`:`xi:include` 元素/属性给出 XInclude 说明;`href` 值悬停像
+ `` 一样解析目标文件(Ctrl+点击跳转此前已可用)。
+
+### 验证
+
+- 真实文件全量扫描:0 未知元素、0 未知属性(修复前 `href`/`xpointer` 两条必现);
+- 新增测试:`isXsdElementName` / `isXsdAttributeName` 断言;`xi:include` 解析类型为
+ null 且不参与校验;全量 39/39 通过。
+
+### 后续(架构方向,待确认)
+
+用户提出“先展开 `xi:include`(类比 C++ 宏展开),再处理 mod XML 解析”。该方向与第二轮
+遗留的“虚拟合并”开放项一致,设计要点:
+
+- 构建**逻辑树**而非文本拼接:把目标文件选中内容(`xpointer` 子集)作为子节点拼入父
+ 元素,节点保留源文件与原始偏移,避免文本级拼接导致的偏移断裂;
+- 展开范围:`xi:include` 与 EA ``(内容合并);`instance` /
+ `reference` 是可见性 / 编译产物语义,不拼树;`inheritFrom` + `joinAction` 是属性级
+ 继承合并,不是宏展开;
+- 收益:跨 include 的上下文类型解析、包含内容的结构校验、以及后续“GameObject 内模块
+ id 局部作用域”(HeadlightDraw2 的模块也是该 GameObject 的模块);
+- 风险:include 环 / 深度限制、大文件性能、`xpointer` 仅支持现有子集形式
+ (`/n:Name/child::*`)。
+
+---
+
+## 十、问题分析(第五轮,2026-08-01):`xi:include` 的 `href` / `xpointer` 被误报为未知属性
+
+### 问题
+
+同一 GameObject.xml 第 246–249 行:
+
+```xml
+
+
+```
+
+报两条 `Unknown attribute "href" / "xpointer" for `(unknown-attribute),
+hover 显示 `Unknown attribute for this element.`。
+
+**与第二轮的关系**:第二轮“问题 C”处理的正是同一行的嵌套 `xi:include`——但那一轮修的是
+**索引器**(嵌套 include 不再被静默忽略、缺失目标产生 include-not-found、目标内容进索引),
+本轮这处 **unknown-attribute 诊断**是当时未覆盖的遗留问题。
+
+### 根因
+
+`xi:include` 属于 W3C XInclude 命名空间(`http://www.w3.org/2001/XInclude`),
+**不是 RA3 XSD(`uri:ea.com:eala:asset`)定义的元素**:
+
+- 未知元素检查已通过 `el.name.startsWith("xi:")` 跳过,所以没有 unknown-element 误报;
+- 但属性检查没有同类守卫:`xi:include` 解析类型为 null → `knownAttrs` 为空 →
+ `href`、`xpointer` 两个非 `xi:` 前缀的属性名全部落入 unknown-attribute 分支。
+
+复现证据(真实文件):
+
+```
+xi:include found: true | parent: Draws
+resolved element type: null
+known attribute names: (none)
+attr href: known=false -> would flag unknown-attribute: true
+```
+
+### 修复
+
+1. **模型层新增命名空间守卫**(`schemaModel.ts`):
+ - `isXsdElementName(name)`:`xi:` 前缀(XInclude)等外来命名空间元素不属于 XSD 模型;
+ - `isXsdAttributeName(name)`:EA XSD 属性一律无前缀,带前缀的属性
+ (`xai:`、`xi:`、`xlink:`、`xml:`、`xsi:`、`xmlns:*`)都是命名空间机制,不做未知属性校验。
+2. **诊断**(`diagnostics.ts`):外来命名空间元素的未知元素/未知属性检查整体跳过
+ (`href`、`xpointer` 不再误报);属性名带前缀的一律跳过校验(比原先只跳过
+ `xmlns`/`xai:`/`xi:` 更完整)。
+3. **hover**(`hover.ts`):`xi:include` 的元素/属性悬停显示 XInclude 说明;
+ `xi:include@href` 与 `Include@source` 一样显示解析后的目标文件(与第二轮已可用的
+ Ctrl+点击跳转对齐)。
+
+**测试(37 → 39,全部通过)**:
+
+- `schemaModel.test.mjs`:`isXsdElementName` / `isXsdAttributeName` 判定
+ (`xi:include` 非 XSD 元素;`href`/`xpointer` 是合法属性名形态;
+ `xai:joinAction`、`xlink:href`、`xmlns:xi` 等带前缀属性不校验);
+- `refs.test.mjs`:GameObject → Draws → `xi:include` 实景结构解析类型为 null、
+ 元素被判定为外来命名空间,`href`/`xpointer` 不会进入未知属性分支;
+- 实机复验:整份 GameObject.xml 0 个 unknown-attribute 残留。
+
+> **架构讨论(用户提议)**:把 XML 先“宏展开”成不含 `xi:include` 的版本再解析。
+> 这与 BAB 编译时的实际行为一致(`defaultscript.cs` 把整个 Mod 合并成一份大 XML),
+> 也是实现“GameObject 内模块 id 局部作用域解析”(第四轮遗留)的正确地基——展开后一个
+> GameObject 连同 include 进来的兄弟模块都在同一棵树里,`AttachModuleId` 等模块引用才能
+> 静态判定。设计备忘(现状 / 逻辑树方案 / 展开范围 / 落地点 / 检查清单)已整理在
+> `docs/plan.md` 第六节,等待确认后作为下一阶段实现。
diff --git a/docs/plan.md b/docs/plan.md
index b5b0547..c6cb59d 100644
--- a/docs/plan.md
+++ b/docs/plan.md
@@ -123,7 +123,7 @@ test/
4. [x] 纯 TS 核心:include 解析、manifest 解析、XML 解析封装、索引器、引用解析。
5. [x] 功能层:补全、hover、导航、诊断、大纲、高亮 grammar。
6. [x] 单测(fixture Mod,31 个用例全绿)+ 编译 + `vsce package` 打包(ra3-mod-xml-0.1.0.vsix,约 259KB)。
-7. [x] 在 AttachTest / GenEvoTest / Corona 上做冒烟验证,并按真实项目反馈修复问题(详见 `docs/analysis-issues.md` 四轮分析)。
+7. [x] 在 AttachTest / GenEvoTest / Corona 上做冒烟验证,并按真实项目反馈修复问题(详见 `docs/analysis-issues.md` 五轮分析)。
## 四、验证结果(实测)
@@ -133,7 +133,7 @@ test/
| GenEvoTest | 24 文件 | ~1.8s | 35,392(manifest 35,322) | 项目 ID(alliedmcv 等)正确收录 |
| Corona | 3,448 文件(+ 非 XML 资产路径) | ~54.5s | 55,305(manifest 35,322) | 3 个流、183 个 Define、0 诊断 |
-单元测试覆盖:XML 解析(自闭合/容错/偏移)、include 解析(BAB 顺序、SDK 根优先于 SageXml)、manifest 二进制解析(合成 v5 样本、类型/ID 推导)、索引器(资产/Define/流/缺失 include/嵌套 xi:include)、XSD 模型(上下文类型、`childTypeOf`、大小写规范化、属性级 refType)、引用过滤(`Weapon="X"` 只跳 `WeaponTemplate`、模块 `id` 定义点、Poid 局部引用、`Side="Allies"` 命中 manifest 的 `PlayerTemplate`)。
+单元测试覆盖:XML 解析(自闭合/容错/偏移)、include 解析(BAB 顺序、SDK 根优先于 SageXml)、manifest 二进制解析(合成 v5 样本、类型/ID 推导)、索引器(资产/Define/流/缺失 include/嵌套 xi:include)、XSD 模型(上下文类型、`childTypeOf`、大小写规范化、属性级 refType、外来命名空间判定)、引用过滤(`Weapon="X"` 只跳 `WeaponTemplate`、模块 `id` 定义点、Poid 局部引用、`xi:include` 不校验、`Side="Allies"` 命中 manifest 的 `PlayerTemplate`)。
> 注:`D:\Mods\CoronaMod` 位于移动硬盘,当前未连接;GenEvoTest / Corona 的回归需在 D: 盘可用时补跑(用户会另行通知)。
@@ -142,4 +142,86 @@ test/
- 假设 SDK 路径默认 `C:\Apps\RA3-MODSDK-X`(与 prompts 一致),可在设置中修改。
- 假设补全/导航以“文本语义分析”为主,不做完整 XSD 校验(BAB 才是权威校验器)。
- 开放:是否发布到 VS Code Marketplace(需要 publisher)——本期先保证本地 `vsce package` 可安装。
-- 开放:嵌套 `xi:include` 内容的"虚拟合并"进父文档(用于父文档内的补全/诊断感知被内联内容)——当前仅保证目标文件可索引、可导航、缺失可诊断。
+- 开放:**“宏展开”式虚拟合并**(用户提议,方向已确认):解析前先把 `xi:include`
+ (以及顶层 ``、`inheritFrom` 继承合并)展开成不含 include 的
+ 文档树,再对展开后的树做 XSD 校验、补全与诊断——与 BAB 编译时把整个 Mod 合并成
+ 一份大 XML 的行为一致。展开树需携带**来源追溯**(错误/跳转仍定位到原始文件),
+ 并处理 xpointer 子集解析与 include 循环。当前仅做到:目标文件可索引、可导航、
+ 缺失可诊断;`xi:include` 本身不参与 XSD 校验(第五轮)。
+ 该功能也是“GameObject 内模块 id 局部作用域解析”(第四轮遗留)的地基。
+ 详细设计备忘见下一节。
+
+## 六、include 展开设计备忘(2026-08-01,待实施)
+
+> 目的:集中记录 include 处理相关的现状、结论与设计,下次遇到 include 问题时从这里继续,
+> 并在实施后把结果回写本节。
+
+### 1. 现状(截至第五轮,已实现)
+
+| 能力 | 状态 |
+|---|---|
+| `` / `instance` 递归索引(顶层资产、流、Define) | 已实现(indexer `walk`) |
+| `reference` → builtmods manifest 解析 / 缺失回退占位 XML | 已实现 |
+| 嵌套 `xi:include`(任意层级):目标可索引、缺失报 `include-not-found`、Ctrl+点击跳转、`href` hover 解析目标 | 已实现(第二轮 + 第五轮) |
+| `xi:include` 及其属性不参与 XSD 校验(外来命名空间守卫 `isXsdElementName` / `isXsdAttributeName`) | 已实现(第五轮) |
+| include 目标内容“虚拟合并”进父文档的逻辑树 | **未实现**(本文档主题) |
+
+### 2. 已确认的方向
+
+先展开成不含 include 的文档树(类比 C++ 宏展开),再对展开后的树做 mod XML 解析。
+BAB(`defaultscript.cs`)编译时正是这样把整个 Mod 合并成一份大 XML 的。
+
+### 3. 关键设计决策:逻辑树拼接,不做文本拼接
+
+- **不要**把 include 目标展开成文本再整体重新解析:源码偏移会断裂,诊断 / 跳转 / hover /
+ 补全全部无法映射回原始文件。
+- **要做**的是:解析器逐文件解析(现状不变);展开器把目标文件选中节点按 `xpointer`
+ 子集挂进父元素,节点保留各自的源文件与原始偏移(来源追溯)。后续分析跑在逻辑树上,
+ 范围映射按节点 `sourceFile` 回到对应文件的 lineMap。
+- 现有 `parseXml` 已记录标签 / 属性 / 值的起止偏移,`XmlElement` 结构可直接复用;拼接时
+ 用浅拷贝节点壳并重建 parent 链,避免破坏目标文件缓存树自身的 parent 指针。
+
+### 4. 展开范围
+
+| 构造 | 拼入逻辑树 | 理由 |
+|---|---|---|
+| `xi:include` | ✅ | 内容并入父元素(HeadlightDraw2 场景) |
+| EA `` | ✅ | BAB“内容合并”,等价于复制进来 |
+| `type="instance"` | ❌ | 只影响编译可见性;拼树会把 BaseVehicle 的顶层资产错误塞进当前文档 |
+| `type="reference"` | ❌ | manifest 编译产物,无文本内容 |
+| `inheritFrom` + `xai:joinAction` | 单独一轮 | 元素级继承深合并(Replace/Remove),不是宏展开 |
+
+### 5. 落地位置与接入点
+
+- 新纯模块(与编辑器解耦,呼应 P1):输入 `(parse 树, resolveSource 回调, readDocument 回调)`,
+ 输出逻辑树(root + elements 扁平列表,沿用 diagnostics 的遍历形态)。
+- 接入:diagnostics / hover / navigation / completion 目前各自 `parseXml(text)`;
+ 改为 parse 后过 expander 取逻辑树;范围映射按节点 `sourceFile` 选对应文件的 lineMap。
+- 按需展开(当前打开文档)+ 按文件缓存(复用 indexer `readDocument` 的 LRU);
+ 环 / 深度守卫复用现有 `visitedAll` 与深度限制思路(建议最大深度 64)。
+- `xpointer`:仅支持 mod 实际使用的 `xmlns(n=...) xpointer(/n:Name/child::*)` 子集
+ (现有 `findXPointerContainer` 正则已覆盖);完整 XPath 暂不支持,遇到新形式先记录到本节。
+
+### 6. 后续收益(承接第四 / 五轮遗留)
+
+- **GameObject 内模块 id 局部作用域**:展开后 include 进来的兄弟模块(HeadlightDraw2)
+ 与本体模块同树,`AttachModuleId` / `ModuleId` / `AutoResolveBody` 等 Poid 引用
+ 才能静态解析与诊断(第四轮遗留);
+- 跨 include 的上下文类型解析与结构校验;
+- 顶层 `type="all"` 合并后,当前文档视角的重复 id / 引用诊断更接近 BAB 结果。
+
+### 7. 风险与边界
+
+- 大文件性能(Corona 约 7500 文件 / 38MB):只展开当前文档的可达链,不全局展开;
+- include 环 / 深度:visited 集合 + 最大深度;
+- 被包含内容的诊断上报位置:建议按节点源文件 URI 上报(与偏移一致),包含点的 `href`
+ 上只报“缺失 / 无法解析 / 环”类问题;
+- `inheritFrom` 深合并(`xai:joinAction` 的 Replace/Remove 语义)单独设计,别与宏展开混在一轮。
+
+### 8. 下次遇到 include 问题的检查清单
+
+1. 现象发生在哪一层:索引(indexer)、诊断(diagnostics)、导航 / hover、还是补全?
+2. 现状能力是否已覆盖(见第 1 节表格);
+3. 涉及内容是否跨文件(需要逻辑树)还是本文件内(当前解析树即可);
+4. 若要展开:先实现第 5 节的纯模块与单测(fixture 增加 HeadlightDraw2 场景),再接入 feature;
+5. 把新结论回写本节与 `docs/analysis-issues.md`。
diff --git a/src/features/diagnostics.ts b/src/features/diagnostics.ts
index b21d213..7563040 100644
--- a/src/features/diagnostics.ts
+++ b/src/features/diagnostics.ts
@@ -115,8 +115,12 @@ export class Ra3Diagnostics {
}
}
+ // Elements outside the EA asset namespace (e.g. XInclude )
+ // are not part of the RA3 XSD model; skip their validation entirely.
+ const isXsdElement = model.isXsdElementName(el.name);
+
// Unknown element.
- if (settings.diagnoseUnknownElements && !el.name.startsWith("xi:")) {
+ if (settings.diagnoseUnknownElements && isXsdElement) {
const knownType = model.elementTypeName(local);
if (!knownType) {
diags.push(
@@ -131,38 +135,42 @@ export class Ra3Diagnostics {
}
// Attributes.
- const elType = resolveElementType(el);
- const knownAttrs = model.attributesOfType(elType);
- const knownNames = new Set(knownAttrs.map((a) => a.name));
- for (const attr of el.attrs) {
- const aName = attr.name;
- if (aName.startsWith("xmlns") || aName.startsWith("xai:") || aName.startsWith("xi:")) {
- continue;
- }
- if (settings.diagnoseUnknownElements && !knownNames.has(aName)) {
- diags.push(
- this.diag(
- new vscode.Range(
- document.positionAt(attr.nameStart),
- document.positionAt(attr.nameEnd),
+ if (isXsdElement) {
+ const elType = resolveElementType(el);
+ const knownAttrs = model.attributesOfType(elType);
+ const knownNames = new Set(knownAttrs.map((a) => a.name));
+ for (const attr of el.attrs) {
+ const aName = attr.name;
+ // Namespace declarations and prefixed attributes (xai:, xi:,
+ // xlink:, xml:, xsi:, xmlns:*) are not defined by the EA XSD.
+ if (aName.startsWith("xmlns") || !model.isXsdAttributeName(aName)) {
+ continue;
+ }
+ if (settings.diagnoseUnknownElements && !knownNames.has(aName)) {
+ diags.push(
+ this.diag(
+ new vscode.Range(
+ document.positionAt(attr.nameStart),
+ document.positionAt(attr.nameEnd),
+ ),
+ `Unknown attribute "${aName}" for <${local}>`,
+ vscode.DiagnosticSeverity.Warning,
+ "unknown-attribute",
),
- `Unknown attribute "${aName}" for <${local}>`,
- vscode.DiagnosticSeverity.Warning,
- "unknown-attribute",
- ),
+ );
+ }
+
+ if (!attr.hasValue) continue;
+ this.checkValueReferences(
+ elType,
+ attr.name,
+ attr.value,
+ attr,
+ document,
+ idx,
+ diags,
);
}
-
- if (!attr.hasValue) continue;
- this.checkValueReferences(
- elType,
- attr.name,
- attr.value,
- attr,
- document,
- idx,
- diags,
- );
}
// Include-specific checks.
diff --git a/src/features/hover.ts b/src/features/hover.ts
index e2d3c45..31ddb1b 100644
--- a/src/features/hover.ts
+++ b/src/features/hover.ts
@@ -29,7 +29,7 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
// Attribute name.
for (const attr of el.attrs) {
if (offset >= attr.nameStart && offset <= attr.nameEnd) {
- return this.attributeHover(elType, attr.name);
+ return this.attributeHover(el, elType, attr.name);
}
}
// Attribute value.
@@ -47,6 +47,14 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
}
private elementHover(name: string): vscode.Hover | null {
+ if (name.startsWith("xi:")) {
+ const md = new vscode.MarkdownString();
+ md.appendCodeblock(`<${name}>`, "xml");
+ md.appendMarkdown(
+ "XInclude element (W3C XInclude namespace) — not part of the RA3 XSD model.",
+ );
+ return new vscode.Hover(md);
+ }
const type = model.elementTypeName(name);
const info = type ? model.typeInfo(type) : undefined;
const md = new vscode.MarkdownString();
@@ -68,7 +76,11 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
return new vscode.Hover(md);
}
- private attributeHover(elementType: string | null, attrName: string): vscode.Hover | null {
+ private attributeHover(
+ el: { name: string },
+ elementType: string | null,
+ attrName: string,
+ ): vscode.Hover | null {
const attrs = model.attributesOfType(elementType);
const attr = attrs.find((a) => a.name === attrName);
const md = new vscode.MarkdownString();
@@ -78,6 +90,12 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
md.appendMarkdown(`Namespace/instance attribute.`);
return new vscode.Hover(md);
}
+ if (!model.isXsdElementName(el.name)) {
+ md.appendMarkdown(
+ `XInclude attribute (W3C XInclude namespace) — not part of the RA3 XSD model.`,
+ );
+ return new vscode.Hover(md);
+ }
md.appendMarkdown("Unknown attribute for this element.");
return new vscode.Hover(md);
}
@@ -117,7 +135,10 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
}
// Include source.
- if (el.name === "Include" && attrName === "source") {
+ if (
+ (el.name === "Include" && attrName === "source") ||
+ (el.name === "xi:include" && attrName === "href")
+ ) {
const resolved = idx
? resolveSource(
value,
diff --git a/src/model/schemaModel.ts b/src/model/schemaModel.ts
index 9a08c94..7d3de2f 100644
--- a/src/model/schemaModel.ts
+++ b/src/model/schemaModel.ts
@@ -212,3 +212,26 @@ export const STRUCTURAL_ELEMENTS = [
"Defines",
"Define",
];
+
+/**
+ * Element names that live outside the RA3 XSD model's namespace. The model
+ * only covers `uri:ea.com:eala:asset`; other namespaces (so far the W3C
+ * XInclude `xi:`) must not be validated against it.
+ */
+const FOREIGN_ELEMENT_PREFIXES = ["xi:"];
+
+/** True when an element name belongs to the RA3 XSD model's namespace. */
+export function isXsdElementName(name: string): boolean {
+ const lower = name.toLowerCase();
+ return !FOREIGN_ELEMENT_PREFIXES.some((p) => lower.startsWith(p));
+}
+
+/**
+ * True when an attribute name can be validated against the RA3 XSD model.
+ * EA schema attributes are unprefixed; prefixed attributes (`xai:`,
+ * `xi:`, `xlink:`, `xml:`, `xsi:`, `xmlns:*`) are namespace machinery and
+ * are not defined by the XSD.
+ */
+export function isXsdAttributeName(name: string): boolean {
+ return !name.includes(":");
+}
diff --git a/test/refs.test.mjs b/test/refs.test.mjs
index cbda73b..2e6638f 100644
--- a/test/refs.test.mjs
+++ b/test/refs.test.mjs
@@ -216,3 +216,29 @@ test("top-level asset ids with plain pipeline ids are not references", () => {
0,
);
});
+
+test("xi:include elements are outside the XSD model and unvalidated", () => {
+ // The nested XInclude inside a GameObject draw list (HeadlightDraw2.xml)
+ // must resolve to no XSD type, so href/xpointer can never be flagged as
+ // unknown attributes.
+ const xml = `
+
+
+
+
+
+
+`;
+ const doc = parseXml(xml);
+ const inc = doc.elements.find((e) => e.name === "xi:include");
+ assert.ok(inc);
+ assert.equal(model.isXsdElementName(inc.name), false);
+ assert.equal(resolveElementType(inc), null);
+ // The XInclude attributes themselves are unprefixed (model-valid name
+ // shape); the element-level foreign check is what keeps them quiet.
+ for (const a of inc.attrs) {
+ assert.equal(model.isXsdAttributeName(a.name), true, a.name);
+ }
+});
diff --git a/test/schemaModel.test.mjs b/test/schemaModel.test.mjs
index 61cf610..6931c2c 100644
--- a/test/schemaModel.test.mjs
+++ b/test/schemaModel.test.mjs
@@ -53,6 +53,23 @@ test("Include has reference/instance/all enum", () => {
assert.deepEqual(type?.enumValues, ["reference", "instance", "all"]);
});
+test("foreign namespaces are excluded from XSD validation", () => {
+ // XInclude elements (xi:include / xi:fallback) come from the W3C XInclude
+ // namespace, not from the EA asset XSD.
+ assert.equal(model.isXsdElementName("xi:include"), false);
+ assert.equal(model.isXsdElementName("xi:fallback"), false);
+ assert.equal(model.isXsdElementName("GameObject"), true);
+ assert.equal(model.isXsdElementName("AssetDeclaration"), true);
+ // EA schema attributes are unprefixed; prefixed attributes (xai:, xi:,
+ // xlink:, xml:, xsi:, xmlns:*) are namespace machinery.
+ assert.equal(model.isXsdAttributeName("href"), true);
+ assert.equal(model.isXsdAttributeName("xpointer"), true);
+ assert.equal(model.isXsdAttributeName("xai:joinAction"), false);
+ assert.equal(model.isXsdAttributeName("xlink:href"), false);
+ assert.equal(model.isXsdAttributeName("xml:space"), false);
+ assert.equal(model.isXsdAttributeName("xmlns:xi"), false);
+});
+
test("type assignability follows inheritance", () => {
assert.ok(model.isAssignableTo("GameObject", "BaseInheritableAsset"));
assert.ok(model.isAssignableTo("GameObject", "GameObject"));