Skip to content

fix(mdcode): report the YAML error for a version an error near it can change - #532

Open
feixufeixu wants to merge 2 commits into
mainfrom
fix/loader/version-past-yaml-error
Open

feixufeixu wants to merge 2 commits into
mainfrom
fix/loader/version-past-yaml-error

Conversation

@feixufeixu

@feixufeixu feixufeixu commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

When a model file is not valid YAML, kcmd still reads its version, because the version decides whether kcmd reads the profiles in the model's GOOGLE blocks and whether profile files may sit beside the model.

Past a YAML error other than a key set twice, the loader read the version from the first line when that line set it alone. A YAML error elsewhere in the file could still change that version. For example, version: "0.2.0" followed by .dev0" read as version 0.2.0. A vanilla model read this way gave no block profiles, so kcmd push --profile prod said the model defines no profiles.

The loader now reads the version past a YAML error only when every error is a key set twice. Otherwise each command reports the YAML error.

Other broken files get the YAML error too. A Google-flavor model with a YAML syntax error gets it from kcmd profiles and kcmd push --profile <name> before any profile is listed or applied. A vanilla model with a YAML syntax error and a profile file beside it gets it instead of the error about profile files beside a vanilla model.

  1. loader.ts: the version is read past a YAML error only when every error is a key set twice.
  2. loader.test.ts: broken version lines that the first-line rule read wrongly.
  3. semantic_model_layout.test.ts and commands.test.ts: a vanilla model with a YAML syntax error no longer counts as vanilla.

… change

Past a YAML error other than a key set twice, the loader reads a model's
version only when the first line sets it alone and the text holds the word
`version` nowhere else. A YAML error on the version's line, or on the lines
between it and the next root key, passed that check. So `version: "0.2.0"`
followed by `  .dev0"` read as version 0.2.0, and
`version: "0.2.0.dev0"#/google"` read as the vanilla version. A second
version key written with an escape, such as `"ver\x73ion"`, also passed when
a YAML error hid it.

A vanilla model read this way gave no block profiles, so
`kcmd push --profile prod` said the model defines no profiles. A
Google-flavor model read as vanilla had its profile files rejected as
sitting beside a vanilla model.

The version is now read only when no YAML error falls from its line to the
start of the next root key, and the text holds no backslash. Each command
then reports the YAML error for these files.
@feixufeixu
feixufeixu requested a review from mmotoyama October 20, 2026 15:56
// Where the next root key starts.
const pairs = (parsed.contents as yaml.YAMLMap).items;
const next = pairs[pairs.findIndex(p => p.value === version) + 1];
const nextKey = yaml.isNode(next?.key) ? next.key.range?.[0] : undefined;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI identified the following bug as high urgency:

Checking that no YAML error falls at or before the start of the next root key misses two cases where the line right after version: can still change what the version is:

  1. A split version on the next line followed by :: When the closing quote on version: comes too early and the next line has a colon and an error on its value—such as version: "0.2.0" followed by .dev0": " (which fixes to version: "0.2.0.dev0" and label: "")—the YAML parser treats .dev0" as the next root key and reports the unclosed-quote error on its value, after that key starts.
  2. A quote left open on the next key: When the root key after version: leaves a quote open that closes on the line after it—such as version: "0.2.0" followed by label: ", .dev0", and x: " (or version: "0.2.0.dev0/google" followed by label: ", 0.2.0.dev0", and x: ")—label swallows the next line as a multiline string and the unclosed-quote error is reported later on x: ".

In both cases, googleBlockProfiles reads the unedited version and returns [] instead of throwing the YAML parse error.

Below is a test case demonstrating the issue:

    test('reports the YAML error when the line after the version holds the rest of the version or an open quote', () => {
      // Setup: broken documents where the line after `version:` either holds
      // the rest of a split version followed by `:` and an unclosed quote, or
      // opens a quote on `label:` that takes in the rest of a split version or
      // a second version and leaves `x:` with an unclosed quote.
      for (const text of [
        `
version: "0.2.0"
.dev0": "
semantic_model:
  - name: m
    custom_extensions:
      - vendor_name: GOOGLE
        data: '{"profiles": [{"name": "prod"}]}'
`,
        `
version: "0.2.0"
label: "
  .dev0"
x: "
semantic_model:
  - name: m
    custom_extensions:
      - vendor_name: GOOGLE
        data: '{"profiles": [{"name": "prod"}]}'
`,
        `
version: "0.2.0.dev0/google"
label: "
  0.2.0.dev0"
x: "
semantic_model:
  - name: m
    custom_extensions:
      - vendor_name: GOOGLE
        data: '{"profiles": [{"name": "prod"}]}'
`,
      ]) {
        expect(() => loadModels(text)).toThrow('could not parse input');
        // Current behavior on this PR: `googleBlockProfiles(text)` returns `[]`
        // because `versionPastYamlError` only checks `err.pos[0] <=
        // next.key.range?.[0]`, which misses errors on `next.value` or a later
        // root key.
        // Expected behavior: throws `Semantic model load error: could not parse
        // input: ...` because fixing the YAML error yields a vanilla model with
        // profile `prod`.
        expect(() => googleBlockProfiles(text)).toThrow(
          'Semantic model load error: could not parse input: ');
      }
    });

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the first-line rule instead of extending it. The loader now reads the version past a YAML error only when every error is a key set twice, so these files report the YAML error, and your three cases are in the version test.

