Skip to content

Support serial dependency ordering for actions and targets (#552) - #557

Open
lodyai[bot] wants to merge 19 commits into
mainfrom
issue-552-support-serial-dependency-ordering-for-actions-and-targets
Open

Support serial dependency ordering for actions and targets (#552)#557
lodyai[bot] wants to merge 19 commits into
mainfrom
issue-552-support-serial-dependency-ordering-for-actions-and-targets

Conversation

@lodyai

@lodyai lodyai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the approved staged-Ninja-dyndep design for issue #552. Actions and
targets can declare dependency_order: serial while preserving one Ninja
scheduler, shared-work reuse, failure short-circuiting, and unrelated-branch
concurrency.

Closes #552.

User documentation

  • Documents dependency_order: parallel | serial for actions and targets in
    the users' guide, with a complete executable manifest.
  • Defines the serial guarantee and its scope: only direct deps are ordered;
    independently reachable and unrelated work remains concurrent.
  • Documents Ninja 1.10 requirements, generated sidecars, and the reserved
    .netsuke/serial and .netsuke/dyndep namespaces.
  • Adds ADR-010 for the staged-dyndep architecture and updates the design,
    developer, repository-layout, roadmap, contents, and living ExecPlan records.

Review walkthrough

Validation

  • make check-fmt: passed.
  • make typecheck: passed.
  • make lint: passed, including Whitaker.
  • make test: passed; 1,939 tests passed, one skipped, and doctests passed.
  • make markdownlint: passed.
  • make nixie: passed.
  • coderabbit review --agent: completed with zero actionable findings.

References

@sourcery-ai

sourcery-ai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds manifest-level dependency_order support, threads it through IR to Ninja generation, and implements staged Ninja dyndep bundles plus atomic sidecar materialization so serial dependency lists run in declaration order while preserving a single Ninja scheduler and parallel behaviour for other branches.

Sequence diagram for serial dependency Ninja bundle generation and execution

sequenceDiagram
    actor User
    participant Runner as runner.generate_ninja
    participant NinjaGen as ninja_gen.generate_bundle
    participant Dyndep as process.materialize_dyndep_files
    participant Ninja

    User->>Runner: netsuke build / clean / generate
    Runner->>NinjaGen: generate_bundle(graph)
    NinjaGen-->>Runner: GeneratedNinja (build_file, dyndep_files)
    Runner->>Dyndep: materialize_dyndep_files(cli, bundle.dyndep_files())
    Dyndep-->>Runner: dyndep sidecars materialized
    Runner->>Ninja: invoke with bundle.build_file()
    Ninja-->>User: serial deps run in order, parallel elsewhere
Loading

File-Level Changes

Change Details Files
Introduce DependencyOrder on manifests and IR build edges so targets/actions can declare serial or parallel dependency ordering with a default of parallel.
  • Add DependencyOrder enum with parallel/serial to ast Target and wire serde defaults so omission means parallel
  • Thread dependency_order into ir::BuildEdge and re-export it from ir for use in generators and tests
  • Update all BuildEdge constructions in tests and fixtures to set dependency_order explicitly, usually Parallel, to keep compilation and existing behaviour intact
  • Add AST and IR tests ensuring serial/parallel parsing, defaulting, and that declaration order and dependency_order survive lowering from manifest to BuildGraph
src/ast.rs
src/ir/graph.rs
src/ir/from_manifest.rs
src/ir/mod.rs
tests/ir_from_manifest_tests.rs
tests/ast_tests.rs
tests/ast_tests/dependency_order.rs
tests/ir_tests.rs
src/graph_view/tests_support.rs
src/ir/cycle_*.rs
tests/ninja_gen_unit_tests.rs
tests/ninja_gen_integration_tests.rs
tests/ninja_gen_property_tests.rs
Refactor Ninja generation to support serial dependency ordering via staged dyndep bundles and expose a bundle API while keeping existing string-only generation for parallel graphs.
  • Split ninja_gen into a module with a new dyndep submodule and move unit tests to keep files under size limits
  • Add GeneratedNinja and GeneratedDyndep bundle types plus generate_bundle, which emits the main Ninja build file and content-addressed dyndep sidecars
  • Implement staged dyndep lowering: serial edges with multiple implicit_deps get phony gate chains and per-dependency dyndep sidecars under .netsuke/serial and .netsuke/dyndep, with ninja_required_version = 1.10 only when needed
  • Add escape_ninja_path and make join/path_key public(crate) for reuse by dyndep generation
  • Make generate/generate_into reject serial graphs by returning a DyndepFilesRequired error without writing partial output
  • Reserve .netsuke/serial and .netsuke/dyndep namespaces and surface a localized ReservedOutputPath error on collisions
  • Add unit tests for dyndep lowering, gate/sidecar structure, reserved namespace rejection, and adjust snapshots/unit tests to include dependency_order and serial behaviour
src/ninja_gen/mod.rs
src/ninja_gen/dyndep.rs
src/ninja_gen/tests.rs
src/ninja_gen_property_tests.rs
tests/ninja_gen_unit_tests.rs
tests/ninja_gen_integration_tests.rs
tests/serial_dependency_runtime_tests.rs
docs/execplans/issue-552-support-serial-dependency-ordering-for-actions-and-targets.md
Materialize dyndep sidecar files atomically in the runner using capability-based filesystem APIs and route all CLI generation/execution through the new bundle API.
  • Add runner/process/dyndep_files.rs to open the effective Ninja working directory, create .netsuke/dyndep, and atomically write/verify content-addressed sidecars via same-directory temp files and rename
  • Introduce new localized runner.io.dyndep.* messages and keys for create/read/write/rename/corrupt/race errors across all locales and register them in localization keys
  • Wire generate_ninja to use ninja_gen::generate_bundle, call materialize_dyndep_files, and pass only the main build file to NinjaContent
  • Expose materialize_dyndep_files from runner::process and update tests to cover serial bundle generation and sidecar materialization behaviour
  • Ensure sidecar materialization is idempotent and treats mismatched existing content as corruption with guidance to delete only the offending file
src/runner/mod.rs
src/runner/process/mod.rs
src/runner/process/dyndep_files.rs
src/localization/keys.rs
locales/*/messages.ftl
tests/serial_dependency_runtime_tests.rs

Assessment against linked issues

Issue Objective Addressed Explanation
#552 Implement manifest, IR, and Ninja generation support for dependency_order: serial on actions and targets, preserving declaration order in execution, stopping on failure, reusing shared dependencies, and keeping the default parallel behaviour and serialization scoped to the annotated deps list.
#552 Add regression coverage for serial dependency behaviour, including ordering, shared dependencies, failure short-circuiting, and unchanged default parallel behaviour.
#552 Document the new action and target syntax (dependency_order) and its execution semantics for users. The PR adds an internal ExecPlan document and code-level comments but does not update the user-facing guides or syntax documentation requested in the issue’s acceptance criteria.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

codescene-access[bot]

This comment was marked as outdated.

leynos added 9 commits August 11, 2026 23:46
Keep local VT Code state out of version control.
Record the staged dyndep design, generated sidecar contract, and atomic
materialization boundary for ordered target and action dependencies.

Define regression evidence for declaration order, shared work, failure
short-circuiting, default parallelism, and serialization scope. Document the
independent-reachability limit that requires approval before implementation.
Introduce the closed `DependencyOrder::{Parallel, Serial}` enum on
`Target` and carry it on every `BuildEdge`, defaulting to parallel so
existing manifests and generated Ninja remain unchanged. Copy the value
during manifest-to-IR lowering while preserving the declaration order of
`implicit_deps`, and re-export it through the `ir` module.

Cover the new surface with regressions: omission defaults to parallel,
explicit parallel and serial parse on targets and actions, unknown values
such as `sequential` are rejected, and serial targets and actions retain
their order and policy through lowering. Update every direct `BuildEdge`
literal, doctest, and fixture to compile against the widened struct.

Update the execplan progress and discoveries with the validated Ninja
dyndep loading and path-escaping semantics.
Introduce a generated-bundle API so a `serial` `deps` list can be
expressed as a single Ninja invocation. `generate_bundle` lowers each
multi-dependency serial edge into one phony gate and content-addressed
dyndep sidecar per dependency under the reserved `.netsuke/serial` and
`.netsuke/dyndep` namespaces, emits `ninja_required_version = 1.10` only
when gates exist, and keeps ordinary parallel graphs byte-for-byte
identical to string-only generation.

String-only `generate`/`generate_into` now refuse serial graphs with a
localized `DyndepFilesRequired` error before writing any output, and a
user output that collides with the reserved namespace is rejected. Add
localization keys for both errors across every shipped catalogue and
split `src/ninja_gen.rs` into a directory module so no file exceeds the
repository line ceiling.

Cover the staging rules with unit and integration tests: sidecar count
and ordering, version-floor gating, the no-sidecar cases for parallel and
single-element serial lists, per-stage sidecars for repeated
dependencies, and the reserved-path error.
Add `src/runner/process/dyndep_files.rs`, which writes each generated
sidecar beneath `.netsuke/dyndep` in the effective Ninja working
directory through a capability-scoped handle: a same-directory
`create_new` temporary file, flushed and synced, and an atomic rename.
An existing file is verified and reused; mismatched bytes are reported as
corruption and a concurrent writer that wins the race is treated as
success only when the surviving content matches.

Route `generate_ninja` through `generate_bundle` and materialize the
bundle before every build, clean, and generate invocation writes or
invokes the main Ninja file, so a serial manifest never reaches Ninja
without its sidecars present. Add localization keys for the materializer
errors across all 35 catalogues, with U+200F direction marks in the RTL
locales.

Add real-Ninja runtime tests proving strict declaration order and failure
short-circuiting, plus materializer unit tests for creation, reuse,
corruption detection, and temporary-file cleanup. The full suite (1930
tests) passes, and an end-to-end serial build over a real manifest
observed `fmt, lint, test, all` order.
Collect the per-edge sidecar list and seen-path set in a single
SerialStages value so render_serial_block no longer threads five
parameters and cannot diverge from the caller's bundle accumulator.
Propagate bundle formatting errors, apply capability-scoped test staging,
and split focused test cases so the serial-dependency implementation meets
the repository's strict Clippy and Whitaker contracts.
Keep sidecar-materialization fixtures within their temporary directory
capability so they satisfy the repository filesystem policy.
Update the approved ExecPlan with implementation and validation findings,
and remove surplus trailing whitespace from the affected Fluent catalogues.
@lodyai
lodyai Bot force-pushed the issue-552-support-serial-dependency-ordering-for-actions-and-targets branch from e1cef57 to 7ed4cc8 Compare August 11, 2026 21:48
codescene-access[bot]

This comment was marked as outdated.

@leynos

leynos commented Aug 11, 2026

Copy link
Copy Markdown
Owner

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response. Ensure that this is validated against the current version of the codegraph.

If further refinement to address this finding would be deleterious, please supply a clear explanatory one to two paragraph markdown message I can paste into the CodeScene web ui's diagnostic suppression function so this diagnostic can be silenced.

src/runner/process/dyndep_files.rs

Comment on lines +114 to +163

fn write_atomic(dir: &Dir, rel: &Utf8Path, content: &str) -> Result<()> {
    let temp = unique_temp_name(rel);
    let mut options = OpenOptions::new();
    options.write(true).create_new(true);
    let mut file = match dir.open_with(&temp, &options) {
        Ok(file) => file,
        Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
            // Another process won the race for our temporary name; verify the
            // final path and treat matching content as success.
            return match read_verified(dir, rel, content)? {
                ReadOutcome::Matching => Ok(()),
                ReadOutcome::Mismatch => Err(anyhow!(
                    localization::message(keys::RUNNER_IO_DYNDEP_CORRUPT)
                        .with_arg("path", rel.as_str())
                )),
                ReadOutcome::Missing => Err(anyhow!(
                    localization::message(keys::RUNNER_IO_DYNDEP_RACE)
                        .with_arg("path", rel.as_str())
                )),
            };
        }
        Err(err) => {
            return Err(err).with_context(|| {
                localization::message(keys::RUNNER_IO_DYNDEP_WRITE).with_arg("path", rel.as_str())
            });
        }
    };
    file.write_all(content.as_bytes()).with_context(|| {
        localization::message(keys::RUNNER_IO_DYNDEP_WRITE).with_arg("path", rel.as_str())
    })?;
    file.flush().with_context(|| {
        localization::message(keys::RUNNER_IO_DYNDEP_WRITE).with_arg("path", rel.as_str())
    })?;
    file.sync_all().with_context(|| {
        localization::message(keys::RUNNER_IO_DYNDEP_WRITE).with_arg("path", rel.as_str())
    })?;
    // Rename is relative to the same directory; `rename` replaces an existing
    // destination, so if another process already wrote the final file, the
    // atomic replace yields content identical to ours.
    if let Err(err) = dir.rename(&temp, dir, rel) {
        // The final file may have appeared via a concurrent writer; verify it.
        if read_verified(dir, rel, content)? != ReadOutcome::Matching {
            return Err(err).with_context(|| {
                localization::message(keys::RUNNER_IO_DYNDEP_RENAME).with_arg("path", rel.as_str())
            });
        }
        drop(dir.remove_file(&temp));
    }
    Ok(())
}

❌ New issue: Bumpy Road Ahead
write_atomic has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is 2 blocks per function

@leynos

leynos commented Aug 11, 2026

Copy link
Copy Markdown
Owner

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response. Ensure that this is validated against the current version of the codegraph.

If further refinement to address this finding would be deleterious, please supply a clear explanatory one to two paragraph markdown message I can paste into the CodeScene web ui's diagnostic suppression function so this diagnostic can be silenced.

src/ninja_gen/dyndep_tests.rs

Comment on lines +123 to +135

fn parallel_edges_produce_no_sidecars() -> Result<()> {
    let graph = graph_with_edge(parallel_edge("all", &["dep1", "dep2"]))?;
    let bundle = generate_bundle(&graph)?;
    ensure!(
        !bundle.build_file().contains("ninja_required_version"),
        "parallel bundle must not emit a version floor"
    );
    ensure!(
        bundle.dyndep_files().is_empty(),
        "parallel graph must produce no sidecars"
    );
    Ok(())
}

❌ New issue: Code Duplication
The module contains 2 functions with similar structure: one_element_serial_list_needs_no_gates,parallel_edges_produce_no_sidecars

@leynos

leynos commented Aug 11, 2026

Copy link
Copy Markdown
Owner

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response. Ensure that this is validated against the current version of the codegraph.

If further refinement to address this finding would be deleterious, please supply a clear explanatory one to two paragraph markdown message I can paste into the CodeScene web ui's diagnostic suppression function so this diagnostic can be silenced.

src/ninja_gen/dyndep.rs

Comment on lines +156 to +230

pub fn generate_bundle(graph: &BuildGraph) -> Result<GeneratedNinja, NinjaGenError> {
    reject_reserved_paths(graph)?;
    let serial_present = graph_requires_dyndep(graph);

    let mut out = String::new();
    if serial_present {
        writeln!(out, "ninja_required_version = 1.10\n")?;
    }

    let mut actions: Vec<_> = graph.actions.iter().collect();
    actions.sort_by_key(|(id, _)| *id);
    for (id, action) in actions {
        use crate::ninja_gen::NamedAction;
        writeln!(out, "{}", NamedAction { id, action })?;
    }

    let mut edges: Vec<_> = graph.targets.values().collect();
    edges.sort_by_key(|a| path_key(&a.explicit_outputs));
    let mut seen: HashSet<String> = HashSet::new();
    let mut stages = SerialStages::default();

    for edge in edges {
        let key = path_key(&edge.explicit_outputs);
        if !seen.insert(key.clone()) {
            continue;
        }
        let action =
            graph
                .actions
                .get(&edge.action_id)
                .ok_or_else(|| NinjaGenError::MissingAction {
                    id: edge.action_id.clone(),
                    message: localization::message(keys::NINJA_GEN_MISSING_ACTION)
                        .with_arg("id", &edge.action_id),
                })?;

        let requires_gates =
            edge.dependency_order == DependencyOrder::Serial && edge.implicit_deps.len() > 1;
        if requires_gates {
            let mut added = Vec::new();
            render_serial_block(edge, &mut out, &mut stages, &mut added)?;
            let mut aggregate = edge.clone();
            aggregate.implicit_deps = added;
            aggregate.dependency_order = DependencyOrder::Parallel;
            writeln!(
                out,
                "{}",
                crate::ninja_gen::DisplayEdge {
                    edge: &aggregate,
                    action_restat: action.restat,
                }
            )?;
        } else {
            writeln!(
                out,
                "{}",
                crate::ninja_gen::DisplayEdge {
                    edge,
                    action_restat: action.restat,
                }
            )?;
        }
    }

    if !graph.default_targets.is_empty() {
        let mut defs = graph.default_targets.clone();
        defs.sort();
        writeln!(out, "default {}", join(&defs))?;
    }

    Ok(GeneratedNinja {
        build_file: out,
        dyndep_files: stages.dyndep_files,
    })
}

❌ New issue: Complex Method
generate_bundle has a cyclomatic complexity of 9, threshold = 9

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

@leynos
leynos marked this pull request as ready for review August 11, 2026 21:58

@sourcery-ai sourcery-ai 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.

Sorry @leynos, your pull request is larger than the review limit of 150000 diff characters

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7ed4cc838e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +37 to +38
let dir = open_effective_dir(cli)?;
dir.create_dir_all(DYNDEP_DIR).with_context(|| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Skip materialization when the bundle has no sidecars

When a manifest has only parallel dependencies, dyndep_files is empty, but every build, clean, and generate path still opens the working directory and creates .netsuke/dyndep. Consequently, previously valid operations such as generating Ninja output from a read-only project directory now fail even though they require no dyndep artefacts. Return early when the slice is empty so existing parallel manifests retain their prior filesystem requirements.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@coderabbitai Has this now been resolved in the latest commit?

Use codegraph analysis to determine your answer.

If this comment is now resolved, please mark it as such using the API. Otherwise, please provide an AI agent prompt for the remaining work to be done to address this 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.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== Module outline =='
ast-grep outline src/runner/process/dyndep_files.rs --items all --view expanded

printf '%s\n' '== materialize_dyndep_files implementation =='
rg -n -A80 -B8 '^\s*pub\s+fn\s+materialize_dyndep_files\s*\(' src/runner/process/dyndep_files.rs

printf '%s\n' '== Typed call sites and surrounding control flow =='
rg -n -A12 -B12 '\bmaterialize_dyndep_files\s*\(' --glob '*.rs' .

printf '%s\n' '== PR discussion thread =='
gh api graphql \
  -F owner='leynos' \
  -F name='netsuke' \
  -F number=557 \
  -f query='
query($owner: String!, $name: String!, $number: Int!) {
  repository(owner: $owner, name: $name) {
    pullRequest(number: $number) {
      reviewThreads(first: 100) {
        nodes {
          id
          isResolved
          comments(first: 20) {
            nodes {
              id
              body
              url
              author { login }
            }
          }
        }
      }
    }
  }
}' \
  --jq '.data.repository.pullRequest.reviewThreads.nodes[]
    | select(any(.comments.nodes[]; .url == "https://github.com/leynos/netsuke/pull/557#discussion_r3762028952"))
    | {id, isResolved, comments}'

Length of output: 16320


@leynos, this comment is not resolved.

The codegraph shows that src/runner/mod.rs always calls materialize_dyndep_files. In src/runner/process/dyndep_files.rs, Line 37 opens the effective directory and Line 38 creates .netsuke/dyndep before the loop. An empty dyndep_files slice still performs these operations.

Use this AI agent prompt:

Update `src/runner/process/dyndep_files.rs`.

Add an early successful return at the start of `materialize_dyndep_files` when `dyndep_files.is_empty()`.

Place the guard before `open_effective_dir(cli)` and before `create_dir_all(DYNDEP_DIR)`. This must preserve the previous behaviour for parallel-only manifests: do not open the working directory and do not create `.netsuke/dyndep` when no dyndep sidecars exist.

Add a focused Rust unit test that passes an empty slice and verifies that the function does not create `.netsuke/dyndep`. Use the existing test helpers and project lint rules. Update the function documentation if its error behaviour changes.

You are interacting with an AI system.

Comment thread src/runner/process/dyndep_files.rs Outdated
Comment on lines +120 to +123
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
// Another process won the race for our temporary name; verify the
// final path and treat matching content as success.
return match read_verified(dir, rel, content)? {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Give concurrent sidecar writes distinct temporary names

When two first-time builds generate the same sidecar concurrently, both choose the fixed <digest>.dd.tmp name; the loser of create_new immediately checks the final path while the winner may still be writing, sees it missing, and fails with the race error. An interrupted writer also leaves this fixed temporary file behind and permanently causes the same failure on later runs. Use a genuinely process-unique temporary name, or safely recover and retry collisions, so concurrent builds and crash recovery work as intended.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@coderabbitai Has this now been resolved in the latest commit?

Use codegraph analysis to determine your answer.

If this comment is now resolved, please mark it as such using the API. Otherwise, please provide an AI agent prompt for the remaining work to be done to address this 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.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== Current PR revision ==='
gh pr view 557 --repo leynos/netsuke --json headRefOid --jq '.headRefOid'

echo '=== Module outline ==='
ast-grep outline src/runner/process/dyndep_files.rs --items all --view expanded

echo '=== Structural map: write_atomic and temporary-name helpers ==='
ast-grep run --lang rust \
  --pattern 'fn write_atomic($$$) -> $RET { $$$ }' \
  src/runner/process/dyndep_files.rs
ast-grep run --lang rust \
  --pattern 'fn unique_temp_name($$$) -> $RET { $$$ }' \
  src/runner/process/dyndep_files.rs

echo '=== Focused source slices ==='
rg -n -A100 -B10 'fn (write_atomic|unique_temp_name)\b' src/runner/process/dyndep_files.rs

echo '=== Call sites and definitions in the dyndep graph ==='
ast-grep run --lang rust \
  --pattern 'unique_temp_name($$$)' \
  src/runner/process/dyndep_files.rs
ast-grep run --lang rust \
  --pattern 'materialize_dyndep_files($$$)' \
  src

echo '=== Review-thread lookup ==='
gh api graphql \
  -F owner='leynos' \
  -F name='netsuke' \
  -F number=557 \
  -f query='
query($owner: String!, $name: String!, $number: Int!) {
  repository(owner: $owner, name: $name) {
    pullRequest(number: $number) {
      reviewThreads(first: 100) {
        nodes {
          id
          isResolved
          comments(first: 20) {
            nodes {
              author { login }
              body
              url
            }
          }
        }
      }
    }
  }
}' \
  --jq '.data.repository.pullRequest.reviewThreads.nodes[]
    | select(any(.comments.nodes[]; .url == "https://github.com/leynos/netsuke/pull/557#discussion_r3762028956"))
    | {id, isResolved, comments}'

Length of output: 15359


@leynos, keep this thread open. The latest commit does not resolve the finding.

unique_temp_name() still returns the fixed path <sidecar-name>.tmp. When this path exists and the final sidecar is absent, handle_temp_name_collision() returns RUNNER_IO_DYNDEP_RACE. This failure still occurs for concurrent first-time writers and stale temporary files.

Use this AI agent prompt for the remaining work:

Fix the concurrent temporary-file collision in `src/runner/process/dyndep_files.rs`.

Problem:
- `unique_temp_name(rel)` currently returns the deterministic `<sidecar-name>.tmp` path.
- `create_temp_file()` uses `create_new(true)`.
- If another process is writing that temporary file, or a previous process left it behind, `handle_temp_name_collision()` checks the final sidecar.
- If the final sidecar is not present yet, the code returns `RUNNER_IO_DYNDEP_RACE`.

Required changes:
1. Generate a genuinely unique same-directory temporary path for each write attempt. Do not use only the sidecar filename plus a fixed suffix.
2. Keep the temporary path under the final sidecar’s parent directory so `Dir::rename()` remains atomic.
3. Preserve `create_new(true)` so a temporary write cannot truncate another file.
4. On a rename failure, verify the final sidecar content. If it matches, remove the exact temporary path created by the current invocation. Pass that path into the cleanup function; do not regenerate a temporary name.
5. Preserve the existing corruption check. A final sidecar with different content must still fail.
6. Add focused tests for:
   - an existing stale temporary file with no final sidecar does not block materialization;
   - two separately generated temporary names for the same final sidecar differ;
   - a matching final sidecar still succeeds when another temporary file exists.
7. Update the temporary-name documentation. Do not describe deterministic names as unique.
8. Keep the module documentation and Rust lint rules compliant.

You are interacting with an AI system.

Comment thread src/ast.rs
codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

leynos added 2 commits August 12, 2026 00:07
Describe dependency_order for actions and targets, including its
path-scoped serial execution guarantee, Ninja version requirement, and
generated-state behaviour. Record the dyndep architecture decision and
exercise the complete documented manifest through the documentation suite.
Capture the final deterministic gate evidence and the zero-finding
CodeRabbit review so the Issue 552 ExecPlan remains a complete handoff.
codescene-access[bot]

This comment was marked as outdated.

leynos added 2 commits August 12, 2026 00:16
Extract the shared no-staging assertion while retaining separate parallel
and single-element serial test cases.
Keep the Issue 552 ExecPlan current with the requested test-only
deduplication, commit, and validation evidence.
codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

leynos added 2 commits August 12, 2026 00:25
Extract temporary creation, writing, and rename-race handling while
preserving the existing atomic write and concurrent-writer protocol.
Keep the Issue 552 ExecPlan current with the atomic-write helper
extraction, collision regression, and required gate results.
codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

leynos added 2 commits August 12, 2026 00:34
Move sorted edge rendering and serial edge lowering out of generate_bundle
while preserving the generated Ninja bundle and error behaviour.
Keep the Issue 552 ExecPlan current with the generate_bundle
complexity reduction and its unchanged-output validation evidence.
codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

Return before opening the effective directory when no generated
sidecars exist, so parallel-only manifests neither require the
working directory nor create `.netsuke/dyndep`.

Pass the existing root `dylint.toml` policy to Whitaker explicitly.
This preserves its documented narrow capability exemptions and makes
`make lint` reproducible.
codescene-access[bot]

This comment was marked as outdated.

Use distinct same-directory temporary names and retry protected
creation collisions, so stale or concurrent temporary files cannot
block sidecar materialization.

Carry the exact created path through rename-race cleanup to avoid
deleting another writer’s temporary sidecar.
codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

@codescene-access codescene-access Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No quality gates enabled for this code.

@buzzybee-df12

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Implement staged Ninja dyndep support

  • Add dependency_order: serial for action and target dependencies.
  • Preserve declaration order, shared-work reuse, failure short-circuiting, and concurrency for unrelated branches.
  • Keep omitted and parallel configurations unchanged.
  • Add manifest and IR propagation through DependencyOrder.
  • Generate ordered phony gates and content-addressed dyndep sidecars.
  • Materialize sidecars atomically under .netsuke/dyndep.
  • Reject reserved output paths, corrupted files, and unsupported single-file generation.
  • Add unit, integration, parsing, ordering, failure, and concurrency tests.
  • Update user and developer documentation, repository design records, roadmap, and add ADR-010.
  • Add the completed Issue #552 ExecPlan.
  • Add localized diagnostics and update Whitaker configuration handling.

Walkthrough

Serial dependency ordering is now configurable for actions and targets through dependency_order. Serial graphs generate staged Ninja dyndep bundles, which the runner materialises atomically. The change adds manifest and IR propagation, runtime validation, documentation, localisation messages, and Whitaker configuration loading.

Changes

Serial dependency ordering

Layer / File(s) Summary
Manifest and IR contract
src/ast.rs, src/ir/..., tests/ast_tests/..., tests/ir...
Adds DependencyOrder::Parallel and DependencyOrder::Serial. Propagates the value from manifests to BuildEdge. Defaults omitted values to parallel execution.
Ninja dyndep lowering
src/ninja_gen/..., tests/ninja_gen...
Generates staged phony gates and content-addressed dyndep sidecars for multi-dependency serial edges. Rejects reserved paths and unsupported string-only generation.
Dyndep sidecar materialisation
src/runner/...
Materialises sidecars under .netsuke/dyndep. Reuses matching files and writes missing files with atomic, race-checked updates.
Behavioural validation
tests/serial_dependency_runtime_tests.rs, tests/ninja_gen_integration_tests.rs, tests/documentation_examples_tests.rs
Validates declaration order, failure short-circuiting, sidecar generation, Ninja version requirements, and documentation examples.
Documentation and diagnostics
docs/..., locales/*/messages.ftl, src/localization/keys.rs
Documents the manifest option, generated-state paths, implementation design, and dyndep errors across locale resources.
Tooling and repository support
Makefile, .gitignore
Loads dylint.toml for Whitaker and ignores .vtcode/.

Sequence Diagram(s)

sequenceDiagram
  participant Manifest
  participant BuildGraph
  participant NinjaGenerator
  participant Runner
  participant Ninja
  Manifest->>BuildGraph: propagate dependency_order
  BuildGraph->>NinjaGenerator: provide serial dependency graph
  NinjaGenerator->>Runner: return Ninja build file and dyndep sidecars
  Runner->>Runner: materialise sidecars atomically
  Runner->>Ninja: invoke one Ninja build
  Ninja->>Ninja: execute dependencies through staged gates
Loading

Possibly related PRs

Suggested labels: Roadmap, Issue

Suggested reviewers: leynos

Poem

Gates line up in ordered flight,
Sidecars bloom in Ninja’s light.
Failures halt the waiting chain,
Parallel paths remain untamed.
One bundle guides the build along.
🛠️ The dependency order is strong.


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (4 errors, 4 warnings, 6 inconclusive)

Check name Status Explanation Resolution
Testing (Overall) ❌ Error Tests run real Ninja for declaration order and early-failure short-circuiting, but no test executes shared dependencies once or proves unrelated branches remain concurrent; repeated-dependency cove... Add real-Ninja tests with a shared output used by multiple serial consumers and an unrelated branch. Assert one execution and concurrent progress. Retain the order and failure assertions.
Unit Architecture ❌ Error The changed generate_ninja returns NinjaContent but now calls materialize_dyndep_files; execute_generate therefore writes .netsuke/dyndep through an apparently read-only generation helper. Split bundle generation from publication. Return GeneratedNinja from the query layer, then call a named materialisation command at each command boundary and inject the filesystem capability.
Security And Privacy ❌ Error The PR adds a developer-specific /home/leynos/.lody/.../6c498022-... worktree path to the committed ExecPlan, exposing private local metadata in documentation. Remove the absolute path, username, and worktree UUID from the ExecPlan; replace them with a generic repository-root placeholder.
Rust Compiler Lint Integrity ❌ Error Flag the new ownership work: render_serial_edge clones a full BuildEdge, materialize_one clones a read-only Utf8PathBuf, and generate_ninja copies the bundle string despite into_parts. Replace these clones with a borrowed path, a rendering view or override instead of cloning BuildEdge, and consume GeneratedNinja to move its build string after materialisation.
Title check ⚠️ Warning The title identifies serial dependency ordering and issue #552, but it omits the required roadmap item reference for ExecPlan 3.14.3. Add the roadmap reference (3.14.3.) to the title while retaining (#552).
Out of Scope Changes check ⚠️ Warning The .gitignore update for .vtcode is unrelated to the serial dependency ordering requirements in [#552]. Remove the unrelated .gitignore change or move it to a separate pull request.
User-Facing Documentation ⚠️ Warning The PR adds serial dependency ordering and dyndep behaviour, but the changed files include no n+1 migration guide and the existing migration guide does not mention this new functionality. Add the appropriate pre-1.0 minor migration guide and link it from the users' guide or documentation index. Describe dependency_order: serial, Ninja 1.10, sidecars, and reserved paths.
Observability ⚠️ Warning The branch adds staged serial execution and filesystem sidecars, but adds no metrics or tracing spans; its only new trace is a reuse debug event, leaving latency, outcomes, and write failures unobs... Add bounded metrics for bundle and materialisation outcomes and duration. Add spans and failure logs at serial lowering and each materialisation boundary with safe operation and outcome fields.
Developer Documentation ❓ Inconclusive Initial revision check shows only the final commit; the aggregate PR base is not yet identified. Identify the PR base or full diff, then verify developer-guide, design, ADR, roadmap, ExecPlan, and locale synchronisation.
Testing (Unit And Behavioural) ❓ Inconclusive The repository contains parser, IR, generator, atomic-materialisation, and real-Ninja tests covering valid, default, edge, error, ordering, and failure behaviour. Confirm the new tests are registered and pass in the project test harness.
Testing (Property / Proof) ❓ Inconclusive Investigation in progress; no final assessment yet. Inspect the changed ordering and dyndep invariants, then verify whether substantive property or proof tests cover their input ranges.
Testing (Compile-Time / Ui) ❓ Inconclusive The change adds Rust manifest and Ninja-generation behaviour, but the custom check does not state whether focused runtime assertions satisfy its snapshot recommendation. Decide whether the generated Ninja and dyndep text requires snapshot coverage beyond the added semantic assertions.
Concurrency And State ❓ Inconclusive Investigating the staged sidecar concurrency model and its tests. Gather evidence on shared state ownership, atomicity, and interleaving coverage before deciding.
Architectural Complexity And Maintainability ❓ Inconclusive Evidence gathering has started; no verdict is submitted yet. Awaiting code and diff inspection.
✅ Passed checks (6 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the serial dependency ordering implementation, documentation, tests, validation, and issue #552.
Linked Issues check ✅ Passed The implementation satisfies the direct-dependency ordering requirements in [#552], including order, failure short-circuiting, shared-work reuse, scope, defaults, and regression coverage.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Module-Level Documentation ✅ Passed Pass this check: every changed or added Rust module starts with //! documentation; new Ninja, dyndep, runner, and test modules state their purpose and component relationships.
Domain Architecture ✅ Passed The change adds an explicit DependencyOrder policy to AST/IR; Ninja lowering stays in ninja_gen and filesystem materialisation stays in runner, with no core-to-adapter imports found.
Performance And Resource Use ✅ Passed The new graph work is linear per edge/dependency and the sidecar bundle is owned by the generated graph; no avoidable quadratic traversal or unbounded collection is evident.
📋 Issue Planner

Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).

View plan for ticket: #552

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-552-support-serial-dependency-ordering-for-actions-and-targets

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 30

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/adr-010-use-ninja-dyndep-for-serial-dependency-ordering.md`:
- Line 114: Update the user contract link text in the ADR to “user's guide”
while preserving its existing destination and surrounding formatting.
- Around line 25-47: Add a concise Y-statement to the ADR-010 Decision section
covering the context, forces, decision, and accepted consequences of staged
Ninja dyndep serial ordering. Preserve the existing paragraphs as supporting
implementation rationale and do not alter their technical details.
- Line 9: Update the ADR date in the document metadata from “2026-08-11.” to the
bare YYYY-MM-DD value “2026-08-11”, removing the trailing punctuation.

In
`@docs/execplans/issue-552-support-serial-dependency-ordering-for-actions-and-targets.md`:
- Around line 798-801: Remove the machine-specific absolute filesystem path from
the plan’s command example, replacing it with <repository-root> or omitting the
path while keeping the commands relative to the repository root.
- Around line 185-186: Update the test-results sentence in the execution plan to
replace “739 lib + touched integration tests pass” with clear grammatical prose
stating that 739 library tests plus the touched integration tests passed.

In `@docs/netsuke-design.md`:
- Around line 2056-2071: Update the BuildEdge IR example around the Rust snippet
and class diagram to represent the dependency_order policy used by the staged
serial-ordering section. If dependency_order is a BuildEdge field, add it
consistently to both representations; otherwise, document the actual field or
graph structure that carries this policy and align references to
BuildEdge.dependency_order accordingly.

In `@docs/users-guide.md`:
- Around line 384-385: Update the Netsuke documentation sentence to state that
generated builds containing staged serial ordering require Ninja 1.10 or newer,
replacing the ambiguous phrase “generated builds containing one” while
preserving the surrounding dyndep and dependency details.

In `@locales/ar/messages.ftl`:
- Around line 101-106: Translate all added dyndep diagnostic values in
locales/ar/messages.ftl, locales/cs/messages.ftl, locales/cy/messages.ftl,
locales/da/messages.ftl, locales/de/messages.ftl, locales/th/messages.ftl, and
locales/tr/messages.ftl at lines 101-106, and apply the same locale-specific
translations to each catalogue’s corresponding lines 171-172. Preserve every
Fluent key, the { $path } placeholder, and inline command names.

In `@locales/el/messages.ftl`:
- Around line 102-107: Translate the six runner.io.dyndep.* messages and two
ninja_gen.* messages in locales/el/messages.ftl (102-107, 172-173),
locales/es-419/messages.ftl (102-107, 172-173), locales/es-ES/messages.ftl
(102-107, 172-173), locales/fa/messages.ftl (102-107, 172-173), and
locales/fi/messages.ftl (102-107, 172-173) into their respective locales.
Preserve every Fluent key and { $path } placeholder exactly, retain technical
identifiers such as dyndep, Ninja, and command names as appropriate, and ensure
Greek wording places { $path } in a nominative sentence position.

In `@locales/en-GB/messages.ftl`:
- Around line 101-106: Update the en-GB catalogue entries to use British
spelling: change “finalize” to “finalise” in runner.io.dyndep.rename and change
“materialized” to “materialised” in the referenced entry around the
materialization messages.

In `@locales/fr/messages.ftl`:
- Around line 102-107: Translate all eight added diagnostic messages in each
affected catalogue: locales/fr/messages.ftl lines 102-107 and 172-173,
locales/gd/messages.ftl lines 101-106 and 171-172, locales/he/messages.ftl lines
101-106 and 171-172, and locales/hi/messages.ftl lines 101-106 and 171-172.
Preserve every Fluent key and the { $path } placeholder while translating the
six runner.io.dyndep.* values and two ninja_gen.* values; no direct changes are
needed elsewhere.

In `@locales/hu/messages.ftl`:
- Around line 101-106: Translate the new dyndep diagnostic values while
preserving every Fluent key and { $path } placeholder: in
locales/hu/messages.ftl lines 101-106 and 171-172 use Hungarian; in
locales/id/messages.ftl lines 101-106 and 171-172 use Indonesian; in
locales/it/messages.ftl lines 102-107 and 172-173 use Italian; and in
locales/ja/messages.ftl lines 101-106 and 171-172 use Japanese.

In `@locales/ko/messages.ftl`:
- Around line 101-106: Translate all newly added English diagnostics in
locales/ko/messages.ftl lines 101-106 and 171-172 into Korean,
locales/nb/messages.ftl lines 101-106 and 171-172 into Norwegian Bokmål, and
locales/nl/messages.ftl lines 101-106 and 171-172 into Dutch. Preserve every
Fluent key, {$path} placeholder, formatting, and command name exactly.

In `@locales/pl/messages.ftl`:
- Around line 101-107: Translate the new dyndep diagnostic values into each
target language while preserving all Fluent keys and the `{ $path }` variable:
update locales/pl/messages.ftl lines 101-107 in Polish,
locales/pt-BR/messages.ftl lines 102-107 in Brazilian Portuguese,
locales/pt-PT/messages.ftl lines 102-107 in European Portuguese,
locales/ro/messages.ftl lines 101-106 in Romanian, locales/ru/messages.ftl lines
101-106 in Russian, and locales/sv/messages.ftl lines 101-106 in Swedish.

In `@locales/uk/messages.ftl`:
- Around line 101-106: Translate every newly added dyndep diagnostic value while
preserving the existing Fluent keys and { $path } placeholders: in
locales/uk/messages.ftl lines 101-106 and 171-172 use Ukrainian; in
locales/vi/messages.ftl lines 101-106 and 171-172 use Vietnamese; in
locales/zh-Hans/messages.ftl lines 100-105 and 170-171 use Simplified Chinese;
and in locales/zh-Hant/messages.ftl lines 100-105 and 170-171 use Traditional
Chinese.

In `@src/manifest/render.rs`:
- Line 156: Update assert_rendered_target to assert that the rendered target
preserves DependencyOrder::Parallel, and add a serial case if the test is
intended to cover both enum variants. Ensure the render_manifest test fails when
dependency_order is changed or omitted.

In `@src/ninja_gen/dyndep_tests.rs`:
- Around line 179-192: Expand reserved path coverage in
reserved_output_namespace_is_rejected by parameterizing the test across
explicit_outputs, implicit_outputs, inputs, implicit_deps, and order_only_deps,
ensuring each case retains a valid non-reserved output for graph_with_edge. Add
a negative case using a path such as .netsuke-extra/x and verify it is accepted,
preserving the prefix-boundary behavior.

In `@src/ninja_gen/dyndep.rs`:
- Around line 144-148: Align generate_bundle’s action and edge emission with
generate_into by replacing the writeln! calls around NamedAction and DisplayEdge
with write!, since those display implementations already append their own
newlines. Add an equivalence test for a parallel-only graph asserting
generate_bundle(&graph).build_file() matches generate(&graph)? exactly.
- Around line 224-225: Consolidate the duplicated serial-edge predicate by
exposing one shared edge_requires_gates(edge: &BuildEdge) -> bool and removing
the local requires_gates definition and duplicate graph_requires_dyndep
implementation in src/ninja_gen/dyndep.rs. Update graph_requires_dyndep in both
modules to call the shared predicate, preserving the existing serial dependency
and implicit-dependency count conditions.
- Around line 133-134: Complete the truncated documentation sentence above the
sidecar fixture by restoring the missing intra-doc link after “rather than
through.” Use the appropriate existing symbol for the bundle-construction path
referenced by the surrounding documentation.

In `@src/ninja_gen/mod.rs`:
- Around line 233-251: Update escape_ninja_path to reject paths containing the
pipe character before generating Ninja output, rather than emitting the
unsupported $| sequence. Preserve the existing escaping behavior for spaces,
dollars, and colons, and ensure callers handle the rejection appropriately.

In `@src/ninja_gen/tests.rs`:
- Around line 1-6: Remove the duplicated “Unit tests for Ninja file generation
and rule synthesis.” module documentation line in the tests module header,
preserving the single original doc line and the remaining comments.

In `@src/runner/process/dyndep_files.rs`:
- Around line 59-69: The non-UTF-8 working-directory error in open_effective_dir
uses an untranslated literal. Add a dedicated localization key alongside
RUNNER_IO_OPEN_AMBIENT_DIR, define its Fluent string in every
locales/*/messages.ftl file, and replace the literal context with
localization::message using the new key.
- Around line 122-126: Update write_atomic and its helper error paths to remove
the temporary file whenever writing, syncing, or renaming fails. Ensure
handle_rename_failure’s existing matching-content cleanup does not cause a
double removal; centralize cleanup or guard that path while preserving the
current successful rename behavior.
- Around line 129-163: Bound the retry loop in create_unique_temp_file with a
finite attempt limit and return a clear error when all candidate names collide,
while preserving successful creation and existing I/O error propagation. In
create_temp_file, replace the handle_temp_name_collision() call with the direct
None result and remove the now-unused helper function.
- Around line 317-328: Update the no_temp_files_left_behind test around
materialize_dyndep_files to enumerate the dyndep directory and assert that no
entry name ends with ".tmp", rather than checking the nonexistent fixed
filename. After cleanup is implemented, add a failing-write case using the same
directory scan to verify temporary files are also removed on errors.

In `@tests/ir_from_manifest_tests.rs`:
- Around line 380-402: Update serial_dependency_order_survives_lowering to
accept a third case parameter containing the expected dependency list, then
assert edge.implicit_deps matches it alongside dependency_order and phony.
Extend each serial test case with the expected declaration-ordered dependencies,
preserving the existing assertions.

In `@tests/ninja_gen_integration_tests.rs`:
- Around line 272-360: Extract reusable rstest fixtures or small constructor
helpers for the repeated Action and BuildEdge literals in
serial_graph_rejected_by_string_only_generation and
bundle_generation_for_serial_graph_materializes_sidecars, parameterizing
command, implicit dependencies, and dependency order as needed. Follow the
existing action, serial_edge, and parallel_edge pattern in dyndep_tests.rs, and
keep the integration test file below the enforced 400-line limit.

In `@tests/serial_dependency_runtime_tests.rs`:
- Around line 1-7: Add a real Ninja runtime test alongside the existing ordering
and short-circuiting tests, using the bundle-generation and filesystem-marker
patterns already present in the file. Define two serial aggregates that depend
on the same shared target, run the encompassing build, and assert the shared
target’s command appends to the log exactly once, proving reuse across serial
lists.
- Line 17: Update the Ninja process-spawn error handling in the serial
dependency runtime tests to distinguish ErrorKind::NotFound from other failures,
explicitly report that ninja must be available on PATH, and preserve test
failure when it is missing. Keep other spawn errors’ existing context and
behavior unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 88cd048c-68a2-4244-9d43-2cb28ee8320a

📥 Commits

Reviewing files that changed from the base of the PR and between ecd92f2 and 723ef58.

📒 Files selected for processing (72)
  • .gitignore
  • Makefile
  • docs/adr-010-use-ninja-dyndep-for-serial-dependency-ordering.md
  • docs/contents.md
  • docs/developers-guide.md
  • docs/execplans/issue-552-support-serial-dependency-ordering-for-actions-and-targets.md
  • docs/netsuke-design.md
  • docs/repository-layout.md
  • docs/roadmap.md
  • docs/users-guide.md
  • locales/ar/messages.ftl
  • locales/cs/messages.ftl
  • locales/cy/messages.ftl
  • locales/da/messages.ftl
  • locales/de/messages.ftl
  • locales/el/messages.ftl
  • locales/en-GB/messages.ftl
  • locales/en-US/messages.ftl
  • locales/es-419/messages.ftl
  • locales/es-ES/messages.ftl
  • locales/fa/messages.ftl
  • locales/fi/messages.ftl
  • locales/fr/messages.ftl
  • locales/gd/messages.ftl
  • locales/he/messages.ftl
  • locales/hi/messages.ftl
  • locales/hu/messages.ftl
  • locales/id/messages.ftl
  • locales/it/messages.ftl
  • locales/ja/messages.ftl
  • locales/ko/messages.ftl
  • locales/nb/messages.ftl
  • locales/nl/messages.ftl
  • locales/pl/messages.ftl
  • locales/pt-BR/messages.ftl
  • locales/pt-PT/messages.ftl
  • locales/ro/messages.ftl
  • locales/ru/messages.ftl
  • locales/sv/messages.ftl
  • locales/th/messages.ftl
  • locales/tr/messages.ftl
  • locales/uk/messages.ftl
  • locales/vi/messages.ftl
  • locales/zh-Hans/messages.ftl
  • locales/zh-Hant/messages.ftl
  • src/ast.rs
  • src/graph_view/tests_support.rs
  • src/ir/cycle_issue322_property_tests.rs
  • src/ir/cycle_property_tests.rs
  • src/ir/cycle_tests.rs
  • src/ir/cycle_verification.rs
  • src/ir/from_manifest.rs
  • src/ir/graph.rs
  • src/ir/mod.rs
  • src/localization/keys.rs
  • src/manifest/render.rs
  • src/ninja_gen/dyndep.rs
  • src/ninja_gen/dyndep_tests.rs
  • src/ninja_gen/mod.rs
  • src/ninja_gen/tests.rs
  • src/ninja_gen_property_tests.rs
  • src/runner/mod.rs
  • src/runner/process/dyndep_files.rs
  • src/runner/process/mod.rs
  • tests/ast_tests.rs
  • tests/ast_tests/dependency_order.rs
  • tests/documentation_examples_tests.rs
  • tests/ir_from_manifest_tests.rs
  • tests/ir_tests.rs
  • tests/ninja_gen_integration_tests.rs
  • tests/ninja_gen_unit_tests.rs
  • tests/serial_dependency_runtime_tests.rs


## Date

2026-08-11.

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use a bare ADR date.

Change 2026-08-11. to 2026-08-11.

Triage: [type:docstyle]

Based on learnings: ADR dates must use bare YYYY-MM-DD values without trailing punctuation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/adr-010-use-ninja-dyndep-for-serial-dependency-ordering.md` at line 9,
Update the ADR date in the document metadata from “2026-08-11.” to the bare
YYYY-MM-DD value “2026-08-11”, removing the trailing punctuation.

Source: Learnings

Comment on lines +25 to +47
## Decision

In the context of a `dependency_order: serial` manifest `deps` list, Netsuke
will use staged Ninja dyndep sidecars to reveal one direct dependency at a time
and will materialize those sidecars atomically beneath `.netsuke/dyndep` before
writing or invoking the main build file.

`dependency_order` is a closed `parallel`/`serial` enum on the shared action
and target AST shape. It is copied to `BuildEdge`, where it remains a logical
graph annotation. Only the Ninja generator lowers a serial list containing two
or more direct dependencies into synthetic phony gates beneath
`.netsuke/serial` and content-addressed dyndep sidecars beneath
`.netsuke/dyndep`.

The main generated build file declares `ninja_required_version = 1.10` only
when staged serial lowering is present. The generator exposes a complete bundle
containing main-file text and every required sidecar. String-only generation
rejects a graph requiring sidecars instead of returning an incomplete file.

The serial guarantee is deliberately path-scoped: each direct dependency in the
annotated list becomes schedulable only after its predecessor succeeds. A later
dependency independently reachable through another requested path remains free
to run through that other 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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the required Y-statement.

Add a concise Y-statement in the Decision section. State the context, forces, decision, and accepted consequences. Keep the existing implementation details as supporting rationale.

Triage: [type:docstyle]

The ExecPlan requires ADR-010 to include a Y-statement.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~42-~42: Possible missing comma found.
Context: ...ly generation rejects a graph requiring sidecars instead of returning an incomplete file...

(AI_HYDRA_LEO_MISSING_COMMA)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/adr-010-use-ninja-dyndep-for-serial-dependency-ordering.md` around lines
25 - 47, Add a concise Y-statement to the ADR-010 Decision section covering the
context, forces, decision, and accepted consequences of staged Ninja dyndep
serial ordering. Preserve the existing paragraphs as supporting implementation
rationale and do not alter their technical details.

[`src/ninja_gen/dyndep.rs`](../src/ninja_gen/dyndep.rs)
- Atomic sidecar materialization:
[`src/runner/process/dyndep_files.rs`](../src/runner/process/dyndep_files.rs)
- User contract: [users guide](users-guide.md#run-direct-dependencies-serially)

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the guide link text.

Change [users guide] to [user's guide].

Triage: [type:grammar] [type:docstyle]

Static analysis flagged the missing apostrophe.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~114-~114: It seems likely that a singular genitive (’s) apostrophe is missing.
Context: ...cess/dyndep_files.rs) - User contract: [users guide](users-guide.md#run-direct-depend...

(AI_HYDRA_LEO_APOSTROPHE_S_XS)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/adr-010-use-ninja-dyndep-for-serial-dependency-ordering.md` at line 114,
Update the user contract link text in the ADR to “user's guide” while preserving
its existing destination and surrounding formatting.

Source: Linters/SAST tools

Comment on lines +185 to +186
and fixture to compile. `cargo check --all-targets` and 739 lib + touched
integration tests pass.

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the test-count wording.

Replace 739 lib + touched integration tests pass. with clear prose such as 739 library tests plus the touched integration tests passed.

Triage: [type:grammar] [type:docstyle]

Static analysis flagged this wording as grammatically unclear.

🧰 Tools
🪛 LanguageTool

[grammar] ~185-~185: After the number ‘739’, use a plural noun. Did you mean “libs”?
Context: ...le. cargo check --all-targets and 739 lib + touched integration tests pass. - [...

(CD_NNU)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@docs/execplans/issue-552-support-serial-dependency-ordering-for-actions-and-targets.md`
around lines 185 - 186, Update the test-results sentence in the execution plan
to replace “739 lib + touched integration tests pass” with clear grammatical
prose stating that 739 library tests plus the touched integration tests passed.

Source: Linters/SAST tools

Comment on lines +798 to +801
```plaintext
/home/leynos/.lody/repos/github---leynos---netsuke/worktrees/
6c498022-7fb6-49a9-94a9-56723bb7d1e1
```

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.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Remove the machine-specific absolute path.

The committed plan exposes /home/leynos/... and is not portable to another checkout. Replace it with <repository-root> or omit the path. Keep the commands relative to the repository root.

The path exposes a developer identifier and local filesystem layout.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@docs/execplans/issue-552-support-serial-dependency-ordering-for-actions-and-targets.md`
around lines 798 - 801, Remove the machine-specific absolute filesystem path
from the plan’s command example, replacing it with <repository-root> or omitting
the path while keeping the commands relative to the repository root.

Comment on lines +317 to +328
#[test]
fn no_temp_files_left_behind() -> Result<()> {
let temp = tempfile::tempdir()?;
let cli = temp_cli(temp.path());
materialize_dyndep_files(&cli, &[sidecar(".netsuke/dyndep/x.dd", "content")])?;
let temp_file = ".netsuke/dyndep/x.dd.tmp";
ensure!(
temp_dir(&temp)?.open(temp_file).is_err(),
"temp file left behind"
);
Ok(())
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix the vacuous assertion: this test checks a filename that is never produced.

unique_temp_name at line 232 builds {name}.{process_id}.{sequence}.tmp, so the temporary file for x.dd is named x.dd.<pid>.<seq>.tmp. Line 322 checks .netsuke/dyndep/x.dd.tmp, which the code never creates. The assertion at lines 323-326 therefore succeeds regardless of whether a temporary file was left behind, and it does not detect the leak described on write_atomic.

Enumerate the directory and assert that no entry ends with .tmp.

🧪 Proposed fix
 #[test]
 fn no_temp_files_left_behind() -> Result<()> {
     let temp = tempfile::tempdir()?;
     let cli = temp_cli(temp.path());
     materialize_dyndep_files(&cli, &[sidecar(".netsuke/dyndep/x.dd", "content")])?;
-    let temp_file = ".netsuke/dyndep/x.dd.tmp";
-    ensure!(
-        temp_dir(&temp)?.open(temp_file).is_err(),
-        "temp file left behind"
-    );
+    let leftovers: Vec<String> = temp_dir(&temp)?
+        .read_dir(DYNDEP_DIR)?
+        .filter_map(|entry| entry.ok()?.file_name().ok())
+        .filter(|name| name.ends_with(".tmp"))
+        .collect();
+    ensure!(
+        leftovers.is_empty(),
+        "temporary files left behind: {leftovers:?}"
+    );
     Ok(())
 }

Extend the same check to a failing-write case once cleanup is added, so the guard covers the error path too.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[test]
fn no_temp_files_left_behind() -> Result<()> {
let temp = tempfile::tempdir()?;
let cli = temp_cli(temp.path());
materialize_dyndep_files(&cli, &[sidecar(".netsuke/dyndep/x.dd", "content")])?;
let temp_file = ".netsuke/dyndep/x.dd.tmp";
ensure!(
temp_dir(&temp)?.open(temp_file).is_err(),
"temp file left behind"
);
Ok(())
}
#[test]
fn no_temp_files_left_behind() -> Result<()> {
let temp = tempfile::tempdir()?;
let cli = temp_cli(temp.path());
materialize_dyndep_files(&cli, &[sidecar(".netsuke/dyndep/x.dd", "content")])?;
let leftovers: Vec<String> = temp_dir(&temp)?
.read_dir(DYNDEP_DIR)?
.filter_map(|entry| entry.ok()?.file_name().ok())
.filter(|name| name.ends_with(".tmp"))
.collect();
ensure!(
leftovers.is_empty(),
"temporary files left behind: {leftovers:?}"
);
Ok(())
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runner/process/dyndep_files.rs` around lines 317 - 328, Update the
no_temp_files_left_behind test around materialize_dyndep_files to enumerate the
dyndep directory and assert that no entry name ends with ".tmp", rather than
checking the nonexistent fixed filename. After cleanup is implemented, add a
failing-write case using the same directory scan to verify temporary files are
also removed on errors.

Comment on lines +380 to +402
fn serial_dependency_order_survives_lowering(
#[case] yaml: &str,
#[case] output: &str,
#[case] expected_phony: bool,
) -> Result<()> {
let manifest = manifest::from_str(yaml)?;
let graph = BuildGraph::from_manifest(&manifest).context("expected graph generation")?;
let edge = graph
.targets
.get(&Utf8PathBuf::from(output))
.with_context(|| format!("expected edge for {output}"))?;
ensure!(
edge.dependency_order == DependencyOrder::Serial,
"expected serial dependency order for {output}, got {:?}",
edge.dependency_order
);
ensure!(
edge.phony == expected_phony,
"unexpected phony flag for {output}: {}",
edge.phony
);
Ok(())
}

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.

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert declaration order in the serial cases.

This test checks the dependency_order enum and the phony flag, but not implicit_deps. Declaration order is the property the feature exists to preserve, and parallel_dependency_order_lowering_is_default at lines 424-428 already asserts it for the parallel path. A lowering change that reorders or de-duplicates a serial deps list passes this test.

Add the expected dependency list as a third case parameter.

🧪 Proposed addition
-), "all", false)]
+), "all", false, &["check-fmt", "lint", "test"])]
-), "gate", true)]
+), "gate", true, &["fmt", "clippy"])]
 fn serial_dependency_order_survives_lowering(
     #[case] yaml: &str,
     #[case] output: &str,
     #[case] expected_phony: bool,
+    #[case] expected_deps: &[&str],
 ) -> Result<()> {
     ensure!(
         edge.phony == expected_phony,
         "unexpected phony flag for {output}: {}",
         edge.phony
     );
+    let expected: Vec<Utf8PathBuf> = expected_deps.iter().map(Utf8PathBuf::from).collect();
+    ensure!(
+        edge.implicit_deps == expected,
+        "declaration order must survive lowering for {output}: {:?}",
+        edge.implicit_deps
+    );
     Ok(())
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/ir_from_manifest_tests.rs` around lines 380 - 402, Update
serial_dependency_order_survives_lowering to accept a third case parameter
containing the expected dependency list, then assert edge.implicit_deps matches
it alongside dependency_order and phony. Extend each serial test case with the
expected declaration-ordered dependencies, preserving the existing assertions.

Comment on lines +272 to +360
#[rstest]
fn serial_graph_rejected_by_string_only_generation() -> Result<()> {
let action = Action {
recipe: Recipe::Command {
command: "echo done".into(),
},
description: None,
depfile: None,
deps_format: None,
pool: None,
restat: false,
};
let edge = BuildEdge {
action_id: "a".into(),
inputs: Vec::new(),
implicit_deps: vec![Utf8PathBuf::from("dep1"), Utf8PathBuf::from("dep2")],
dependency_order: netsuke::ast::DependencyOrder::Serial,
explicit_outputs: vec![Utf8PathBuf::from("all")],
implicit_outputs: Vec::new(),
order_only_deps: Vec::new(),
phony: false,
always: false,
};
let mut graph = BuildGraph::default();
graph.actions.insert("a".into(), action);
graph.targets.insert(Utf8PathBuf::from("all"), edge);

let mut out = String::new();
let err = generate_into(&graph, &mut out)
.err()
.context("serial graph must be rejected by string-only generation")?;
ensure!(
matches!(err, NinjaGenError::DyndepFilesRequired { .. }),
"expected DyndepFilesRequired, got {err:?}"
);
ensure!(
out.is_empty(),
"string-only generation must not write partial output"
);
Ok(())
}

#[rstest]
fn bundle_generation_for_serial_graph_materializes_sidecars() -> Result<()> {
let action = Action {
recipe: Recipe::Command {
command: "echo done".into(),
},
description: None,
depfile: None,
deps_format: None,
pool: None,
restat: false,
};
let edge = BuildEdge {
action_id: "a".into(),
inputs: Vec::new(),
implicit_deps: vec![Utf8PathBuf::from("check-fmt"), Utf8PathBuf::from("test")],
dependency_order: netsuke::ast::DependencyOrder::Serial,
explicit_outputs: vec![Utf8PathBuf::from("all")],
implicit_outputs: Vec::new(),
order_only_deps: Vec::new(),
phony: false,
always: false,
};
let mut graph = BuildGraph::default();
graph.actions.insert("a".into(), action);
graph.targets.insert(Utf8PathBuf::from("all"), edge);

let bundle = netsuke::ninja_gen::generate_bundle(&graph)?;
ensure!(
bundle
.build_file()
.contains("ninja_required_version = 1.10"),
"serial bundle must declare version floor"
);
ensure!(
bundle.dyndep_files().len() == 2,
"expected two sidecars, got {}",
bundle.dyndep_files().len()
);
for dd in bundle.dyndep_files() {
ensure!(
dd.content().starts_with("ninja_dyndep_version = 1\n"),
"sidecar must start with dyndep version header"
);
}
Ok(())
}

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.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract a shared edge fixture; the file is close to the 400-line ceiling.

The two new tests repeat the full Action and BuildEdge literals that already appear six times earlier in this file. The only variation is the recipe command, the implicit deps, and the dependency order. The file now reaches line 360, and the repository enforces a 400-line maximum for Rust files, including crate-root integration tests under tests/.

Add rstest fixtures or small constructor helpers for Action and BuildEdge, as src/ninja_gen/dyndep_tests.rs already does with action, serial_edge, and parallel_edge. That removes the duplication and keeps headroom under the ceiling.

Based on learnings, enforce the 400-line maximum for every Rust code file, including crate-root integration test files under tests/; do not treat Whitaker lint's current failure to enforce this on crate roots as an exception.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/ninja_gen_integration_tests.rs` around lines 272 - 360, Extract
reusable rstest fixtures or small constructor helpers for the repeated Action
and BuildEdge literals in serial_graph_rejected_by_string_only_generation and
bundle_generation_for_serial_graph_materializes_sidecars, parameterizing
command, implicit dependencies, and dependency order as needed. Follow the
existing action, serial_edge, and parallel_edge pattern in dyndep_tests.rs, and
keep the integration test file below the enforced 400-line limit.

Sources: Coding guidelines, Learnings

Comment on lines +1 to +7
//! Real-Ninja runtime tests for serial dependency ordering.
//!
//! These tests drive an actual `ninja` process against a bundle generated by
//! `generate_bundle`, using filesystem markers to prove declaration order,
//! failure short-circuiting, and shared-work reuse. They deliberately assert
//! observable behaviour rather than generated text.

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add the shared-dependency test that the module doc promises.

Line 5 states these tests prove "declaration order, failure short-circuiting, and shared-work reuse". The file contains two tests: ordering and short-circuiting. No test covers shared-work reuse.

Issue #552 requires that a dependency shared by several serial lists executes at most once per encompassing build. src/ninja_gen/dyndep_tests.rs checks only that a repeated dependency produces two distinct sidecars; it does not prove the underlying recipe runs once. This is exactly the property that needs a real Ninja run to confirm, because it depends on Ninja unifying the revealed dependency node across gates.

Add a test with two serial aggregates that both depend on a common target, then assert the shared command appended to the log exactly once.

Do you want me to generate that test?

Also applies to: 115-210

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/serial_dependency_runtime_tests.rs` around lines 1 - 7, Add a real
Ninja runtime test alongside the existing ordering and short-circuiting tests,
using the bundle-generation and filesystem-marker patterns already present in
the file. Define two serial aggregates that depend on the same shared target,
run the encompassing build, and assert the shared target’s command appends to
the log exactly once, proving reuse across serial lists.

use std::process::Command;
use tempfile::TempDir;

const NINJA: &str = "ninja";

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.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find the existing convention for tests that require an external ninja binary.
set -euo pipefail

# Locate other tests that shell out to ninja and see how they guard availability.
rg -n -C4 --type=rust '"ninja"|Command::new\(NINJA\)|ninja_available|skip' tests/ src/ | head -n 120

# Check for a shared test helper crate or module that gates on external tools.
fd -t f -e rs . tests/ --exec rg -l 'which|is_available|requires_tool' {} \;

Repository: leynos/netsuke

Length of output: 9964


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- target file ---'
fd -t f -a 'serial_dependency_runtime_tests.rs' . --exec sh -c 'cat -n "$1"' sh {}

printf '%s\n' '--- external-command test patterns ---'
rg -n -C3 --type rust \
  'Command::new|\.output\(\)|spawn [A-Za-z0-9_-]+|which::|PATH' \
  tests src | head -n 240

printf '%s\n' '--- test support modules ---'
fd -t f -a . tests | rg 'support|helper|utils|common' | head -n 120

Repository: leynos/netsuke

Length of output: 28527


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- all repository references to Ninja ---'
rg -n -i -C3 '\bninja\b' . \
  -g '!target/**' -g '!node_modules/**' -g '!*.lock' | head -n 320

printf '%s\n' '--- integration-test registration ---'
rg -n -C4 'serial_dependency_runtime_tests|harness *=|test-support|make test|cargo test' \
  Cargo.toml Makefile .github tests 2>/dev/null | head -n 240

printf '%s\n' '--- command error behaviour ---'
python3 - <<'PY'
import errno
from pathlib import Path
print("ENOENT:", errno.ENOENT, Path("/definitely/missing/ninja").exists())
PY

Repository: leynos/netsuke

Length of output: 46004


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- test target and CI prerequisites ---'
sed -n '1,180p' Makefile
sed -n '1,130p' .github/workflows/ci.yml
sed -n '1,180p' Cargo.toml

printf '%s\n' '--- tool-availability and skip conventions ---'
rg -n -i -C5 \
  'skip|ignored|unavailable|not installed|missing prerequisite|command_available|which\(' \
  tests .github Makefile docs \
  -g '*.rs' -g '*.yml' -g '*.yaml' -g 'Makefile' -g '*.md' | head -n 320

printf '%s\n' '--- target file registration ---'
rg -n -C6 'serial_dependency_runtime_tests' . \
  -g '!target/**' -g '!node_modules/**'

Repository: leynos/netsuke

Length of output: 44634


Report a missing ninja prerequisite explicitly.

Keep these tests failing when Ninja is unavailable because CI requires it and the repository has no skip convention. Distinguish NotFound from other spawn errors and state that ninja must be available on PATH. The current spawn ninja context names the command but does not identify the missing prerequisite.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/serial_dependency_runtime_tests.rs` at line 17, Update the Ninja
process-spawn error handling in the serial dependency runtime tests to
distinguish ErrorKind::NotFound from other failures, explicitly report that
ninja must be available on PATH, and preserve test failure when it is missing.
Keep other spawn errors’ existing context and behavior unchanged.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support serial dependency ordering for actions and targets

2 participants