From ba75229638ba659117e3b395c4399f07da5914b9 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Fri, 24 Jul 2026 22:47:01 +0800 Subject: [PATCH] fix(tool-todo): declare the unknown-key rejection in the item schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit additionalProperties stays true in the published schema while execute rejected extra keys, so generated typings and validation disagreed with runtime behavior. The item schema now declares additionalProperties: false — the registry's arg validation rejects extra keys with a path-qualified violation before execute runs — and the redundant manual check is dropped (tool catalog regenerated). --- docs/tool-catalog.md | 2 +- packages/todo/tool-todo/src/index.ts | 16 ++++++---------- packages/todo/tool-todo/tests/tool-todo.spec.ts | 2 +- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index c10b0c90a7..864b5378d8 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -898,7 +898,7 @@ Record and update a structured task list for the current work. Send the ENTIRE l "description": "The COMPLETE task list, replacing any previous list.", "items": { "type": "object", - "additionalProperties": true, + "additionalProperties": false, "properties": { "content": { "type": "string", diff --git a/packages/todo/tool-todo/src/index.ts b/packages/todo/tool-todo/src/index.ts index 5297103e77..66b0a8ab12 100644 --- a/packages/todo/tool-todo/src/index.ts +++ b/packages/todo/tool-todo/src/index.ts @@ -28,21 +28,17 @@ const DESCRIPTION = /** * Validate the value constraints the ParameterSchemaSpec can't express and build the canonical {@link - * TodoItem}[]: known keys only, trimmed non-empty unique content, and at most one in-progress - * item. The registry has already enforced the status enum; the cast below records that - * guarantee. Unknown keys are rejected rather than dropped — the logged snapshot must equal - * what the model believes it wrote (model-visible ⟺ logged), so a nested/extended item shape - * fails loud instead of silently flattening. + * TodoItem}[]: trimmed non-empty unique content and at most one in-progress item. The registry + * has already enforced the status enum and rejected unknown item keys (`additionalProperties: + * false` — the logged snapshot must equal what the model believes it wrote, so a nested/extended + * item shape fails loud at the schema boundary instead of silently flattening); the cast below + * records that guarantee. */ function toTodoList(raw: { content: string; status: string }[]): TodoItem[] { const todos: TodoItem[] = [] const seen = new Set() let inProgress = 0 for (const item of raw) { - const unknown = Object.keys(item).filter(key => key !== 'content' && key !== 'status') - if (unknown.length > 0) { - throw new Error(`invalid todo: unknown key(s) ${unknown.map(k => JSON.stringify(k)).join(', ')} — each item is exactly { content, status }`) - } const content = item.content.trim() if (content.length === 0) { throw new Error('invalid todo: `content` must be a non-empty string') @@ -73,7 +69,7 @@ export function apply(ctx: Context): void { description: 'The COMPLETE task list, replacing any previous list.', items: { type: 'object', - additionalProperties: true, + additionalProperties: false, properties: { content: { type: 'string', required: true, description: 'What the task is — a short imperative line.' }, status: { diff --git a/packages/todo/tool-todo/tests/tool-todo.spec.ts b/packages/todo/tool-todo/tests/tool-todo.spec.ts index f83b202d32..2883cfdc02 100644 --- a/packages/todo/tool-todo/tests/tool-todo.spec.ts +++ b/packages/todo/tool-todo/tests/tool-todo.spec.ts @@ -126,7 +126,7 @@ describe('dsh-tool-todo', () => { { label: 'empty content', todos: [{ content: ' ', status: 'pending' }], fragment: 'non-empty' }, { label: 'duplicate content', todos: [{ content: 'dup', status: 'pending' }, { content: 'dup', status: 'completed' }], fragment: 'duplicate' }, { label: 'two in_progress', todos: [{ content: 'a', status: 'in_progress' }, { content: 'b', status: 'in_progress' }], fragment: 'in_progress' }, - { label: 'unknown item keys', todos: [{ content: 'a', status: 'pending', children: [] }], fragment: 'unknown key' }, + { label: 'unknown item keys', todos: [{ content: 'a', status: 'pending', children: [] }], fragment: 'not a declared property' }, ])('rejects $label as an isError result', async ({ todos, fragment }) => { const ctx = await setup() const result = await callTodo(ctx, { todos })