Skip to content

Fix compiler error quality: YAML context offset, import hint, early engine validation#41234

Merged
pelikhan merged 7 commits into
mainfrom
copilot/syntax-error-quality-fix-again
Jun 24, 2026
Merged

Fix compiler error quality: YAML context offset, import hint, early engine validation#41234
pelikhan merged 7 commits into
mainfrom
copilot/syntax-error-quality-fix-again

Conversation

Copilot AI commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Three DX bugs degraded compiler error quality (avg score 44/100 in the daily syntax-error quality check): a code-context display bug that omits the faulty line, generic actionability hints on import errors, and an invalid engine name silently swallowed when imports also fail.

Changes

  • Fix YAML context offset (pkg/workflow/frontmatter_error.go): sourceContextPattern used \s+ which doesn't match the > N | highlighted-error-line prefix. When the error falls on the first frontmatter line (line 2), the entire error line was absent from the snippet. Updated to >?\s*\d+\s*\| so context extraction starts from the > marker line.

    # Before — error line missing
    test-1.md:2:1: error: missing ':' after key …
       3 | engine: copilot
    
    # After — error line included
    test-1.md:2:1: error: missing ':' after key …
    >  2 | private
       3 | engine: copilot
    
  • Tailored import hints (pkg/parser/import_error.go): Added buildImportErrorHint() and populated CompilerError.Hint in FormatImportError. Each cause variant (file not found, download failure, bad ref, invalid workflowspec) now gets a specific actionable hint, e.g.:

    hint: Ensure `shared/skip-if-issue-open.md` exists relative to the workflow directory, or check the `imports:` path.
    
  • Early engine validation (pkg/workflow/compiler_orchestrator_engine.go): Validates engineSetting against engineCatalog.Resolve() immediately after injectBuiltinEngineImportIfNeeded, before import processing. A typo like copiilot now surfaces "Did you mean: copilot?" even when imports also fail — previously the import error shadowed the engine error entirely.

Tests

  • New TestYAMLFormatErrorOutput/error_on_line_2_included_in_snippet — asserts the faulty line appears in the snippet.
  • New TestInvalidEngineReportedBeforeImportErrors — asserts engine validation fires before import resolution.
  • New TestImportNotFoundHint — asserts hint: appears in import-not-found compiler output.
  • Updated TestFormatImportError to assert hint: is present for all import error variants.

pr-sous-chef: rebase and re-run checks
Run: https://github.com/github/gh-aw/actions/runs/28124550476

Generated by 👨‍🍳 PR Sous Chef · 66.4 AIC · ⌖ 1.12 AIC · ⊞ 17.6K ·

… validation

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix compiler error display bug and improve suggestions Fix compiler error quality: YAML context offset, import hint, early engine validation Jun 24, 2026
Copilot AI requested a review from pelikhan June 24, 2026 14:39
@pelikhan pelikhan marked this pull request as ready for review June 24, 2026 14:40
Copilot AI review requested due to automatic review settings June 24, 2026 14:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Improves gh aw compiler error UX by ensuring YAML frontmatter errors include the correct source line, adding actionable hint: text to import errors, and validating engine: early so engine typos aren’t masked by import failures.

Changes:

  • Fix frontmatter source-context extraction so the highlighted > N | line is included in YAML syntax error snippets.
  • Add tailored import-error hints via CompilerError.Hint to improve actionability for common import failure modes.
  • Validate engine: immediately after builtin-engine import injection (and before import processing) to preserve error precedence for invalid engine names.
Show a summary per file
File Description
pkg/workflow/frontmatter_error.go Updates the regex used to locate the start of YAML source-context output so the highlighted error line isn’t dropped.
pkg/workflow/compiler_yaml_test.go Adds regression tests for YAML snippet inclusion, engine-vs-import precedence, and import hint rendering in full compilation output.
pkg/workflow/compiler_orchestrator_engine.go Adds early engine validation before import processing to prevent import failures from shadowing invalid engine errors.
pkg/parser/import_error.go Adds buildImportErrorHint() and populates CompilerError.Hint in FormatImportError.
pkg/parser/import_error_test.go Updates expectations to require hint: output across import error variants.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 5/5 changed files
  • Comments generated: 2

