mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
fix(ai-first-config): 收口 PR 306 审查遗留项
- 新增 Generator 与 Tooling 的 anyOf 和坏形状回归覆盖,补齐组合关键字与未知 type 拒绝 - 修复 VS Code 配置工具的 object-array 直属项收集与 contains 文案一致性问题 - 更新 README、Game 文档与工具说明,明确 additionalProperties 显式 false 边界与最小接入路径 - 补充 ai-plan 跟踪与 trace,记录 PR 306 open threads 收口结果和验证摘要
This commit is contained in:
parent
040bcb99e4
commit
e671646a74
@ -1843,6 +1843,54 @@ public class SchemaConfigGeneratorTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证生成器会显式拒绝当前共享子集尚未支持的 <c>anyOf</c>。
|
||||||
|
/// </summary>
|
||||||
|
[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"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证生成器接受显式声明的 <c>additionalProperties: false</c>。
|
/// 验证生成器接受显式声明的 <c>additionalProperties: false</c>。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -77,7 +77,7 @@
|
|||||||
|
|
||||||
如果你采用 AI-First 配置工作流,建议在进入实现前先确认两条边界:
|
如果你采用 AI-First 配置工作流,建议在进入实现前先确认两条边界:
|
||||||
|
|
||||||
- 当前共享子集接受闭合对象边界 `additionalProperties: false`
|
- 当前共享子集接受闭合对象边界 `additionalProperties: false`(需显式设置为 `false`;省略或 `true` 视为不支持)
|
||||||
- `oneOf` / `anyOf` 这类会改变生成类型形状的组合关键字当前会被 Runtime / Generator / Tooling 直接拒绝
|
- `oneOf` / `anyOf` 这类会改变生成类型形状的组合关键字当前会被 Runtime / Generator / Tooling 直接拒绝
|
||||||
|
|
||||||
更复杂的 schema shape 需要先回到 schema 设计与 raw YAML 维护路径,而不是假定编辑器工具存在隐藏支持。
|
更复杂的 schema shape 需要先回到 schema 设计与 raw YAML 维护路径,而不是假定编辑器工具存在隐藏支持。
|
||||||
|
|||||||
@ -86,6 +86,7 @@
|
|||||||
- `2026-04-17` 之前的详细实现记录与定向验证命令已归档到历史 tracking / trace
|
- `2026-04-17` 之前的详细实现记录与定向验证命令已归档到历史 tracking / trace
|
||||||
- active 跟踪文件只保留当前恢复点、当前状态和下一步,不再重复堆积已完成阶段的完整历史
|
- active 跟踪文件只保留当前恢复点、当前状态和下一步,不再重复堆积已完成阶段的完整历史
|
||||||
- 最近验证摘要:`2026-04-30` 已完成 Tooling / Docs reader-facing 收口与工具 parser 边界收紧,详细命令、批次背景与验证结果保留在 trace 的 `2026-04-30` 分阶段记录中
|
- 最近验证摘要:`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 不再重复展开逐条命令历史
|
- PR review 跟进指针:当前分支的 latest review follow-up 与后续本地核验结论以 `ai-first-config-system-trace.md` 为准,active tracking 不再重复展开逐条命令历史
|
||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|||||||
@ -205,3 +205,29 @@
|
|||||||
|
|
||||||
1. 继续盘点 Runtime / Generator / Tooling 三端是否还有类似“工具宽松吞掉、主线不支持”的 schema 形状
|
1. 继续盘点 Runtime / Generator / Tooling 三端是否还有类似“工具宽松吞掉、主线不支持”的 schema 形状
|
||||||
2. 若继续做 Tooling lane,优先补 reader-facing 示例或采用路径,而不是继续堆积边界清单
|
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 混在一起处理
|
||||||
|
|||||||
@ -13,7 +13,7 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题
|
|||||||
2. 再进专题页确认安装、生命周期和推荐接线方式
|
2. 再进专题页确认安装、生命周期和推荐接线方式
|
||||||
3. 最后回到源码中的 XML 文档核对具体契约
|
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 设计本体处理。
|
||||||
|
|
||||||
## 阅读顺序
|
## 阅读顺序
|
||||||
|
|
||||||
|
|||||||
@ -109,6 +109,97 @@ Explorer + 表单预览。
|
|||||||
|
|
||||||
日常采用时,建议把它理解为“优先用工具加速已支持子集的维护;遇到边界时立刻回到 schema + raw YAML 本体确认”。
|
日常采用时,建议把它理解为“优先用工具加速已支持子集的维护;遇到边界时立刻回到 schema + raw YAML 本体确认”。
|
||||||
|
|
||||||
|
### 最小接入示例与兼容 / 迁移说明
|
||||||
|
|
||||||
|
项目里至少需要准备三类内容:
|
||||||
|
|
||||||
|
- `config/<domain>/*.yaml`:实际配置文件
|
||||||
|
- `schemas/<domain>.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
|
### 1. 浏览配置与 schema
|
||||||
|
|||||||
@ -24,7 +24,7 @@ description: 以当前 GFramework.Game 源码与 PersistenceTests 为准,说
|
|||||||
|
|
||||||
- 配置系统的 schema 支持边界,仍以 `GFramework.Game` Runtime 和 `GFramework.Game.SourceGenerators` 当前共享的 schema 子集为准
|
- 配置系统的 schema 支持边界,仍以 `GFramework.Game` Runtime 和 `GFramework.Game.SourceGenerators` 当前共享的 schema 子集为准
|
||||||
- `DataRepository`、`UnifiedSettingsDataRepository` 和 `SaveRepository<TSaveData>` 负责的是数据怎么落盘、怎么回读、怎么组织槽位,而不是放宽配置契约
|
- `DataRepository`、`UnifiedSettingsDataRepository` 和 `SaveRepository<TSaveData>` 负责的是数据怎么落盘、怎么回读、怎么组织槽位,而不是放宽配置契约
|
||||||
- 如果配置设计依赖 `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 层自动接管这些边界
|
||||||
|
|
||||||
## 什么时候用哪个仓库
|
## 什么时候用哪个仓库
|
||||||
|
|
||||||
|
|||||||
@ -86,7 +86,7 @@ IStorage storage = new FileStorage("GameData", serializer);
|
|||||||
这条工作流的正式契约,以 `GFramework.Game` Runtime 和 `GFramework.Game.SourceGenerators` 当前共享支持的 schema
|
这条工作流的正式契约,以 `GFramework.Game` Runtime 和 `GFramework.Game.SourceGenerators` 当前共享支持的 schema
|
||||||
子集为准。`VS Code` 配置工具主要负责编辑期提示和表单辅助,不单独扩展运行时可接受的 schema 形状。
|
子集为准。`VS Code` 配置工具主要负责编辑期提示和表单辅助,不单独扩展运行时可接受的 schema 形状。
|
||||||
|
|
||||||
开始接入时,建议先把 schema 约束控制在共享子集内,并尽早确认像 `additionalProperties: false` 这类已收口的对象边界,以及
|
开始接入时,建议先把 schema 约束控制在共享子集内,并尽早确认像 `additionalProperties: false`(需显式设置为 `false`;省略或 `true` 视为非 `false`)这类已收口的对象边界,以及
|
||||||
`oneOf` / `anyOf` 当前会被直接拒绝,而不是在工具里看起来“可以先写”。如果你的配置模型需要更深层的嵌套数组、联合分支或其他超出共享子集的复杂
|
`oneOf` / `anyOf` 当前会被直接拒绝,而不是在工具里看起来“可以先写”。如果你的配置模型需要更深层的嵌套数组、联合分支或其他超出共享子集的复杂
|
||||||
shape,优先回到 raw YAML 和 schema 设计本体处理,再决定是否拆分结构或调整约束方式。
|
shape,优先回到 raw YAML 和 schema 设计本体处理,再决定是否拆分结构或调整约束方式。
|
||||||
|
|
||||||
|
|||||||
@ -148,14 +148,14 @@ var restored = serializer.Deserialize(json, data.GetType());
|
|||||||
|
|
||||||
如果你的目标是静态内容配置表,而不是运行时持久化对象,请改看 [配置系统](./config-system.md)。
|
如果你的目标是静态内容配置表,而不是运行时持久化对象,请改看 [配置系统](./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 实现
|
- 当前公开默认实现只有 JSON,没有内建 MessagePack、Binary 或 ProtoBuf 实现
|
||||||
- `JsonSerializer` 负责序列化,不负责对象版本迁移;版本迁移属于 `SettingsModel<TRepository>` 或 `SaveRepository<TSaveData>`
|
- `JsonSerializer` 负责序列化,不负责对象版本迁移;版本迁移属于 `SettingsModel<TRepository>` 或 `SaveRepository<TSaveData>`
|
||||||
- 序列化器共享后应视为只读配置对象,避免在运行期继续修改 settings / converters
|
- 序列化器共享后应视为只读配置对象,避免在运行期继续修改 settings / converters
|
||||||
- 如果配置设计依赖 `oneOf`、`anyOf`、非 `false` 的 `additionalProperties`,或其他需要开放对象形状与联合分支的复杂约束,请直接按配置系统主文档回到 raw YAML / schema 方案处理,而不是把这些场景归到序列化层
|
- 如果配置设计依赖 `oneOf`、`anyOf`、非 `false` 的 `additionalProperties`(例如省略或 `true`),或其他需要开放对象形状与联合分支的复杂约束,请直接按配置系统主文档回到 raw YAML / schema 方案处理,而不是把这些场景归到序列化层
|
||||||
|
|
||||||
## 继续阅读
|
## 继续阅读
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,7 @@ description: 以当前 SettingsModel、SettingsSystem 与相关测试为准,
|
|||||||
如果你关注的是“配置内容最后怎么变成运行时设置”,这里也需要先分清职责:
|
如果你关注的是“配置内容最后怎么变成运行时设置”,这里也需要先分清职责:
|
||||||
|
|
||||||
- 配置 schema 的正式支持边界,仍以 `GFramework.Game` Runtime 和 `GFramework.Game.SourceGenerators` 当前共享的 schema 子集为准
|
- 配置 schema 的正式支持边界,仍以 `GFramework.Game` Runtime 和 `GFramework.Game.SourceGenerators` 当前共享的 schema 子集为准
|
||||||
- `UnifiedSettingsDataRepository`、`SettingsModel<TRepository>` 和 `SettingsSystem` 负责设置数据的加载、迁移、保存与应用,不负责放宽 `oneOf`、`anyOf`、非 `false` 的 `additionalProperties` 等配置边界
|
- `UnifiedSettingsDataRepository`、`SettingsModel<TRepository>` 和 `SettingsSystem` 负责设置数据的加载、迁移、保存与应用,不负责放宽 `oneOf`、`anyOf`、非 `false` 的 `additionalProperties`(例如省略或 `true`)等配置边界
|
||||||
- 一旦配置设计开始依赖更复杂的 schema shape,应直接回到 [配置系统](./config-system.md) 与 raw YAML / schema 本体处理,再决定设置层怎么消费这些结果
|
- 一旦配置设计开始依赖更复杂的 schema shape,应直接回到 [配置系统](./config-system.md) 与 raw YAML / schema 本体处理,再决定设置层怎么消费这些结果
|
||||||
|
|
||||||
## 当前公开入口
|
## 当前公开入口
|
||||||
|
|||||||
@ -174,7 +174,7 @@ var cacheStorage = new ScopedStorage(rootStorage, "runtime-cache");
|
|||||||
- `ScopedStorage` 只做 key 前缀,不做权限、事务或迁移控制
|
- `ScopedStorage` 只做 key 前缀,不做权限、事务或迁移控制
|
||||||
- 锁粒度是“当前实例内的目标路径”,不是跨进程文件锁
|
- 锁粒度是“当前实例内的目标路径”,不是跨进程文件锁
|
||||||
- 原子写入只覆盖单文件替换,不等于多文件事务
|
- 原子写入只覆盖单文件替换,不等于多文件事务
|
||||||
- 如果配置建模依赖 `oneOf`、`anyOf`、非 `false` 的 `additionalProperties`,或其他超出当前共享 schema 子集的复杂组合约束,这不是 `IStorage` 层能放宽的限制;应直接回到配置系统主文档与 raw YAML / schema 设计处理
|
- 如果配置建模依赖 `oneOf`、`anyOf`、非 `false` 的 `additionalProperties`(例如省略或 `true`),或其他超出当前共享 schema 子集的复杂组合约束,这不是 `IStorage` 层能放宽的限制;应直接回到配置系统主文档与 raw YAML / schema 设计处理
|
||||||
|
|
||||||
## 继续阅读
|
## 继续阅读
|
||||||
|
|
||||||
|
|||||||
@ -61,7 +61,7 @@ GFramework 当前发布的生成器包是:
|
|||||||
当前不属于默认采用路径的典型情况包括:
|
当前不属于默认采用路径的典型情况包括:
|
||||||
|
|
||||||
- `oneOf`、`anyOf` 这类会改变生成类型形状的组合关键字
|
- `oneOf`、`anyOf` 这类会改变生成类型形状的组合关键字
|
||||||
- 非 `false` 的 `additionalProperties`
|
- 非 `false` 的 `additionalProperties`(例如省略或 `true`)
|
||||||
- 其他需要开放对象形状、联合分支或更自由属性合并的 schema 设计
|
- 其他需要开放对象形状、联合分支或更自由属性合并的 schema 设计
|
||||||
|
|
||||||
这些场景当前不应被理解为“文档还没写到的隐藏支持”,而应被理解为:它们不在 `GFramework.Game` 现阶段共享配置契约内。
|
这些场景当前不应被理解为“文档还没写到的隐藏支持”,而应被理解为:它们不在 `GFramework.Game` 现阶段共享配置契约内。
|
||||||
|
|||||||
@ -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
|
5. Open the lightweight form preview or domain batch editing actions, then fall back to raw YAML for deeper nested edits
|
||||||
when needed.
|
when needed.
|
||||||
|
|
||||||
|
Minimal adoption checklist:
|
||||||
|
|
||||||
|
- Keep one workspace folder that contains both `config/` and `schemas/`
|
||||||
|
- Place each config domain under `config/<domain>/*.yaml`
|
||||||
|
- Place the matching schema at `schemas/<domain>.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:
|
Use raw YAML directly when you need:
|
||||||
|
|
||||||
- deeper or more heterogeneous array shapes
|
- deeper or more heterogeneous array shapes
|
||||||
- object rules centered on `allOf`, `dependentSchemas`, or object-focused `if` / `then` / `else`
|
- supported object rules such as `allOf`, `dependentSchemas`, or object-focused `if` / `then` / `else` only when they
|
||||||
- `contains` / `minContains` / `maxContains` verification on structures that are easier to reason about in source form
|
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`
|
- schema designs outside the current shared subset, including `oneOf`, `anyOf`, or non-`false` `additionalProperties`
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|||||||
@ -19,6 +19,7 @@ const DurationFormatPattern =
|
|||||||
const TimeFormatPattern =
|
const TimeFormatPattern =
|
||||||
/^(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?<fraction>\.\d+)?(?<offset>Z|[+-]\d{2}:\d{2})$/u;
|
/^(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?<fraction>\.\d+)?(?<offset>Z|[+-]\d{2}:\d{2})$/u;
|
||||||
const SupportedStringFormats = new Set(["date", "date-time", "duration", "email", "time", "uri", "uuid"]);
|
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
|
* 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);
|
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 patternMetadata = normalizeSchemaPattern(value.pattern, displayPath);
|
||||||
const stringFormat = normalizeSchemaStringFormat(value.format, type, displayPath);
|
const stringFormat = normalizeSchemaStringFormat(value.format, type, displayPath);
|
||||||
const negatedSchemaNode = parseNegatedSchemaNode(value.not, displayPath);
|
const negatedSchemaNode = parseNegatedSchemaNode(value.not, displayPath);
|
||||||
@ -1298,13 +1299,7 @@ function validateUnsupportedOpenObjectKeyword(schemaNode, displayPath) {
|
|||||||
* @returns {SchemaNode} Parsed child schema node.
|
* @returns {SchemaNode} Parsed child schema node.
|
||||||
*/
|
*/
|
||||||
function parseRequiredArrayChildSchema(rawChild, displayPath, keywordName) {
|
function parseRequiredArrayChildSchema(rawChild, displayPath, keywordName) {
|
||||||
const childNode = parseArrayChildSchema(rawChild, displayPath, keywordName);
|
return 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'.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1316,13 +1311,7 @@ function parseRequiredArrayChildSchema(rawChild, displayPath, keywordName) {
|
|||||||
* @returns {SchemaNode | undefined} Parsed child schema node.
|
* @returns {SchemaNode | undefined} Parsed child schema node.
|
||||||
*/
|
*/
|
||||||
function parseOptionalArrayChildSchema(rawChild, displayPath, keywordName) {
|
function parseOptionalArrayChildSchema(rawChild, displayPath, keywordName) {
|
||||||
const childNode = parseArrayChildSchema(rawChild, displayPath, keywordName);
|
return 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'.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1333,18 +1322,40 @@ function parseOptionalArrayChildSchema(rawChild, displayPath, keywordName) {
|
|||||||
* @param {unknown} rawChild Raw child schema node.
|
* @param {unknown} rawChild Raw child schema node.
|
||||||
* @param {string} displayPath Logical parent array path.
|
* @param {string} displayPath Logical parent array path.
|
||||||
* @param {"items" | "contains"} keywordName Child schema keyword.
|
* @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) {
|
function parseArrayChildSchema(rawChild, displayPath, keywordName) {
|
||||||
if (!rawChild || typeof rawChild !== "object" || Array.isArray(rawChild)) {
|
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") {
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1009,8 +1009,14 @@ function renderFormHtml(fileName, schemaInfo, parsedYaml, options) {
|
|||||||
current = current[segment];
|
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) {
|
function renumberObjectArrayItems(editor) {
|
||||||
const items = editor.querySelectorAll("[data-object-array-items] > [data-object-array-item]");
|
const items = getDirectObjectArrayItems(editor);
|
||||||
items.forEach((item, index) => {
|
items.forEach((item, index) => {
|
||||||
const title = item.querySelector(".object-array-item-title");
|
const title = item.querySelector(".object-array-item-title");
|
||||||
if (title) {
|
if (title) {
|
||||||
@ -1023,7 +1029,7 @@ function renderFormHtml(fileName, schemaInfo, parsedYaml, options) {
|
|||||||
}
|
}
|
||||||
function collectObjectArrayEditorItems(editor) {
|
function collectObjectArrayEditorItems(editor) {
|
||||||
const items = [];
|
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));
|
items.push(collectObjectArrayItemValue(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -247,7 +247,7 @@ const zhCnMessages = {
|
|||||||
"webview.hint.format": "格式:{value}",
|
"webview.hint.format": "格式:{value}",
|
||||||
"webview.hint.minItems": "最少元素数:{value}",
|
"webview.hint.minItems": "最少元素数:{value}",
|
||||||
"webview.hint.maxItems": "最多元素数:{value}",
|
"webview.hint.maxItems": "最多元素数:{value}",
|
||||||
"webview.hint.contains": "Contains 条件:{summary}",
|
"webview.hint.contains": "contains 条件:{summary}",
|
||||||
"webview.hint.minContains": "最少匹配数:{value}",
|
"webview.hint.minContains": "最少匹配数:{value}",
|
||||||
"webview.hint.maxContains": "最多匹配数:{value}",
|
"webview.hint.maxContains": "最多匹配数:{value}",
|
||||||
"webview.hint.uniqueItems": "元素必须唯一",
|
"webview.hint.uniqueItems": "元素必须唯一",
|
||||||
|
|||||||
@ -225,6 +225,21 @@ test("parseSchemaContent should reject unsupported additionalProperties forms",
|
|||||||
/unsupported 'additionalProperties' metadata/u);
|
/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", () => {
|
test("parseSchemaContent should build object const comparable keys with ordinal property ordering", () => {
|
||||||
const schema = parseSchemaContent(`
|
const schema = parseSchemaContent(`
|
||||||
{
|
{
|
||||||
@ -2701,6 +2716,49 @@ test("applyFormUpdates should rewrite nested object arrays from structured form
|
|||||||
assert.match(updated, /^ value: true$/mu);
|
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", () => {
|
test("applyFormUpdates should clear object arrays when the form removes all items", () => {
|
||||||
const updated = applyFormUpdates(
|
const updated = applyFormUpdates(
|
||||||
[
|
[
|
||||||
|
|||||||
@ -111,7 +111,7 @@ test("buildContainsHintLines should use updated Chinese contains hint wording",
|
|||||||
localizer);
|
localizer);
|
||||||
|
|
||||||
assert.deepEqual(lines, [
|
assert.deepEqual(lines, [
|
||||||
"Contains 条件:string, 允许值:potion, elixir",
|
"contains 条件:string, 允许值:potion, elixir",
|
||||||
"最少匹配数:1",
|
"最少匹配数:1",
|
||||||
"最多匹配数:2"
|
"最多匹配数:2"
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -68,6 +68,9 @@ test("createLocalizer should expose contains-count validation keys", () => {
|
|||||||
assert.equal(
|
assert.equal(
|
||||||
chineseLocalizer.t(ValidationMessageKeys.maxContainsViolation, {displayPath: "dropRates", value: 1}),
|
chineseLocalizer.t(ValidationMessageKeys.maxContainsViolation, {displayPath: "dropRates", value: 1}),
|
||||||
"属性“dropRates”最多只能包含 1 个匹配 contains 条件的元素。");
|
"属性“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", () => {
|
test("createLocalizer should resolve dependentRequired through the explicit validation key", () => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user