fix(ir): reject cross-pipeline duplicate StepIds; strict de-indent in lower_raw_yaml; document EnvValue::Literal contract#992
Conversation
… lower_raw_yaml; document EnvValue::Literal contract
Three follow-ups from the post-merge review of the native IR PR.
1. Bug: src/compile/ir/graph.rs index_job_steps only rejected
duplicate StepIds within the SAME job. Two jobs each carrying a
step with the same StepId (e.g. a future extension author
accidentally reuses "synthPr" or "marker") would silently
overwrite step_locations, and every OutputRef pointing to that
StepId would resolve to whichever producer was indexed last. The
IR's OutputRef carries only StepId (no job qualifier), so the
implicit contract is that StepIds must be pipeline-wide unique
for correct producer resolution - but that constraint was
neither enforced nor documented.
- index_job_steps now rejects any duplicate StepId regardless of
job / stage, with a typed error citing both previous and
current locations.
- Uniqueness contract is now documented on the StepId module-level
doc (src/compile/ir/ids.rs) and on the OutputRef type-level
doc (src/compile/ir/output.rs).
- Two new tests: cross_job_duplicate_step_id_is_rejected and
cross_stage_duplicate_step_id_is_rejected.
2. Suggestion: src/compile/ir/lower.rs lower_raw_yaml's 2-space
de-indent for `- ` prefixed bodies used .unwrap_or(line) as
fallback. For inputs from step_to_raw_yaml_string the fallback
was dead code, but for any future caller passing a string with
different indentation the fallback would silently produce
misaligned YAML (deferred parse failure several stack frames
later in serde_yaml).
Replaced with strict ok_or_else returning a typed error citing
the offending line. Empty / whitespace-only continuation lines
are kept verbatim (legal between mapping keys, between block
scalars, etc.). Two new tests:
raw_yaml_dash_prefix_rejects_unindented_continuation and
raw_yaml_dash_prefix_accepts_blank_continuation_lines.
3. Suggestion: src/compile/ir/env.rs EnvValue::Literal had no
doc-comment marking the compiler-internal contract. lower_env_value
emits Literal(s) verbatim with no sanitisation; all current
construction sites use hardcoded strings, but the type accepts
any impl Into<String>. A future EnvValue::literal(user_string)
would silently embed raw user input in the env: block.
Doc-comment on the variant now says "**Compiler-internal use
only.**" and lists the safe input categories (hardcoded strings,
constants, values pre-routed through reject_pipeline_injection)
with pointers to PipelineVar / Secret / AdoMacro as the right
choices for runtime-resolved values. Constructor doc updated to
reference the contract.
Validation: cargo build, cargo test (1833 passed, +4 new), cargo
clippy --all-targets --all-features, cargo test --test
bash_lint_tests, cargo run -- compile --force across all 33 fixtures
produces zero lock-file drift - the new validation rejects only
contract-violating input that the production compile path never
emits today.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: looks good — three well-targeted fixes with clear rationale, good error messages, and adequate test coverage. Findings✅ What Looks Good
Documentation trilogy (
|
Three follow-ups from the post-merge review of the native IR PR (#960).
1. Bug — cross-pipeline duplicate StepIds silently overwrite
step_locationssrc/compile/ir/graph.rs::index_job_stepsonly rejected duplicateStepIds within the same job. Two jobs each carrying a step with the sameStepId(e.g. a future extension author accidentally reuses"synthPr"or"marker") would silently overwritestep_locations, and everyOutputRefpointing to thatStepIdwould resolve to whichever producer was indexed last.The IR''s
OutputRefcarries onlyStepId(no job qualifier), so the implicit contract is thatStepIds must be pipeline-wide unique — but that constraint was neither enforced nor documented.index_job_stepsnow rejects any duplicateStepIdregardless of job / stage, with a typed error citing both previous and current locations.StepIdmodule-level doc (src/compile/ir/ids.rs) and on theOutputReftype-level doc (src/compile/ir/output.rs).2. Suggestion — strict de-indent in
lower_raw_yamlThe 2-space de-indent for
-prefixed bodies used.unwrap_or(line)as fallback. For inputs fromstep_to_raw_yaml_stringthe fallback was dead code, but any future caller with a different indentation assumption would silently produce misaligned YAML — surfacing as a serde_yaml parse failure several stack frames later.Replaced with strict
ok_or_elsereturning a typed error citing the offending line. Empty / whitespace-only continuation lines stay verbatim (legal between mapping keys, between block scalars, etc.).Two new tests cover the strict rejection and the blank-continuation-line allowance.
3. Suggestion — document
EnvValue::Literalcompiler-internal contractlower_env_valueemitsLiteral(s)verbatim with no sanitisation. All current construction sites use hardcoded strings, but the type acceptsimpl Into<String>— a futureEnvValue::literal(user_supplied_string)would silently embed raw user input in theenv:block.The variant doc-comment now reads "Compiler-internal use only." and lists the safe input categories (hardcoded strings, constants, values pre-routed through
reject_pipeline_injection) with pointers toPipelineVar/Secret/AdoMacroas the right choices for runtime-resolved values. TheEnvValue::literalconstructor doc references the contract.Validation
cargo buildcleancargo test— 1833 passed, +4 new testscargo clippy --all-targets --all-featurescleancargo test --test bash_lint_testscleancargo run -- compile --forceacross all 33 fixtures produces zero lock-file drift — the new validation rejects only contract-violating input that the production compile path never emits today.