Comment on lines +148 to +161
func buildImportErrorHint(message, importPath string) string {
switch {
case strings.Contains(message, "file not found") || strings.Contains(message, "failed to resolve import"):
return fmt.Sprintf("Ensure `%s` exists relative to the workflow directory, or check the `imports:` path.", importPath)
case strings.Contains(message, "failed to download"):
return fmt.Sprintf("Check that the remote import `%s` is accessible and the path is correct.", importPath)
case strings.Contains(message, "failed to resolve ref"):
return fmt.Sprintf("Verify the ref (branch, tag, or SHA) in `%s` is valid and accessible.", importPath)
case strings.Contains(message, "invalid import specification"):
return "Use the format `owner/repo/path@ref` for remote imports (e.g. `github/my-org/shared.md@main`)."
default:
return fmt.Sprintf("Ensure `%s` exists relative to the workflow directory, or check the `imports:` path.", importPath)
}
}
Comment on lines +604 to +607
// Should include a "Did you mean: copilot?" suggestion
if !strings.Contains(errorStr, "copilot") {
t.Errorf("error should mention 'copilot' as a suggestion, got: %s", errorStr)
}
@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (135 new lines in pkg/, above the 100-line threshold) but did not link an Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/41234-compiler-error-precedence-and-hints.md — review and complete it before merging. It captures the most architectural choice in this diff: validating engine: before import processing so engine typos are no longer shadowed by import errors (plus the cause-specific import hints and the context-snippet offset fix).

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff.
  2. Complete the missing sections — confirm the decision rationale, and sanity-check the alternatives and consequences (especially the two noted limitations: early validation is skipped for --engine overrides / import-supplied engines, and fail-fast precedence means an engine typo + broken import surface in two compile iterations).
  3. Commit the finalized ADR to docs/adr/ on your branch.
  4. Reference the ADR in this PR body, e.g.:

    ADR: ADR-41234: Compiler error reporting — validate engine before imports and enrich errors with hints

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

This PR encodes a new compiler convention — engine errors take precedence over import errors. That ordering is invisible in the code but matters to anyone who later adds a new error kind and has to decide where it slots in. An ADR makes the why searchable and permanent for future contributors (and your future self).

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 41234-compiler-error-precedence-and-hints.md for PR #41234).

🔒 This PR cannot merge until an ADR is linked in the PR body.

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 70.3 AIC · ⌖ 10.6 AIC · ⊞ 11.6K ·

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /diagnose and /tdd — requesting changes for one correctness bug and two follow-up improvements.

📋 Key Themes & Highlights

One correctness bug (blocking)

  • Dead hint for ref errors (import_error.go line 154): the case strings.Contains(message, "failed to resolve ref") branch is unreachable. FormatImportError sets message = "failed to resolve import reference", which contains "failed to resolve import" — so case 1 fires first and users see the wrong "file not found" hint for ref typos. The fix is a one-line change to the case predicate, plus a matching test.

Two improvements (non-blocking but worth fixing)

  • default hint misleads (import_error.go line 159): the fallback returns the same "Ensure the file exists" copy as "file not found". For unknown or unexpected causes it will actively mislead. An empty string or a genuinely generic message is safer.
  • Double engineCatalog.Resolve (compiler_orchestrator_engine.go line 55): the early-validation result is discarded; resolveEngineRuntimeConfig resolves again on every successful build. Cheap and idempotent in practice, but a one-line comment would clarify the intent for future readers.

Positive highlights

  • ✅ The sourceContextPattern regex fix is minimal and correct — >?\s*\d+\s*\| precisely captures the highlighted YAML error line.
  • ✅ 102 new test lines for ~29 net production lines is an excellent ratio.
  • ✅ The TestInvalidEngineReportedBeforeImportErrors test cleanly specifies the ordering invariant.
  • ✅ Early engine guard condition (engineSetting != "" && c.engineOverride == "") is well-documented and handles the "engine from import" case correctly.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 76.6 AIC · ⌖ 8.38 AIC · ⊞ 6.5K