@@ -1630,8 +1636,13 @@ function versionPastYamlError(
if (errors.every(err => err.code === 'DUPLICATE_KEY')) return version.value;
const first = FIRST_LINE_VERSION.exec(text);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI found the following bug (classified as medium urgency):

FIRST_LINE_VERSION still accepts a broken version: line in two cases where fixing the YAML error changes the version:

  1. A quote left open on version: and closed on the same line by #" or a mismatched quote: Unlike the unquoted branch, the quoted branches in FIRST_LINE_VERSION allow # and the opposite quote character. If the version's closing quote is missing and there is no space before #—such as version: "0.2.0.dev0#" (which fixes to version: "0.2.0.dev0" # ")—or a mismatched quote such as version: "0.2.0.dev0'", line 1 parses as "0.2.0.dev0#" / "0.2.0.dev0'" and googleBlockProfiles treats the document as non-vanilla instead of reporting a later unclosed quote.
  2. A lone carriage return inside a # comment on the version: line: Because FIRST_LINE_VERSION uses the m flag, $ matches before a bare \r, whereas the YAML parser only ends a # comment at \n. In version: "0.2.0" #\r.dev0", FIRST_LINE_VERSION reads "0.2.0" while the YAML parser swallows #\r.dev0" into the comment on line 1 with no error on that line.

Here is a test highlighting the problem:

    test('reports the YAML error when the version line has an unspaced comment quote, mismatched quote, or carriage return in a comment', () => {
      // Setup: broken documents where the `version:` line has an unclosed quote
      // closed by `#"` or `'"` on the same line (paired with an unclosed quote
      // inside `semantic_model`), or a lone `\r` after `#` hiding `.dev0"`.
      for (const text of [
        `
version: "0.2.0.dev0#"
semantic_model:
  - name: m
    description: "
    custom_extensions:
      - vendor_name: GOOGLE
        data: '{"profiles": [{"name": "prod"}]}'
`,
        `
version: '0.2.0.dev0#'
semantic_model:
  - name: m
    description: '
    custom_extensions:
      - vendor_name: GOOGLE
        data: '{"profiles": [{"name": "prod"}]}'
`,
        `
version: "0.2.0.dev0'"
semantic_model:
  - name: m
    description: '
    custom_extensions:
      - vendor_name: GOOGLE
        data: '{"profiles": [{"name": "prod"}]}'
`,
        `
version: "0.2.0" #\r.dev0"
semantic_model:
  - name: m
    description: "
    custom_extensions:
      - vendor_name: GOOGLE
        data: '{"profiles": [{"name": "prod"}]}'
`,
      ]) {
        expect(() => loadModels(text)).toThrow('could not parse input');
        // Current behavior on this PR: `googleBlockProfiles(text)` returns `[]`
        // because `FIRST_LINE_VERSION` allows `#` and `'` inside double-quoted
        // versions (`"` inside single-quoted versions) and matches `$` before a
        // lone `\r` under the `m` flag.
        // Expected behavior: throws `Semantic model load error: could not parse
        // input: ...` because fixing the YAML error yields a vanilla model with
        // profile `prod`.
        expect(() => googleBlockProfiles(text)).toThrow(
          'Semantic model load error: could not parse input: ');
      }
    });

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same change fixes these, since the first-line expression is gone. All four files now report the YAML error, and they are in the same test.

…s set twice

A YAML error other than a key set twice could still change the version that
the loader read from the first line. An error on the next root key's value or
on a later key passed the checks. So did a version quote closed after a `#` or
by the other quote, and a comment that runs past a lone carriage return.

The loader now reads the version past a YAML error only when every error is
a key set twice. The first-line expression and its next-key, word-count and
backslash checks are removed.

Other broken files change too. A Google-flavor model with any other YAML
error now gets the YAML error from the block profile lookup. `kcmd profiles`
prints that error alone instead of listing each profile with it, and
`kcmd push --profile prod` prints it instead of saying the model defines no
profiles. A vanilla model with such an error and a profile file beside it
gets the YAML error instead of the error about profile files beside a
vanilla model.

@mmotoyama mmotoyama left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants