From 3c8045103c84f1d38041a9629442f396b67175b6 Mon Sep 17 00:00:00 2001 From: "glm-5.1" Date: Tue, 28 Apr 2026 09:08:39 +0000 Subject: [PATCH] fix(frontmatter): normalize depends_on to dependsOn for Rust CLI compatibility YAML frontmatter from the Rust CLI uses depends_on (snake_case) but the TaskInput schema expects dependsOn (camelCase). Without normalization, Value.Clean() strips the unknown key and dependencies are silently lost. Add step 3.5 in parseFrontmatter: if depends_on is present and dependsOn is not, remap the key before schema validation. If both are present, dependsOn wins (camelCase is canonical). --- src/frontmatter/parse.ts | 11 +++++++++++ test/frontmatter.test.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/frontmatter/parse.ts b/src/frontmatter/parse.ts index b3433e5..24ec611 100644 --- a/src/frontmatter/parse.ts +++ b/src/frontmatter/parse.ts @@ -115,6 +115,17 @@ export function parseFrontmatter(markdown: string): TaskInputType { throw new InvalidInputError('', 'YAML frontmatter must be a mapping (object), not a scalar or array'); } + // Step 3.5: Normalize known snake_case aliases to camelCase. + // The YAML format uses snake_case (e.g., `depends_on`) but our schema + // expects camelCase (e.g., `dependsOn`). Without this, `Value.Clean()` + // would strip the unknown snake_case key and the field would be silently + // lost — causing empty dependency lists and broken graphs. + const record = parsed as Record; + if ('depends_on' in record && !('dependsOn' in record)) { + record.dependsOn = record.depends_on; + delete record.depends_on; + } + // Step 4: Clean — strip unknown properties from untrusted input const cleaned = Value.Clean(TaskInput, parsed); diff --git a/test/frontmatter.test.ts b/test/frontmatter.test.ts index 6982fd1..f74b93c 100644 --- a/test/frontmatter.test.ts +++ b/test/frontmatter.test.ts @@ -484,4 +484,44 @@ risk: invalid-value const result = parseFrontmatter(input); expect(result.id).toBe('bom-task'); }); + + // ─── Snake_case compatibility ────────────────────────────────────────── + + it('normalizes depends_on to dependsOn', () => { + const input = `--- +id: snake-task +name: Snake Task +depends_on: + - task-a + - task-b +--- +Body`; + const result = parseFrontmatter(input); + expect(result.dependsOn).toEqual(['task-a', 'task-b']); + }); + + it('prefers dependsOn when both depends_on and dependsOn are present', () => { + const input = `--- +id: both-task +name: Both Task +dependsOn: + - from-camel +depends_on: + - from-snake +--- +Body`; + const result = parseFrontmatter(input); + expect(result.dependsOn).toEqual(['from-camel']); + }); + + it('normalizes depends_on with empty array', () => { + const input = `--- +id: empty-snake +name: Empty Snake +depends_on: [] +--- +Body`; + const result = parseFrontmatter(input); + expect(result.dependsOn).toEqual([]); + }); }); \ No newline at end of file