Comment thread pkg/parser/import_error.go Outdated
return fmt.Sprintf("Ensure `%s` exists relative to the workflow directory, or check the `imports:` path.", importPath)
case strings.Contains(message, "failed to download"):
return fmt.Sprintf("Check that the remote import `%s` is accessible and the path is correct.", importPath)
case strings.Contains(message, "failed to resolve ref"):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/diagnose] Dead code: this case will never fire. FormatImportError sets message = "failed to resolve import reference" (line 120 above), which contains "failed to resolve import" — so case 1 matches first, and users see the misleading "Ensure the file exists" hint instead of the intended "Verify the ref" hint.

💡 Fix suggestion

Change the switch case to match the actual message string that FormatImportError produces:

case message == "failed to resolve import reference":
    return fmt.Sprintf("Verify the ref (branch, tag, or SHA) in `%s` is valid and accessible.", importPath)

And add a regression test in TestFormatImportError:

{
    name: "ref resolution error",
    err: &parser.ImportError{
        ImportPath: "owner/repo/file.md@v0.0.0-bad",
        FilePath:   "test.md",
        Line: 3, Column: 3,
        Cause: errors.New("failed to resolve ref v0.0.0-bad: not found"),
    },
    wantContain: []string{"hint:", "ref", "owner/repo/file.md@v0.0.0-bad"},
},

// when engineSetting is empty (engine may come from an import) or when a
// command-line --engine override is active (it will be validated later).
if engineSetting != "" && c.engineOverride == "" {
if _, err := c.engineCatalog.Resolve(engineSetting, engineConfig); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/zoom-out] The result of this early Resolve() is discarded (_, err). On every successful compilation, c.engineCatalog.Resolve() is called a second time further down in resolveEngineRuntimeConfig. If resolution is cheap and idempotent this is fine, but the double call is non-obvious to future readers.

💡 Options

Option A — add a comment acknowledging the double call:

// Eagerly validate the engine name; the resolved result is discarded because
// applyEngineImportDefaults may mutate engineConfig before the final
// resolveEngineRuntimeConfig call.
if _, err := c.engineCatalog.Resolve(engineSetting, engineConfig); err != nil {

Option B — cache and reuse, threading the result through so resolveEngineRuntimeConfig skips the second Resolve when it already has a valid result.

Either way, a brief comment prevents the next reader from wondering whether the double call is intentional.

Comment thread pkg/parser/import_error.go Outdated
case strings.Contains(message, "invalid import specification"):
return "Use the format `owner/repo/path@ref` for remote imports (e.g. `github/my-org/shared.md@main`)."
default:
return fmt.Sprintf("Ensure `%s` exists relative to the workflow directory, or check the `imports:` path.", importPath)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/diagnose] The default case returns the identical hint as the "file not found" branch — "Ensure the file exists locally". When err.Cause doesn't match any known substring (e.g., a permission error, an unexpected network message), the user is actively misled into looking for a missing file.

💡 Suggestion

Return an empty string for unknown causes, or a genuinely generic fallback:

default:
    return "" // no hint for unrecognised errors — don't mislead the user

Or:

default:
    return fmt.Sprintf("Check the `imports:` configuration for `%s`.", importPath)

"test.md:3:3:",
"error:",
"invalid import specification",
"hint:",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] No test case covers the "failed to resolve ref" error path (causeMsg containing "failed to resolve ref"message = "failed to resolve import reference"). This gap is also what let the dead-code bug on import_error.go line 154 slip through — a test asserting "ref" in the hint would have caught it immediately.

💡 Suggested test case to add
{
    name: "ref resolution error",
    err: &parser.ImportError{
        ImportPath: "owner/repo/file.md@v0.0.0-bad",
        FilePath:   "test.md",
        Line:       3,
        Column:     3,
        Cause:      errors.New("failed to resolve ref v0.0.0-bad: not found"),
    },
    yamlContent: `---
on: push
imports:
  - owner/repo/file.md@v0.0.0-bad
---`,
    wantContain: []string{
        "error:",
        "failed to resolve import reference",
        "hint:",
        "ref",
        "owner/repo/file.md@v0.0.0-bad",
    },
},

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 95/100 — Excellent

