From e671646a74a3c3fac355943cfe2f3a8dd9bc170d Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:22:04 +0800 Subject: [PATCH] =?UTF-8?q?fix(ai-first-config):=20=E6=94=B6=E5=8F=A3=20PR?= =?UTF-8?q?=20306=20=E5=AE=A1=E6=9F=A5=E9=81=97=E7=95=99=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 Generator 与 Tooling 的 anyOf 和坏形状回归覆盖,补齐组合关键字与未知 type 拒绝 - 修复 VS Code 配置工具的 object-array 直属项收集与 contains 文案一致性问题 - 更新 README、Game 文档与工具说明,明确 additionalProperties 显式 false 边界与最小接入路径 - 补充 ai-plan 跟踪与 trace,记录 PR 306 open threads 收口结果和验证摘要 --- .../Config/SchemaConfigGeneratorTests.cs | 48 ++++++++++ README.md | 2 +- .../todos/ai-first-config-system-tracking.md | 1 + .../traces/ai-first-config-system-trace.md | 26 ++++++ docs/zh-CN/api-reference/index.md | 2 +- docs/zh-CN/game/config-tool.md | 91 +++++++++++++++++++ docs/zh-CN/game/data.md | 2 +- docs/zh-CN/game/index.md | 2 +- docs/zh-CN/game/serialization.md | 4 +- docs/zh-CN/game/setting.md | 2 +- docs/zh-CN/game/storage.md | 2 +- docs/zh-CN/source-generators/index.md | 2 +- tools/gframework-config-tool/README.md | 14 ++- .../src/configValidation.js | 49 ++++++---- tools/gframework-config-tool/src/extension.js | 10 +- .../src/localization.js | 2 +- .../test/configValidation.test.js | 58 ++++++++++++ .../test/containsSummary.test.js | 2 +- .../test/localization.test.js | 3 + 19 files changed, 288 insertions(+), 34 deletions(-) diff --git a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs index cbde49a3..fb3bc15d 100644 --- a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs @@ -1843,6 +1843,54 @@ public class SchemaConfigGeneratorTests }); } + /// + /// 验证生成器会显式拒绝当前共享子集尚未支持的 anyOf。 + /// + [Test] + public void Run_Should_Report_Diagnostic_When_Object_Schema_Declares_Unsupported_AnyOf() + { + const string source = DummySource; + const string schema = """ + { + "type": "object", + "required": ["id", "reward"], + "properties": { + "id": { "type": "integer" }, + "reward": { + "type": "object", + "properties": { + "itemCount": { "type": "integer" } + }, + "anyOf": [ + { + "type": "object", + "required": ["itemCount"], + "properties": { + "itemCount": { "type": "integer" } + } + } + ] + } + } + } + """; + + var result = SchemaGeneratorTestDriver.Run( + source, + ("monster.schema.json", schema)); + + var diagnostic = result.Results.Single().Diagnostics.Single(); + + Assert.Multiple(() => + { + Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_015")); + Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error)); + Assert.That(diagnostic.GetMessage(), Does.Contain("reward")); + Assert.That(diagnostic.GetMessage(), Does.Contain("anyOf")); + Assert.That(diagnostic.GetMessage(), Does.Contain("does not support combinators that can change generated type shape")); + }); + } + /// /// 验证生成器接受显式声明的 additionalProperties: false。 /// diff --git a/README.md b/README.md index e75f987f..5b2ea3ec 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ 如果你采用 AI-First 配置工作流,建议在进入实现前先确认两条边界: -- 当前共享子集接受闭合对象边界 `additionalProperties: false` +- 当前共享子集接受闭合对象边界 `additionalProperties: false`(需显式设置为 `false`;省略或 `true` 视为不支持) - `oneOf` / `anyOf` 这类会改变生成类型形状的组合关键字当前会被 Runtime / Generator / Tooling 直接拒绝 更复杂的 schema shape 需要先回到 schema 设计与 raw YAML 维护路径,而不是假定编辑器工具存在隐藏支持。 diff --git a/ai-plan/public/ai-first-config-system/todos/ai-first-config-system-tracking.md b/ai-plan/public/ai-first-config-system/todos/ai-first-config-system-tracking.md index f2bcd033..13939f27 100644 --- a/ai-plan/public/ai-first-config-system/todos/ai-first-config-system-tracking.md +++ b/ai-plan/public/ai-first-config-system/todos/ai-first-config-system-tracking.md @@ -86,6 +86,7 @@ - `2026-04-17` 之前的详细实现记录与定向验证命令已归档到历史 tracking / trace - active 跟踪文件只保留当前恢复点、当前状态和下一步,不再重复堆积已完成阶段的完整历史 - 最近验证摘要:`2026-04-30` 已完成 Tooling / Docs reader-facing 收口与工具 parser 边界收紧,详细命令、批次背景与验证结果保留在 trace 的 `2026-04-30` 分阶段记录中 +- PR `#306` follow-up 摘要:已按 latest open review threads 补齐 Generator `anyOf` 对称回归、Tooling schema type 白名单、object-array 直系收集边界,以及 reader-facing docs 的显式 `additionalProperties: false` / adoption guidance 说明;细节和验证命令保留在 trace 的 `2026-04-30` 新增阶段记录中 - PR review 跟进指针:当前分支的 latest review follow-up 与后续本地核验结论以 `ai-first-config-system-trace.md` 为准,active tracking 不再重复展开逐条命令历史 ## 下一步 diff --git a/ai-plan/public/ai-first-config-system/traces/ai-first-config-system-trace.md b/ai-plan/public/ai-first-config-system/traces/ai-first-config-system-trace.md index 1b6139f3..589cb4ff 100644 --- a/ai-plan/public/ai-first-config-system/traces/ai-first-config-system-trace.md +++ b/ai-plan/public/ai-first-config-system/traces/ai-first-config-system-trace.md @@ -205,3 +205,29 @@ 1. 继续盘点 Runtime / Generator / Tooling 三端是否还有类似“工具宽松吞掉、主线不支持”的 schema 形状 2. 若继续做 Tooling lane,优先补 reader-facing 示例或采用路径,而不是继续堆积边界清单 + +### 阶段:PR #306 open threads 收口(AI-FIRST-CONFIG-RP-003) + +- 已重新抓取 PR `#306` 的 latest open review threads,并按“本地仍成立 / 已被当前分支吸收”重新核验 +- 本轮收口重点不是继续扩能力,而是把 open threads 中仍成立的三类问题一次性补齐: + - Generator:补齐 `GF_ConfigSchema_015` 的 `anyOf` 对称负例,避免组合关键字只覆盖 `oneOf` + - Tooling:拒绝未知显式 `type`、收窄 object-array 只遍历当前 editor 直属 items、统一 `contains` hint 文案 + - Docs:把 `additionalProperties: false` 的“必须显式设置为 false”写清,并为工具补最小接入示例、迁移提示与更准确的 raw YAML 回退条件 +- 本轮同时更新了 JS / .NET 回归测试与 active tracking,避免只修 review comment 不保留恢复点 + +### 验证 + +- 2026-04-30:`bun run test`(`tools/gframework-config-tool`) + - 结果:通过(132 tests) + - 备注:新增未知 schema `type` 拒绝、嵌套 object-array 不串层,以及 `contains` hint 文案回归 +- 2026-04-30:`dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~SchemaConfigGeneratorTests"` + - 结果:通过(54 tests) + - 备注:补齐 `Run_Should_Report_Diagnostic_When_Object_Schema_Declares_Unsupported_AnyOf` +- 2026-04-30:`git diff --check` + - 结果:通过 + - 备注:本轮代码与文档改动未引入空白或冲突标记问题 + +### 下一步 + +1. 推送本轮修复后,重新抓取 PR `#306` review 状态,确认哪些 open threads 会被 GitHub 自动折叠或仍需人工回复 +2. 若还有残留 open threads,优先区分“远端未刷新 / 已过时评论 / 仍成立问题”,不要再把 review body 摘要和 latest open threads 混在一起处理 diff --git a/docs/zh-CN/api-reference/index.md b/docs/zh-CN/api-reference/index.md index d1ea7d4f..5573089e 100644 --- a/docs/zh-CN/api-reference/index.md +++ b/docs/zh-CN/api-reference/index.md @@ -13,7 +13,7 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题 2. 再进专题页确认安装、生命周期和推荐接线方式 3. 最后回到源码中的 XML 文档核对具体契约 -如果你在阅读 AI-First 配置工作流相关 API,先把 `GFramework.Game` Runtime 与 `GFramework.Game.SourceGenerators` 视为正式契约入口,再把 `VS Code` 配置工具视为辅助层。当前默认采用路径围绕共享 schema 子集展开,其中 `additionalProperties: false` 表示闭合对象边界,`oneOf` / `anyOf` 不在默认入口范围内;更复杂的 shape 应回到 raw YAML 与 schema 设计本体处理。 +如果你在阅读 AI-First 配置工作流相关 API,先把 `GFramework.Game` Runtime 与 `GFramework.Game.SourceGenerators` 视为正式契约入口,再把 `VS Code` 配置工具视为辅助层。当前默认采用路径围绕共享 schema 子集展开,其中 `additionalProperties: false` 表示闭合对象边界(需显式设置为 `false`);`oneOf` / `anyOf` 在 Runtime / Generator / Tooling 层面会被直接拒绝。更复杂的 shape 应回到 raw YAML 与 schema 设计本体处理。 ## 阅读顺序 diff --git a/docs/zh-CN/game/config-tool.md b/docs/zh-CN/game/config-tool.md index 3f735699..9cdb7c34 100644 --- a/docs/zh-CN/game/config-tool.md +++ b/docs/zh-CN/game/config-tool.md @@ -109,6 +109,97 @@ Explorer + 表单预览。 日常采用时,建议把它理解为“优先用工具加速已支持子集的维护;遇到边界时立刻回到 schema + raw YAML 本体确认”。 +### 最小接入示例与兼容 / 迁移说明 + +项目里至少需要准备三类内容: + +- `config//*.yaml`:实际配置文件 +- `schemas/.schema.json`:与该配置域对应的 schema +- VS Code 工作区里的 `GFramework Config Tool` 扩展,以及与 schema 保持一致的 `x-gframework-ref-table` 引用约定 + +最小目录可以从下面这个形态起步: + +```text +GameProject/ +├─ config/ +│ └─ monster/ +│ └─ slime.yaml +└─ schemas/ + └─ monster.schema.json +``` + +最小 schema 示例: + +```json +{ + "type": "object", + "additionalProperties": false, + "required": ["id", "name", "rarity", "dropItems"], + "properties": { + "id": { + "type": "integer", + "title": "Monster Id", + "description": "Primary monster key.", + "default": 1 + }, + "name": { + "type": "string", + "title": "Display Name", + "minLength": 1 + }, + "rarity": { + "type": "string", + "enum": ["common", "elite", "boss"], + "default": "common" + }, + "spawnTime": { + "type": "string", + "format": "time" + }, + "dropItems": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "rewardTableId": { + "type": "string", + "x-gframework-ref-table": "reward-table" + } + } +} +``` + +对应的 YAML 初始文件可以保持很小: + +```yaml +id: 1 +name: Slime +rarity: common +spawnTime: 08:30:00Z +dropItems: + - potion +rewardTableId: starter-reward +``` + +推荐接入顺序: + +1. 在 VS Code 中打开包含 `config/` 和 `schemas/` 的工作区 +2. 如果目录不是默认值,先设置 `gframeworkConfig.configPath` 与 `gframeworkConfig.schemasPath` +3. 通过 Explorer 打开目标 YAML 或 schema,先跑一次全量校验 +4. 对空 YAML 使用“基于 schema 的示例 YAML 初始化”,或直接从 raw YAML 开始录入 +5. 需要统一改同域顶层标量字段时,再进入批量编辑 + +迁移自纯 raw YAML 工作流时,至少先检查下面几件事: + +- `additionalProperties` 是否显式设置为 `false`;省略或 `true` 不属于当前共享支持子集 +- schema 是否依赖 `oneOf` / `anyOf`;这些组合关键字会被 Runtime / Generator / Tooling 直接拒绝 +- 对象数组里是否混入标量项,或是否存在更深、更异构的数组结构 +- Runtime / Source Generator 是否已经接受这份 schema,而不是只有编辑器里“暂时看起来能写” + +当 schema 仍在共享支持子集内,但某段编辑路径已经超出轻量表单可视化边界时,优先回到 raw YAML;不要把“工具暂时没有表单入口”误判成“运行时契约已放宽”。 + ## 推荐工作流 ### 1. 浏览配置与 schema diff --git a/docs/zh-CN/game/data.md b/docs/zh-CN/game/data.md index 843ea4e5..bbf9dc7a 100644 --- a/docs/zh-CN/game/data.md +++ b/docs/zh-CN/game/data.md @@ -24,7 +24,7 @@ description: 以当前 GFramework.Game 源码与 PersistenceTests 为准,说 - 配置系统的 schema 支持边界,仍以 `GFramework.Game` Runtime 和 `GFramework.Game.SourceGenerators` 当前共享的 schema 子集为准 - `DataRepository`、`UnifiedSettingsDataRepository` 和 `SaveRepository` 负责的是数据怎么落盘、怎么回读、怎么组织槽位,而不是放宽配置契约 -- 如果配置设计依赖 `oneOf`、`anyOf`、非 `false` 的 `additionalProperties`,或其他更复杂的 schema shape,应直接回到 [配置系统](./config-system.md) 与 raw YAML / schema 本体继续处理,而不是期待 repository 层自动接管这些边界 +- 如果配置设计依赖 `oneOf`、`anyOf`、非 `false` 的 `additionalProperties`(例如省略或 `true`),或其他更复杂的 schema shape,应直接回到 [配置系统](./config-system.md) 与 raw YAML / schema 本体继续处理,而不是期待 repository 层自动接管这些边界 ## 什么时候用哪个仓库 diff --git a/docs/zh-CN/game/index.md b/docs/zh-CN/game/index.md index c534fd80..69eb146d 100644 --- a/docs/zh-CN/game/index.md +++ b/docs/zh-CN/game/index.md @@ -86,7 +86,7 @@ IStorage storage = new FileStorage("GameData", serializer); 这条工作流的正式契约,以 `GFramework.Game` Runtime 和 `GFramework.Game.SourceGenerators` 当前共享支持的 schema 子集为准。`VS Code` 配置工具主要负责编辑期提示和表单辅助,不单独扩展运行时可接受的 schema 形状。 -开始接入时,建议先把 schema 约束控制在共享子集内,并尽早确认像 `additionalProperties: false` 这类已收口的对象边界,以及 +开始接入时,建议先把 schema 约束控制在共享子集内,并尽早确认像 `additionalProperties: false`(需显式设置为 `false`;省略或 `true` 视为非 `false`)这类已收口的对象边界,以及 `oneOf` / `anyOf` 当前会被直接拒绝,而不是在工具里看起来“可以先写”。如果你的配置模型需要更深层的嵌套数组、联合分支或其他超出共享子集的复杂 shape,优先回到 raw YAML 和 schema 设计本体处理,再决定是否拆分结构或调整约束方式。 diff --git a/docs/zh-CN/game/serialization.md b/docs/zh-CN/game/serialization.md index 762a435c..8397b004 100644 --- a/docs/zh-CN/game/serialization.md +++ b/docs/zh-CN/game/serialization.md @@ -148,14 +148,14 @@ var restored = serializer.Deserialize(json, data.GetType()); 如果你的目标是静态内容配置表,而不是运行时持久化对象,请改看 [配置系统](./config-system.md)。 -如果你在配置系统里进一步碰到更复杂的 schema shape,也要尽快回到配置系统主文档和 raw YAML / schema 本体继续设计。当前默认采用路径面向的是与 `GFramework.Game` Runtime 和 `Game.SourceGenerators` 对齐的共享 schema 子集,不是任意 `JSON Schema` 的全量支持。 +如果你在配置系统里进一步碰到更复杂的 schema shape,也要尽快回到配置系统主文档和 raw YAML / schema 本体继续设计。当前默认采用路径面向的是与 `GFramework.Game` Runtime 和 `GFramework.Game.SourceGenerators` 对齐的共享 schema 子集,不是任意 `JSON Schema` 的全量支持。 ## 当前边界 - 当前公开默认实现只有 JSON,没有内建 MessagePack、Binary 或 ProtoBuf 实现 - `JsonSerializer` 负责序列化,不负责对象版本迁移;版本迁移属于 `SettingsModel` 或 `SaveRepository` - 序列化器共享后应视为只读配置对象,避免在运行期继续修改 settings / converters -- 如果配置设计依赖 `oneOf`、`anyOf`、非 `false` 的 `additionalProperties`,或其他需要开放对象形状与联合分支的复杂约束,请直接按配置系统主文档回到 raw YAML / schema 方案处理,而不是把这些场景归到序列化层 +- 如果配置设计依赖 `oneOf`、`anyOf`、非 `false` 的 `additionalProperties`(例如省略或 `true`),或其他需要开放对象形状与联合分支的复杂约束,请直接按配置系统主文档回到 raw YAML / schema 方案处理,而不是把这些场景归到序列化层 ## 继续阅读 diff --git a/docs/zh-CN/game/setting.md b/docs/zh-CN/game/setting.md index a1e4d6cf..be96f510 100644 --- a/docs/zh-CN/game/setting.md +++ b/docs/zh-CN/game/setting.md @@ -23,7 +23,7 @@ description: 以当前 SettingsModel、SettingsSystem 与相关测试为准, 如果你关注的是“配置内容最后怎么变成运行时设置”,这里也需要先分清职责: - 配置 schema 的正式支持边界,仍以 `GFramework.Game` Runtime 和 `GFramework.Game.SourceGenerators` 当前共享的 schema 子集为准 -- `UnifiedSettingsDataRepository`、`SettingsModel` 和 `SettingsSystem` 负责设置数据的加载、迁移、保存与应用,不负责放宽 `oneOf`、`anyOf`、非 `false` 的 `additionalProperties` 等配置边界 +- `UnifiedSettingsDataRepository`、`SettingsModel` 和 `SettingsSystem` 负责设置数据的加载、迁移、保存与应用,不负责放宽 `oneOf`、`anyOf`、非 `false` 的 `additionalProperties`(例如省略或 `true`)等配置边界 - 一旦配置设计开始依赖更复杂的 schema shape,应直接回到 [配置系统](./config-system.md) 与 raw YAML / schema 本体处理,再决定设置层怎么消费这些结果 ## 当前公开入口 diff --git a/docs/zh-CN/game/storage.md b/docs/zh-CN/game/storage.md index 7f5778c8..901c9c33 100644 --- a/docs/zh-CN/game/storage.md +++ b/docs/zh-CN/game/storage.md @@ -174,7 +174,7 @@ var cacheStorage = new ScopedStorage(rootStorage, "runtime-cache"); - `ScopedStorage` 只做 key 前缀,不做权限、事务或迁移控制 - 锁粒度是“当前实例内的目标路径”,不是跨进程文件锁 - 原子写入只覆盖单文件替换,不等于多文件事务 -- 如果配置建模依赖 `oneOf`、`anyOf`、非 `false` 的 `additionalProperties`,或其他超出当前共享 schema 子集的复杂组合约束,这不是 `IStorage` 层能放宽的限制;应直接回到配置系统主文档与 raw YAML / schema 设计处理 +- 如果配置建模依赖 `oneOf`、`anyOf`、非 `false` 的 `additionalProperties`(例如省略或 `true`),或其他超出当前共享 schema 子集的复杂组合约束,这不是 `IStorage` 层能放宽的限制;应直接回到配置系统主文档与 raw YAML / schema 设计处理 ## 继续阅读 diff --git a/docs/zh-CN/source-generators/index.md b/docs/zh-CN/source-generators/index.md index d65147be..e4f1981a 100644 --- a/docs/zh-CN/source-generators/index.md +++ b/docs/zh-CN/source-generators/index.md @@ -61,7 +61,7 @@ GFramework 当前发布的生成器包是: 当前不属于默认采用路径的典型情况包括: - `oneOf`、`anyOf` 这类会改变生成类型形状的组合关键字 -- 非 `false` 的 `additionalProperties` +- 非 `false` 的 `additionalProperties`(例如省略或 `true`) - 其他需要开放对象形状、联合分支或更自由属性合并的 schema 设计 这些场景当前不应被理解为“文档还没写到的隐藏支持”,而应被理解为:它们不在 `GFramework.Game` 现阶段共享配置契约内。 diff --git a/tools/gframework-config-tool/README.md b/tools/gframework-config-tool/README.md index e237ec2c..1f0473a2 100644 --- a/tools/gframework-config-tool/README.md +++ b/tools/gframework-config-tool/README.md @@ -109,11 +109,21 @@ This extension is an editor-side helper. It does not define the runtime contract 5. Open the lightweight form preview or domain batch editing actions, then fall back to raw YAML for deeper nested edits when needed. +Minimal adoption checklist: + +- Keep one workspace folder that contains both `config/` and `schemas/` +- Place each config domain under `config//*.yaml` +- Place the matching schema at `schemas/.schema.json` +- Use `x-gframework-ref-table` only on fields that should link to another config domain or reference file +- Keep `additionalProperties` explicitly set to `false` when you need closed-object validation; omitting it or setting + it to `true` is outside the supported subset + Use raw YAML directly when you need: - deeper or more heterogeneous array shapes -- object rules centered on `allOf`, `dependentSchemas`, or object-focused `if` / `then` / `else` -- `contains` / `minContains` / `maxContains` verification on structures that are easier to reason about in source form +- supported object rules such as `allOf`, `dependentSchemas`, or object-focused `if` / `then` / `else` only when they + push the edit path beyond the lightweight form boundary +- `contains` / `minContains` / `maxContains` when the structure is easier to reason about directly in YAML - schema designs outside the current shared subset, including `oneOf`, `anyOf`, or non-`false` `additionalProperties` ## Documentation diff --git a/tools/gframework-config-tool/src/configValidation.js b/tools/gframework-config-tool/src/configValidation.js index 19a43b0b..07bbc671 100644 --- a/tools/gframework-config-tool/src/configValidation.js +++ b/tools/gframework-config-tool/src/configValidation.js @@ -19,6 +19,7 @@ const DurationFormatPattern = const TimeFormatPattern = /^(?\d{2}):(?\d{2}):(?\d{2})(?\.\d+)?(?Z|[+-]\d{2}:\d{2})$/u; const SupportedStringFormats = new Set(["date", "date-time", "duration", "email", "time", "uri", "uuid"]); +const SupportedSchemaTypes = new Set(["object", "array", "string", "integer", "number", "boolean"]); /** * Compare two strings using the same UTF-16 code-unit ordering as C#'s @@ -1104,7 +1105,7 @@ function parseSchemaNode(rawNode, displayPath) { validateUnsupportedOpenObjectKeyword(value, displayPath); - const type = typeof value.type === "string" ? value.type : "object"; + const type = resolveSupportedSchemaType(value.type, displayPath); const patternMetadata = normalizeSchemaPattern(value.pattern, displayPath); const stringFormat = normalizeSchemaStringFormat(value.format, type, displayPath); const negatedSchemaNode = parseNegatedSchemaNode(value.not, displayPath); @@ -1298,13 +1299,7 @@ function validateUnsupportedOpenObjectKeyword(schemaNode, displayPath) { * @returns {SchemaNode} Parsed child schema node. */ function parseRequiredArrayChildSchema(rawChild, displayPath, keywordName) { - const childNode = parseArrayChildSchema(rawChild, displayPath, keywordName); - if (childNode) { - return childNode; - } - - throw new Error( - `Schema property '${displayPath}' must declare '${keywordName}' as an object-valued schema with an explicit 'type'.`); + return parseArrayChildSchema(rawChild, displayPath, keywordName); } /** @@ -1316,13 +1311,7 @@ function parseRequiredArrayChildSchema(rawChild, displayPath, keywordName) { * @returns {SchemaNode | undefined} Parsed child schema node. */ function parseOptionalArrayChildSchema(rawChild, displayPath, keywordName) { - const childNode = parseArrayChildSchema(rawChild, displayPath, keywordName); - if (childNode) { - return childNode; - } - - throw new Error( - `Schema property '${displayPath}' must declare '${keywordName}' as an object-valued schema with an explicit 'type'.`); + return parseArrayChildSchema(rawChild, displayPath, keywordName); } /** @@ -1333,18 +1322,40 @@ function parseOptionalArrayChildSchema(rawChild, displayPath, keywordName) { * @param {unknown} rawChild Raw child schema node. * @param {string} displayPath Logical parent array path. * @param {"items" | "contains"} keywordName Child schema keyword. - * @returns {SchemaNode | undefined} Parsed child schema node. + * @returns {SchemaNode} Parsed child schema node. */ function parseArrayChildSchema(rawChild, displayPath, keywordName) { if (!rawChild || typeof rawChild !== "object" || Array.isArray(rawChild)) { - return undefined; + throw new Error( + `Schema property '${displayPath}' must declare '${keywordName}' as an object-valued schema with an explicit 'type'.`); } if (typeof rawChild.type !== "string") { - return undefined; + throw new Error( + `Schema property '${displayPath}' must declare '${keywordName}' as an object-valued schema with an explicit 'type'.`); } - return parseSchemaNode(rawChild, joinArrayTemplatePath(displayPath, keywordName)); + return parseSchemaNode(rawChild, joinArrayTemplatePath(displayPath)); +} + +/** + * Resolve one schema type while rejecting explicit strings that the shared + * subset does not support. + * + * @param {unknown} rawType Raw schema type value. + * @param {string} displayPath Logical property path. + * @returns {"object" | "array" | "string" | "integer" | "number" | "boolean"} Supported schema type. + */ +function resolveSupportedSchemaType(rawType, displayPath) { + if (typeof rawType !== "string") { + return "object"; + } + + if (!SupportedSchemaTypes.has(rawType)) { + throw new Error(`Schema property '${displayPath}' declares unsupported type '${rawType}'.`); + } + + return rawType; } /** diff --git a/tools/gframework-config-tool/src/extension.js b/tools/gframework-config-tool/src/extension.js index 1fd71390..ce18e071 100644 --- a/tools/gframework-config-tool/src/extension.js +++ b/tools/gframework-config-tool/src/extension.js @@ -1009,8 +1009,14 @@ function renderFormHtml(fileName, schemaInfo, parsedYaml, options) { current = current[segment]; } } + function getDirectObjectArrayItems(editor) { + const itemsHost = editor.querySelector(":scope > [data-object-array-items]"); + return itemsHost + ? Array.from(itemsHost.querySelectorAll(":scope > [data-object-array-item]")) + : []; + } function renumberObjectArrayItems(editor) { - const items = editor.querySelectorAll("[data-object-array-items] > [data-object-array-item]"); + const items = getDirectObjectArrayItems(editor); items.forEach((item, index) => { const title = item.querySelector(".object-array-item-title"); if (title) { @@ -1023,7 +1029,7 @@ function renderFormHtml(fileName, schemaInfo, parsedYaml, options) { } function collectObjectArrayEditorItems(editor) { const items = []; - for (const item of editor.querySelectorAll("[data-object-array-items] > [data-object-array-item]")) { + for (const item of getDirectObjectArrayItems(editor)) { items.push(collectObjectArrayItemValue(item)); } diff --git a/tools/gframework-config-tool/src/localization.js b/tools/gframework-config-tool/src/localization.js index 736cae20..605504cc 100644 --- a/tools/gframework-config-tool/src/localization.js +++ b/tools/gframework-config-tool/src/localization.js @@ -247,7 +247,7 @@ const zhCnMessages = { "webview.hint.format": "格式:{value}", "webview.hint.minItems": "最少元素数:{value}", "webview.hint.maxItems": "最多元素数:{value}", - "webview.hint.contains": "Contains 条件:{summary}", + "webview.hint.contains": "contains 条件:{summary}", "webview.hint.minContains": "最少匹配数:{value}", "webview.hint.maxContains": "最多匹配数:{value}", "webview.hint.uniqueItems": "元素必须唯一", diff --git a/tools/gframework-config-tool/test/configValidation.test.js b/tools/gframework-config-tool/test/configValidation.test.js index 056df511..b31b6adc 100644 --- a/tools/gframework-config-tool/test/configValidation.test.js +++ b/tools/gframework-config-tool/test/configValidation.test.js @@ -225,6 +225,21 @@ test("parseSchemaContent should reject unsupported additionalProperties forms", /unsupported 'additionalProperties' metadata/u); }); +test("parseSchemaContent should reject unsupported explicit schema types", () => { + assert.throws( + () => parseSchemaContent(` + { + "type": "object", + "properties": { + "reward": { + "type": "bogus" + } + } + } + `), + /declares unsupported type 'bogus'/u); +}); + test("parseSchemaContent should build object const comparable keys with ordinal property ordering", () => { const schema = parseSchemaContent(` { @@ -2701,6 +2716,49 @@ test("applyFormUpdates should rewrite nested object arrays from structured form assert.match(updated, /^ value: true$/mu); }); +test("applyFormUpdates should not mix nested object-array items into the parent array", () => { + const updated = applyFormUpdates( + [ + "phases:", + " -", + " wave: 1" + ].join("\n"), + { + objectArrays: { + phases: [ + { + wave: "1", + spawns: [ + { + monsterId: "slime" + }, + { + monsterId: "goblin" + } + ] + }, + { + wave: "2", + spawns: [ + { + monsterId: "bat" + } + ] + } + ] + } + }); + + assert.equal((updated.match(/^ -$/gmu) || []).length, 2); + assert.equal((updated.match(/^ -$/gmu) || []).length, 3); + assert.doesNotMatch(updated, /^ monsterId: slime$/mu); + assert.doesNotMatch(updated, /^ monsterId: goblin$/mu); + assert.match(updated, /^ -$/mu); + assert.match(updated, /^ monsterId: slime$/mu); + assert.match(updated, /^ monsterId: goblin$/mu); + assert.match(updated, /^ monsterId: bat$/mu); +}); + test("applyFormUpdates should clear object arrays when the form removes all items", () => { const updated = applyFormUpdates( [ diff --git a/tools/gframework-config-tool/test/containsSummary.test.js b/tools/gframework-config-tool/test/containsSummary.test.js index 13122cd5..b053b091 100644 --- a/tools/gframework-config-tool/test/containsSummary.test.js +++ b/tools/gframework-config-tool/test/containsSummary.test.js @@ -111,7 +111,7 @@ test("buildContainsHintLines should use updated Chinese contains hint wording", localizer); assert.deepEqual(lines, [ - "Contains 条件:string, 允许值:potion, elixir", + "contains 条件:string, 允许值:potion, elixir", "最少匹配数:1", "最多匹配数:2" ]); diff --git a/tools/gframework-config-tool/test/localization.test.js b/tools/gframework-config-tool/test/localization.test.js index 35010a7d..9493f7af 100644 --- a/tools/gframework-config-tool/test/localization.test.js +++ b/tools/gframework-config-tool/test/localization.test.js @@ -68,6 +68,9 @@ test("createLocalizer should expose contains-count validation keys", () => { assert.equal( chineseLocalizer.t(ValidationMessageKeys.maxContainsViolation, {displayPath: "dropRates", value: 1}), "属性“dropRates”最多只能包含 1 个匹配 contains 条件的元素。"); + assert.equal( + chineseLocalizer.t("webview.hint.contains", {summary: "object, Required: itemCount"}), + "contains 条件:object, Required: itemCount"); }); test("createLocalizer should resolve dependentRequired through the explicit validation key", () => {