Skip to content

Preserve declared imports: order in compiled prompt assembly#36825

Merged
pelikhan merged 7 commits into
mainfrom
copilot/fix-import-order-issue
Jun 4, 2026
Merged

Preserve declared imports: order in compiled prompt assembly#36825
pelikhan merged 7 commits into
mainfrom
copilot/fix-import-order-issue

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Jun 4, 2026

The compiler currently reorders effective prompt content when imports: mixes prompt-only files and files that require runtime import handling (e.g., files with steps:), causing agent context to diverge from declared import order. This change preserves import declaration order regardless of whether each import is emitted as inline markdown or {{#runtime-import ...}}.

  • Ordered import representation

    • Added an ordered prompt import model in parser output (PromptImportEntry) to capture per-import prompt contribution in declaration order.
    • Records both forms:
      • ImportPath → emit {{#runtime-import ...}}
      • Markdown → inline at compile time (e.g., parameterized imports)
  • Compiler data flow updates

    • Threaded ordered prompt import entries through orchestrator/workflow compilation structs.
    • Kept legacy fields (ImportPaths, MergedMarkdown) for compatibility and fallback behavior.
    • Fixed fallback robustness so legacy grouped import handling is still used when ordered entries are absent/empty:
      • preserve nil semantics when cloning PromptImports
      • branch to ordered import handling only when len(PromptImports) > 0
  • Prompt generation behavior

    • Updated generatePrompt to consume ordered entries and interleave runtime-import macros and inlined markdown exactly in declared order.
    • Legacy grouped behavior remains as fallback only when ordered data is unavailable.
  • Regression coverage

    • Added a focused test for a mixed-import case:
      • first import emits runtime macro
      • second import is parameterized/inlined
      • compiled prompt preserves that exact order.
    • Added fallback regression tests to ensure:
      • empty ordered import slices still trigger legacy ImportedMarkdown/ImportPaths behavior
      • resolveMarkdownArtifacts preserves nil PromptImports when source data is unavailable
imports:
  - shared/infra.md
  - uses: shared/instructions.md
    with:
      mode: strict

Expected compiled prompt ordering (schematic):

{{#runtime-import shared/infra.md}}
...inlined content from shared/instructions.md with substitutions...
{{#runtime-import .github/workflows/<workflow>.md}}

Copilot AI and others added 3 commits June 4, 2026 05:02
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix import ordering in compiler generated prompt Preserve declared imports: order in compiled prompt assembly Jun 4, 2026
Copilot AI requested a review from pelikhan June 4, 2026 05:05
@pelikhan pelikhan marked this pull request as ready for review June 4, 2026 05:06
Copilot AI review requested due to automatic review settings June 4, 2026 05:06
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

🧪 Test Quality Sentinel completed test quality analysis.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

PR Code Quality Reviewer completed the code quality review.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

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

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the workflow compiler pipeline to preserve the declared imports: order when assembling the compiled prompt, even when some imports are emitted as inline markdown (parameterized imports) and others are emitted as {{#runtime-import ...}} macros.

Changes:

  • Introduces an ordered per-import prompt contribution model (parser.PromptImportEntry) and threads it through compiler structs.
  • Updates prompt generation (generatePrompt) to interleave runtime-import macros and inlined markdown in declaration order, with a legacy fallback path.
  • Adds a regression test covering a mixed runtime + parameterized import ordering case.
Show a summary per file
File Description
pkg/workflow/workflow_builder.go Wires ordered prompt import entries into WorkflowData.
pkg/workflow/imports_inputs_test.go Adds regression test ensuring mixed imports preserve declared ordering.
pkg/workflow/compiler_yaml.go Updates prompt assembly to consume ordered import entries and interleave output.
pkg/workflow/compiler_types.go Extends WorkflowData with PromptImports.
pkg/workflow/compiler_orchestrator_tools.go Threads ordered import entries through markdown/tool processing artifacts.
pkg/parser/import_processor.go Defines PromptImportEntry and adds it to ImportsResult.
pkg/parser/import_field_extractor.go Records ordered prompt contributions while processing imports.
pkg/parser/import_bfs.go Records ordered prompt contributions for agent-file imports.

Copilot's findings

Tip

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

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

Comment on lines 334 to 338
mainWorkflowMarkdown := markdownContent
orchestratorToolsLog.Printf("Main workflow markdown: %d bytes", len(mainWorkflowMarkdown))
importPaths := append([]string{}, importsResult.ImportPaths...)
promptImports := append([]parser.PromptImportEntry{}, importsResult.PromptImports...)
if len(importPaths) > 0 {
Comment on lines +486 to +491
// Step 1a/1b: Process imports in declaration order, interleaving:
// - compile-time inlined markdown (imports with inputs)
// - runtime-import macros (imports without inputs)
// In older workflow data (without PromptImports), fall back to legacy grouped handling.
if data.PromptImports != nil {
compilerYamlLog.Printf("Processing %d ordered prompt import entries", len(data.PromptImports))
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (191 new lines in pkg/) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/36825-preserve-declared-imports-order.md — review and complete it before merging.

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

📋 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 — add context the AI could not infer, refine the decision rationale, and list real alternatives you considered
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-36825: Preserve Declared imports Order

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

❓ Why ADRs Matter

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you. This change introduces a new ordered prompt-import data model (PromptImportEntry) that runs in parallel with the existing ImportPaths / MergedMarkdown fields — exactly the kind of structural decision worth recording.

📋 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., 36825-preserve-declared-imports-order.md for PR #36825).

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

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · opus48 4.1M ·

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

🧪 Test Quality Sentinel Report

⚠️ Test Quality Score: 70/100 — Acceptable

Analyzed 1 test: 1 design (behavioral contract), 0 implementation, 0 guideline violations.

📊 Metrics & Test Classification (1 test analyzed)
Metric Value
New/modified tests analyzed 1
✅ Design tests (behavioral contracts) 1 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 0 (0%)
Duplicate test clusters 0
Test inflation detected No
🚨 Coding-guideline violations 0

Test Classification Details

Test File Classification Issues Detected
TestImportPromptOrderInterleavesRuntimeAndParameterizedImports pkg/workflow/imports_inputs_test.go ✅ Design No error/edge-case path

Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 1 test — unit (//go:build !integration)
  • 🟨 JavaScript (*.test.cjs, *.test.js): 0 tests
⚠️ Flagged Tests — Requires Review (1 issue)

⚠️ TestImportPromptOrderInterleavesRuntimeAndParameterizedImports (pkg/workflow/imports_inputs_test.go)

Classification: Design test (behavioral contract) — but missing error/edge-case coverage
Issue: The test only verifies the happy path (runtime import appears before parameterized import). It does not cover edge cases such as: all-runtime imports, all-parameterized imports, reversed declaration order, or what happens when one import type is empty.
What design invariant does this test enforce? That the declared imports: order is preserved in the compiled prompt assembly when mixing runtime and parameterized imports.
What would break if deleted? A regression in import ordering (exactly the bug this PR fixes) would go undetected.
Suggested improvement: Add a sub-case (or a table-driven extension) that asserts the reverse — e.g., parameterized import declared first, runtime import second — confirming the order is driven by declaration position rather than import type.

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). The one new test covers the core behavioral contract of this PR — import ordering is preserved — but consider adding at least one edge-case scenario as suggested above.

📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

References: §26932042569

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 1M ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

✅ Test Quality Sentinel: 70/100. Test quality is acceptable — 0% of new tests are implementation tests (threshold: 30%). One minor suggestion: consider adding a reversed-order sub-case to the new ordering test for stronger coverage.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

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, /tdd, and /zoom-out — the fix is sound and the overall approach is clean. Leaving a COMMENT review with a few non-blocking suggestions around observability, test coverage, and assertion strength.

📋 Key Themes & Highlights

Key Themes

  • Silent secondary bug fix: The old code had expressionMappings = exprMaps (overwrite) for the ImportedMarkdown case. The new append is correct, but this implicit fix is unacknowledged and untested.
  • Test assertion strength: The ordering test uses strings.Index for relative position, which passes even with unrelated content between the two markers. A regex or tighter window would catch real regressions more reliably.
  • Uncovered inlinedImports code path: The new ordered loop handles inlinedImports: true mode, but no test exercises it.
  • Observability regression: The Inlined import without inputs log line was removed without replacement in the new path.

Positive Highlights

  • ✅ Clean PromptImportEntry union type — simple, purpose-fit, well-commented
  • ✅ Legacy fallback (data.PromptImports == nil) is a good backward-compatibility hedge
  • ✅ Regression test that directly encodes the bug scenario is exactly right — good instinct
  • ✅ Data flow threading across parser → orchestrator → compiler_types → compiler_yaml is tidy and consistent

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 1.6M

}
chunks, exprMaps := extractPromptChunksFromMarkdown(cleaned)
userPromptChunks = append(userPromptChunks, chunks...)
expressionMappings = append(expressionMappings, exprMaps...)
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] Silent bug fix for expressionMappings — the old code used expressionMappings = exprMaps (overwrite), which would have dropped mappings from any previously processed import. The new append is correct, but this implicit fix is untested.

💡 Suggested regression test addition

Consider adding a test case with multiple parameterized imports (uses: ... with: ...) and verifying that expressionMappings from all of them are preserved in the compiled output. Without such a test, a future refactor could silently reintroduce the overwrite.

// two parameterized imports → both expression mappings must survive
imports:
  - uses: shared/a.md
    with: {x: "${{github.event.issue.number}}"}
  - uses: shared/b.md
    with: {y: "${{github.event.issue.title}}"}

expressionMappings = append(expressionMappings, exprMaps...)
continue
}
if entry.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] Zero-value PromptImportEntry{} (both ImportPath and Markdown empty) is silently skipped with no log output. This could hide data flow bugs — e.g., a missed append site in the parser producing empty entries.

💡 Suggestion

Add a warning log before the continue:

if entry.ImportPath == "" {
	compilerYamlLog.Printf("Warning: skipping empty PromptImportEntry (both fields unset)")
	continue
}

This costs nothing at runtime and makes unexpected zero-value entries immediately visible during debugging.

userPromptChunks = append(userPromptChunks, chunks...)
expressionMappings = append(expressionMappings, exprMaps...)
compilerYamlLog.Printf("Inlined import without inputs: %s", importPath)
continue
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 successful inlined-import log line (Inlined import without inputs: %s) was removed in the new path but not replaced. This makes it harder to trace which imports were resolved from disk vs emitted as runtime-import macros.

💡 Suggestion

Restore the log after the successful inline:

compilerYamlLog.Printf("Inlined import without inputs (ordered): %s", importPath)
continue

if parameterizedIdx == -1 {
t.Fatalf("Expected lock file to contain substituted parameterized import content %q", parameterizedContent)
}
if runtimeIdx > parameterizedIdx {
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] The ordering assertion uses strings.Index which only checks relative substring position — it passes even if main workflow content appears between the two imports, or if either string appears in an unrelated part of the lock file (e.g., comments). This makes the test weaker than it looks.

💡 Stronger assertion approach

Consider splitting the lock file on a known separator or searching within a specific YAML block, then asserting the exact sequence. Alternatively, assert a tight window:

// Assert the two markers appear with only whitespace/newlines between them
pattern := regexp.MustCompile(
	`\{\{#runtime-import shared/infra\.md\}\}[\s\S]*?Use mode: strict\.`,
)
if !pattern.MatchString(lockContent) {
	t.Fatalf("expected runtime-import to immediately precede parameterized content")
}

This validates both order and adjacency.

orchestratorToolsLog.Printf("Found %d body-level {{#runtime-import}} directive(s) to promote to lock-file macros", len(bodyImports))
for _, bodyImport := range bodyImports {
importPaths = append(importPaths, bodyImport.Path)
promptImports = append(promptImports, parser.PromptImportEntry{ImportPath: bodyImport.Path})
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] Body-level {{#runtime-import}} directives are always appended to promptImports after frontmatter import entries, so they will always appear at the end of the ordered import sequence. If a workflow body intentionally mixes {{#runtime-import}} macros between paragraphs of its own markdown, this relative ordering may diverge from what the author sees in the source file.

📋 Context

This behaviour is consistent with how importPaths was handled before this PR, so it is not a regression. However, now that promptImports is the canonical ordering mechanism, it is worth a brief comment here documenting the deliberate decision ("body-level macros always follow frontmatter imports") so future contributors don't accidentally change it.

}
}

func TestImportPromptOrderInterleavesRuntimeAndParameterizedImports(t *testing.T) {
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] The new test covers only the inlinedImports: false (normal runtime-import macro) mode. The inlinedImports: true code path in the new ordered loop (lines 512–526 of compiler_yaml.go) is untested — a file-not-found fallback in that path would silently emit a runtime-import macro instead of inlining.

💡 Suggestion

Add a second test case that sets inlined-imports: true in the workflow frontmatter and asserts that the parameterized import content is actually embedded verbatim in the lock file, and that the runtime-import-able file is also embedded (not emitted as a macro).

@pelikhan
Copy link
Copy Markdown
Collaborator

pelikhan commented Jun 4, 2026

@copilot review all comments
Apply copilot-review skill

Copilot AI and others added 2 commits June 4, 2026 05:25
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Jun 4, 2026

@copilot review all comments
Apply copilot-review skill

Addressed in 682d37e. I reviewed the in-scope comments and applied both requested fallback fixes: preserving nil semantics when cloning PromptImports and switching prompt-generation selection to len(data.PromptImports) > 0 so legacy import handling remains active when ordered entries are absent.

@pelikhan
Copy link
Copy Markdown
Collaborator

pelikhan commented Jun 4, 2026

@copilot merge main, recompile, add tests

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Jun 4, 2026

@copilot merge main, recompile, add tests

Done in eb457cb. I merged main, recompiled workflows, and added regression tests for ordered-import fallback behavior (TestGeneratePromptFallsBackWhenPromptImportsIsEmpty and TestResolveMarkdownArtifactsPreservesNilPromptImports).

@pelikhan pelikhan merged commit 6bf5c25 into main Jun 4, 2026
@pelikhan pelikhan deleted the copilot/fix-import-order-issue branch June 4, 2026 05:55
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.

Prompt content from imports with steps: appears after prompt-only imports, breaking declared import order

3 participants