Analyzed 6 tests across 2 modified test files: 6 design, 0 implementation, 0 guideline violations.

📊 Metrics & Test Classification (6 tests analyzed)
Metric Value
New/modified tests analyzed 6
✅ Design tests (behavioral contracts) 6 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 6 (100%)
Duplicate test clusters 0
Test inflation detected Note — see below
🚨 Coding-guideline violations 0 (no mock libraries, no missing build tags)
Test File Classification Issues Detected
error_on_line_2_included_in_snippet (table row) pkg/workflow/compiler_yaml_test.go:408 ✅ Design
TestInvalidEngineReportedBeforeImportErrors pkg/workflow/compiler_yaml_test.go:567 ✅ Design
TestImportNotFoundHint pkg/workflow/compiler_yaml_test.go:618 ✅ Design
import file_not_found hint assertion pkg/parser/import_error_test.go:36 ✅ Design
import failed_to_download hint assertion pkg/parser/import_error_test.go:59 ✅ Design
import invalid_import_spec hint assertion pkg/parser/import_error_test.go:80 ✅ Design

Go: 6 (*_test.go); JavaScript: 0. Other languages detected but not scored.

Inflation note: compiler_yaml_test.go grew by 102 lines while its named counterpart compiler_yaml.go was unchanged (102:0 ratio for the direct file pair). The new tests intentionally exercise cross-cutting features in compiler_orchestrator_engine.go (+10) and import_error.go (+18) — legitimate integration coverage, not padding. The 5-point reduction reflects the formal ratio exceeding 2:1.

Verdict

Check passed. 0% implementation tests (threshold: 30%). All new tests verify observable behavioral contracts: correct error-source line in YAML snippets, error-message prioritisation when both engine: and imports fail, and actionable hint: text in import error output. Minor: two of three import-hint table rows assert only on hint presence ("hint:") rather than hint content — a small coverage gap worth noting.

References:

🧪 Test quality analysis by Test Quality Sentinel · 61.8 AIC · ⌖ 15.9 AIC · ⊞ 8.4K ·

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Test Quality Sentinel: 95/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). All 6 new/modified tests verify observable behavioral contracts.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

REQUEST_CHANGES — ref-error hint is completely unreachable (compound correctness bug)

The YAML-context offset fix and the early engine validation are correct. The import hint work has a compound bug that makes the ref-specific hint dead code.

Blocking findings

buildImportErrorHint: two independent bugs conspire to make the ref hint unreachable

  1. (existing comment, import_error.go:161) — Case 1's second predicate strings.Contains(message, "failed to resolve import") matches "failed to resolve import reference" (the message produced for ref failures), so case 1 fires and returns the wrong file-not-found hint.

  2. (new, import_error.go:154) — Even if the ordering were fixed by moving case 3 before case 1, case 3 would still never fire. The pattern "failed to resolve ref" is not a substring of "failed to resolve import reference" — the string "import " sits between "resolve " and "ref", breaking the match. strings.Contains("failed to resolve import reference", "failed to resolve ref") == false.

Both bugs must be fixed together. Minimal fix: change case 3's predicate from strings.Contains(message, "failed to resolve ref") to strings.Contains(message, "import reference"), and move it before case 1.

Test quality (new, compiler_yaml_test.go:600)TestInvalidEngineReportedBeforeImportErrors omits the source-location assertion (filename:line:col:) that TestEngineValidationErrorHasFileLocation provides for the normal engine-error path. If formatEngineSetupError is later bypassed, users lose IDE navigation but no test catches it.

(existing comment, compiler_yaml_test.go:607)strings.Contains(errorStr, "copilot") is trivially satisfied by the always-present Example:\nengine: copilot block in the error message; the "Did you mean: copilot?" suggestion line is not actually exercised.

🔎 Code quality review by PR Code Quality Reviewer · 120.3 AIC · ⌖ 7.37 AIC · ⊞ 5.2K

