mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
fix(config-tool): 收紧坏形状 schema 解析边界
- 修复 Tooling 侧 additionalProperties 仅接受 false 的共享边界校验 - 补充数组 items 与 contains 子 schema 必须显式声明 type 的拒绝逻辑 - 更新 ai-plan 恢复摘要与 JS 回归测试验证记录
This commit is contained in:
parent
e8cceac7ae
commit
fdcb11c92c
@ -96,6 +96,10 @@
|
|||||||
- `2026-04-30` Tooling / Docs reader-facing 收口:
|
- `2026-04-30` Tooling / Docs reader-facing 收口:
|
||||||
- `git diff --check -- docs/zh-CN/game/config-tool.md docs/zh-CN/game/config-system.md tools/gframework-config-tool/README.md ai-plan/public/ai-first-config-system/todos/ai-first-config-system-tracking.md ai-plan/public/ai-first-config-system/traces/ai-first-config-system-trace.md`:通过
|
- `git diff --check -- docs/zh-CN/game/config-tool.md docs/zh-CN/game/config-system.md tools/gframework-config-tool/README.md ai-plan/public/ai-first-config-system/todos/ai-first-config-system-tracking.md ai-plan/public/ai-first-config-system/traces/ai-first-config-system-trace.md`:通过
|
||||||
- 已补齐工具能力边界、`additionalProperties: false` / `oneOf` / `anyOf` 说明、工具与 Runtime 契约关系,以及复杂 shape 的 raw YAML 回退路径
|
- 已补齐工具能力边界、`additionalProperties: false` / `oneOf` / `anyOf` 说明、工具与 Runtime 契约关系,以及复杂 shape 的 raw YAML 回退路径
|
||||||
|
- `2026-04-30` Tooling parser 边界收紧:
|
||||||
|
- `bun run test`(`tools/gframework-config-tool`):通过(4 test files)
|
||||||
|
- 已让工具侧对 `additionalProperties` 的共享边界与 Runtime / Generator 对齐,只接受 `additionalProperties: false`
|
||||||
|
- 已让数组 `items` / `contains` 子 schema 必须显式声明 object-shaped 且带 `type`,避免 tuple-array 或缺失类型的坏形状被工具侧宽松吞掉
|
||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
|
|||||||
@ -189,3 +189,25 @@
|
|||||||
|
|
||||||
1. Tooling / Docs 后续若继续推进,优先补真实采用示例,而不是重复扩写边界清单
|
1. Tooling / Docs 后续若继续推进,优先补真实采用示例,而不是重复扩写边界清单
|
||||||
2. 主线代码批次继续以 Runtime / Generator / Tooling 三端共享关键字收口为中心
|
2. 主线代码批次继续以 Runtime / Generator / Tooling 三端共享关键字收口为中心
|
||||||
|
|
||||||
|
## 2026-04-30
|
||||||
|
|
||||||
|
### 阶段:Tooling parser 坏形状拒绝收紧(AI-FIRST-CONFIG-RP-003)
|
||||||
|
|
||||||
|
- 已在 `tools/gframework-config-tool/src/configValidation.js` 收紧工具侧 schema parser 边界
|
||||||
|
- 本轮不是扩 JSON Schema 能力,而是避免工具侧比 Runtime / Generator 更宽松:
|
||||||
|
- `additionalProperties` 现在只接受 `false`
|
||||||
|
- 数组 `items` 必须是 object-shaped 且显式带 `type`
|
||||||
|
- 数组 `contains` 若声明,也必须是 object-shaped 且显式带 `type`
|
||||||
|
- 这样 tuple-array `items: []`、缺失 `type` 的 `contains` 子 schema,以及其他会误导用户以为“工具支持但运行时不支持”的坏形状,会在工具解析阶段直接失败
|
||||||
|
|
||||||
|
### 验证
|
||||||
|
|
||||||
|
- 2026-04-30:`bun run test`(`tools/gframework-config-tool`)
|
||||||
|
- 结果:通过
|
||||||
|
- 备注:新增 JS 回归覆盖 `additionalProperties`、tuple-array `items` 与缺失 `type` 的 `contains`
|
||||||
|
|
||||||
|
### 下一步
|
||||||
|
|
||||||
|
1. 继续盘点 Runtime / Generator / Tooling 三端是否还有类似“工具宽松吞掉、主线不支持”的 schema 形状
|
||||||
|
2. 若继续做 Tooling lane,优先补 reader-facing 示例或采用路径,而不是继续堆积边界清单
|
||||||
|
|||||||
@ -1102,6 +1102,8 @@ function parseSchemaNode(rawNode, displayPath) {
|
|||||||
"The current config schema subset does not support combinators that can change generated type shape.");
|
"The current config schema subset does not support combinators that can change generated type shape.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateUnsupportedOpenObjectKeyword(value, displayPath);
|
||||||
|
|
||||||
const type = typeof value.type === "string" ? value.type : "object";
|
const type = typeof value.type === "string" ? value.type : "object";
|
||||||
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);
|
||||||
@ -1175,15 +1177,19 @@ function parseSchemaNode(rawNode, displayPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === "array") {
|
if (type === "array") {
|
||||||
const itemNode = parseSchemaNode(value.items || {}, joinArrayTemplatePath(displayPath));
|
const itemNode = parseRequiredArrayChildSchema(value.items, displayPath, "items");
|
||||||
const containsNode = value.contains && typeof value.contains === "object"
|
const containsNode = value.contains === undefined
|
||||||
? parseSchemaNode(value.contains, joinArrayTemplatePath(displayPath))
|
? undefined
|
||||||
: undefined;
|
: parseOptionalArrayChildSchema(value.contains, displayPath, "contains");
|
||||||
if (!containsNode &&
|
if (!containsNode &&
|
||||||
(typeof metadata.minContains === "number" || typeof metadata.maxContains === "number")) {
|
(typeof metadata.minContains === "number" || typeof metadata.maxContains === "number")) {
|
||||||
throw new Error(`Schema property '${displayPath}' declares 'minContains' or 'maxContains' without 'contains'.`);
|
throw new Error(`Schema property '${displayPath}' declares 'minContains' or 'maxContains' without 'contains'.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (itemNode.type === "array") {
|
||||||
|
throw new Error(`Schema property '${displayPath}' uses unsupported nested array items.`);
|
||||||
|
}
|
||||||
|
|
||||||
if (containsNode && containsNode.type === "array") {
|
if (containsNode && containsNode.type === "array") {
|
||||||
throw new Error(`Schema property '${displayPath}' uses unsupported nested array 'contains' schemas.`);
|
throw new Error(`Schema property '${displayPath}' uses unsupported nested array 'contains' schemas.`);
|
||||||
}
|
}
|
||||||
@ -1260,6 +1266,87 @@ function parseSchemaNode(rawNode, displayPath) {
|
|||||||
}, value.const, displayPath), value.enum, displayPath);
|
}, value.const, displayPath), value.enum, displayPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reject open-object keyword forms that would drift away from the Runtime and
|
||||||
|
* Source Generator contracts. The current shared subset keeps object fields
|
||||||
|
* closed and only accepts an explicit `additionalProperties: false` reminder.
|
||||||
|
*
|
||||||
|
* @param {Record<string, unknown>} schemaNode Raw schema object.
|
||||||
|
* @param {string} displayPath Logical property path.
|
||||||
|
*/
|
||||||
|
function validateUnsupportedOpenObjectKeyword(schemaNode, displayPath) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(schemaNode, "additionalProperties")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schemaNode.additionalProperties === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Schema property '${displayPath}' uses unsupported 'additionalProperties' metadata. ` +
|
||||||
|
"The current config schema subset only accepts 'additionalProperties: false' so object fields remain closed and strongly typed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse one required array child schema while keeping tooling errors aligned
|
||||||
|
* with the Runtime and Source Generator contracts.
|
||||||
|
*
|
||||||
|
* @param {unknown} rawChild Raw child schema node.
|
||||||
|
* @param {string} displayPath Logical parent array path.
|
||||||
|
* @param {"items" | "contains"} keywordName Child schema keyword.
|
||||||
|
* @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'.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse one optional array child schema when it is present.
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
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'.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse one array child schema only when it is object-shaped and explicitly
|
||||||
|
* typed. This avoids silently treating tuple arrays or malformed child
|
||||||
|
* schemas as empty object nodes.
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
function parseArrayChildSchema(rawChild, displayPath, keywordName) {
|
||||||
|
if (!rawChild || typeof rawChild !== "object" || Array.isArray(rawChild)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof rawChild.type !== "string") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseSchemaNode(rawChild, joinArrayTemplatePath(displayPath, keywordName));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the first combinator keyword that the current shared schema subset
|
* Return the first combinator keyword that the current shared schema subset
|
||||||
* intentionally rejects to keep Runtime / Generator / Tooling behavior aligned.
|
* intentionally rejects to keep Runtime / Generator / Tooling behavior aligned.
|
||||||
|
|||||||
@ -206,6 +206,25 @@ test("parseSchemaContent should reject unsupported oneOf combinators", () => {
|
|||||||
/unsupported combinator keyword 'oneOf'/u);
|
/unsupported combinator keyword 'oneOf'/u);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("parseSchemaContent should reject unsupported additionalProperties forms", () => {
|
||||||
|
assert.throws(
|
||||||
|
() => parseSchemaContent(`
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"reward": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"itemCount": { "type": "integer" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
/unsupported 'additionalProperties' metadata/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(`
|
||||||
{
|
{
|
||||||
@ -1544,6 +1563,45 @@ test("parseSchemaContent should reject nested-array contains schemas", () => {
|
|||||||
/unsupported nested array 'contains' schemas/u);
|
/unsupported nested array 'contains' schemas/u);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("parseSchemaContent should reject array items without an explicit typed object schema", () => {
|
||||||
|
assert.throws(
|
||||||
|
() => parseSchemaContent(`
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"dropRates": {
|
||||||
|
"type": "array",
|
||||||
|
"items": [
|
||||||
|
{ "type": "integer" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
/must declare 'items' as an object-valued schema with an explicit 'type'/u);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parseSchemaContent should reject contains without an explicit typed object schema", () => {
|
||||||
|
assert.throws(
|
||||||
|
() => parseSchemaContent(`
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"dropRates": {
|
||||||
|
"type": "array",
|
||||||
|
"contains": {
|
||||||
|
"const": 5
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
/must declare 'contains' as an object-valued schema with an explicit 'type'/u);
|
||||||
|
});
|
||||||
|
|
||||||
test("parseSchemaContent should reject minContains and maxContains without contains", () => {
|
test("parseSchemaContent should reject minContains and maxContains without contains", () => {
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => parseSchemaContent(`
|
() => parseSchemaContent(`
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user