Comment thread pkg/parser/import_error.go Outdated
return fmt.Sprintf("Ensure `%s` exists relative to the workflow directory, or check the `imports:` path.", importPath)
case strings.Contains(message, "failed to download"):
return fmt.Sprintf("Check that the remote import `%s` is accessible and the path is correct.", importPath)
case strings.Contains(message, "failed to resolve ref"):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Case 3 is dead code — its pattern never matches the message produced by FormatImportError: "failed to resolve ref" is not a substring of "failed to resolve import reference", so this branch is unreachable even after fixing the case-ordering issue flagged in the existing review comment.

💡 Explanation and fix

FormatImportError (line 119) normalises ref failures to:

message = "failed to resolve import reference"

But case 3 in buildImportErrorHint checks:

case strings.Contains(message, "failed to resolve ref"):

"failed to resolve ref" is not a substring of "failed to resolve import reference" (the literal string "import " appears between "resolve " and "ref", breaking the match). Confirming: strings.Contains("failed to resolve import reference", "failed to resolve ref") == false.

As a result:

  • Case 1 fires first (because "failed to resolve import reference" does contain "failed to resolve import"), producing the wrong file-not-found hint.
  • Even if the ordering bug were fixed, case 3 still would not fire — the ref-specific hint is completely unreachable.

Minimal fix — align case 3 to the actual message string:

case strings.Contains(message, "import reference"):
    return fmt.Sprintf("Verify the ref (branch, tag, or SHA) in `%s` is valid and accessible.", importPath)

errorStr := err.Error()

// Should report the invalid engine, not the missing import
if !strings.Contains(errorStr, "invalid engine") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

TestInvalidEngineReportedBeforeImportErrors doesn't assert source location — the test verifies the error text but never checks that the error includes a filename:line:col: prefix pointing to the engine: field, which is the DX improvement this test is meant to protect.

💡 Why this matters

The CompileWorkflow path annotates the error via formatEngineSetupError, which looks up the engine: field line and wraps the error with source context. If that wrapper is ever removed or bypassed, users silently lose location info, but this test would still pass.

There is already a TestEngineValidationErrorHasFileLocation test that checks filename:line:col: format for normal engine errors. The same assertion is missing here for the early-validation-with-failing-imports scenario.

Add a check such as:

if !strings.Contains(errorStr, testFile) {
    t.Errorf("error should include the source file path for IDE navigation, got: %s", errorStr)
}
// e.g. verify format "test.md:2:1: error: invalid engine"
if !regexp.MustCompile(`test\.md:\d+:\d+: error:`).MatchString(errorStr) {
    t.Errorf("error should have filename:line:col: format, got: %s", errorStr)
}

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

@github-actions

Copy link
Copy Markdown
Contributor

``
@copilot review all comments and address unresolved review feedback.

Generated by 👨‍🍳 PR Sous Chef · 68.2 AIC · ⌖ 1.04 AIC · ⊞ 17.6K ·

@github-actions

Copy link
Copy Markdown
Contributor

``
Please refresh the branch state, rerun the validation path, and summarize the remaining blocker(s).

Generated by 👨‍🍳 PR Sous Chef · 68.2 AIC · ⌖ 1.04 AIC · ⊞ 17.6K ·

Copilot AI and others added 2 commits June 24, 2026 18:09
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

Done in 626ff06. I addressed the in-scope review feedback, ran local validation (make fmt, make lint, make test-unit, make test, make recompile, make agent-report-progress), and left the PR ready for a human CI re-trigger.

@github-actions

Copy link
Copy Markdown
Contributor

``
@copilot review all comments and address unresolved review feedback.

Please rebase onto main, resolve the remaining review threads, and re-run checks.

Generated by 👨‍🍳 PR Sous Chef · 66.4 AIC · ⌖ 1.12 AIC · ⊞ 17.6K ·

@pelikhan pelikhan merged commit 505dda5 into main Jun 24, 2026
29 checks passed
@pelikhan pelikhan deleted the copilot/syntax-error-quality-fix-again branch June 24, 2026 20:16
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.

3 participants