Skip to content

validate: separate spec defects from values a run supplies - #2450

Merged
yohamta0 merged 4 commits into
mainfrom
fix/validate-runtime-only-notices
Jul 27, 2026
Merged

validate: separate spec defects from values a run supplies#2450
yohamta0 merged 4 commits into
mainfrom
fix/validate-runtime-only-notices

Conversation

@yohamta0

@yohamta0 yohamta0 commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Problem

dagu validate reports every preserved value reference at the same level, so these three are indistinguishable in its output:

Reference Resolves at run time?
${steps.build.outputs.undeclared} never, the output is not declared
${context.paths.artifacts_dir} always
${env.API_TOKEN} yes, the operator supplies it

Only the first is a defect. The other two are reported because validation evaluates a spec outside a run, where a synthetic scope holds almost nothing: noticeBuiltinContext models 3 of the 22 supported context.* bindings, and the env scope is deliberately built with includeOS=false so validation does not depend on the shell it runs in.

The noise is not harmless. A correct workflow was deleted on the strength of a reason=namespace_unavailable line that was a false alarm. Dagu's own bundled examples trip it too, since dagu example 12 uses ${context.paths.artifacts_dir}.

The true positives are worth keeping. The undeclared-output check is what caught a wrong cursor example in the docs (dagucloud/docs#24).

Approach

Classify instead of suppress.

Resolution semantics are unchanged. They already carried the needed distinction: namespace_unavailable means "well formed, absent from this scope", while a name the spec does not define already produced unknown_context_field. So no resolver behaviour moves, and the tests that pin it (builtin_context_test.go, value_notices_test.go) needed no edits.

  • defectunknown_step_id, unknown_output_name, missing_dependency, self_reference, unknown_context_field, unknown_const_name. Reported by default, now at warning level.
  • runtime_onlynamespace_unavailable, unknown_env_binding. Kept out of the default output; --show-unresolved prints them.

noticeBuiltinContext is deliberately not extended to cover all 22 keys. That would silence the symptom while duplicating the runtime list in internal/runtime/eval.go, so every new context key would reintroduce the bug.

Two reasons that had no code

bindingEnvValue and the consts branch of bindingMapValue returned bare fmt.Errorf, so their notices arrived with an empty reason and could not be classified. They now report unknown_env_binding and unknown_const_name.

The split matters: a const is declared in the spec, so an unknown one can never resolve and is a defect. A param is not, and neither is an environment variable. Verified rather than assumed:

$ dagu start undeclared-param.yaml -- UNDECLARED=supplied
value=[supplied]

so params stays runtime-only.

Unrelated false positive fixed alongside

A step's with: was resolved against the DAG-level env scope, so referencing the step's own env: was reported as unresolved. reportSingleStepEnvValueReferenceNotices already built the step scope and threw it away; it is now returned and threaded into the field walk via a new ReferenceField.OwnerStepPath.

Verification

$ dagu validate defects.yaml
level=WARN ${steps.producer.outputs.undeclared} ... the referenced output name is not declared
level=WARN ${context.paths.typo_dir} ... unknown context field
# ${context.paths.artifacts_dir} and ${env.OPERATOR_SUPPLIED}: silent

$ dagu validate --show-unresolved defects.yaml
# ... plus both runtime-only references at INFO

$ dagu validate dbt-nightly.yaml      # the workflow that was deleted
$ dagu example 12 > ex.yaml && dagu validate ex.yaml
# both clean

Green: make lint (0 issues, both GOOS), and internal/cmn/value, internal/core, internal/core/spec, internal/cmd, internal/service/frontend/api/v1, plus the spec007 and spec017 conformance packages.

Note the defect subtests in spec007 passed untouched; only the two namespace_unavailable cases needed --show-unresolved, which is a good sign the split lands where intended.

Notes for review

  • api/v1/api.gen.go is hand-edited (13 lines) rather than regenerated. go.mod pins oapi-codegen v2.7.1 but the checked-in file was produced by v2.5.1, so a real regeneration produces a 12,298-line diff unrelated to this change. That drift is pre-existing and worth a separate PR.
  • make api cannot run on main at all: api-validate fails with schema "DAG": extra sibling fields: [description]. Confirmed present before this change.
  • class is an optional API field and the UI falls back to deriving it from reason, so an older server keeps working.

Summary by cubic

Make dagu validate separate real spec defects from values that only exist during a run. Adds context-aware classification, warns on defects by default, and hides runtime-only references unless you pass --show-unresolved.

  • New Features

    • Classified notices: defect vs runtime_only, with context-aware handling (step-output refs in fields without lookup scope are defects; runtime context misses stay runtime-only).
    • New flag --show-unresolved to print runtime-only notices; defects log at warn, runtime-only at info.
    • API: added optional ValueReferenceNotice.class and reasons unknown_env_binding and unknown_const_name; responses include the computed class. UI groups notices into “Needs a fix” vs “Resolved during a run,” adds a label for unknown_const_name, and classifies old responses correctly (treats ${steps.*} namespace_unavailable as defects), with tests.
  • Bug Fixes

    • Step fields resolve against the step’s own env: (including foreach bodies and handlers), removing false positives.
    • Conformance and CLI tests updated to assert hidden-by-default behavior and --show-unresolved output.

Written for commit a983067. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added --show-unresolved to optionally display references resolved only during workflow execution.
    • Validation now distinguishes actionable defects from runtime-only unresolved references.
    • Inspection views categorize notices into “Needs a fix” and “Resolved during a run,” with clearer reason labels.
    • Added support for identifying unresolved environment bindings and constant names.
  • Bug Fixes
    • Improved environment scoping so step-level variables resolve correctly within the same step.
    • Unsupported context references are reported by default, while supported runtime context remains hidden unless requested.

`dagu validate` reported every preserved value reference at the same level,
so `${context.paths.artifacts_dir}` and `${env.API_TOKEN}` looked exactly
like `${steps.build.outputs.undeclared}`. The first two resolve on every
run; only the third can never resolve. Readers acted on the noise: a
correct workflow was deleted because validation appeared to condemn it.

Notices now carry a class. A reason that names a missing step, output,
context field, or const is a defect and is reported by default, now at
warning level. A reason that only says the evaluating scope held no value
is runtime-only and stays out of the default output; --show-unresolved
prints it. The classification needed no change to how references resolve,
because `namespace_unavailable` already meant "well formed, absent here"
while an undefined name already produced `unknown_context_field`.

Two reasons were unclassifiable because they carried no code at all.
`env` and `consts` lookups returned bare errors, so their notices arrived
with an empty reason. They now report `unknown_env_binding` and
`unknown_const_name`. The split between them is load-bearing: a const is
declared in the spec, so an unknown one is a defect, whereas a param or an
environment variable can be supplied at start time and is not.

Also fixes an unrelated false positive found while classifying: a step's
`with:` was evaluated against the DAG env scope, so a reference to the
step's own `env:` was reported as unresolved. The step scope was already
built and then discarded; it is now threaded through the field walk.
Copilot AI review requested due to automatic review settings July 27, 2026 06:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Unresolved value-reference notices now carry defect or runtime-only classification. Validation hides runtime-only notices unless --show-unresolved is supplied, while APIs and the UI expose and separate both categories. Environment scoping, reason codes, generated schemas, specifications, and conformance tests are updated accordingly.

Changes

Value reference notice classification

Layer / File(s) Summary
Notice contracts and classification
api/v1/api.gen.go, api/v1/api.yaml, internal/cmn/value/notices.go, internal/cmn/value/template.go, internal/cmn/value/notices_test.go, specs/...
Adds notice classes and unresolved reason codes, propagates classifications through notice creation, and documents defect versus runtime-only behavior.
Step-local environment scope resolution
internal/core/value_fields.go, internal/core/value_notices.go
Records owning step paths and uses step- and handler-specific environment scopes when resolving fields.
Validation visibility and logging
internal/cmd/flags.go, internal/cmd/validate.go, internal/cmd/validate_test.go, conformance/spec007..., conformance/spec017...
Adds --show-unresolved, filters runtime-only notices by default, assigns structured log severity, and tests both visibility modes.
Inspection API and notice presentation
internal/service/frontend/api/v1/dags.go, ui/src/api/v1/schema.ts, ui/src/features/dags/components/value-reference-notices/ValueReferenceNoticesButton.tsx
Exposes notice classes through API schemas and separates defect and runtime-only notices in the UI.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ValidateCommand
  participant ReportValueReferenceNotices
  participant NoticeLogger
  ValidateCommand->>ReportValueReferenceNotices: collect classified notices
  ValidateCommand->>NoticeLogger: pass show-unresolved
  NoticeLogger->>ValidateCommand: emit defects and selected runtime-only notices
Loading

Possibly related PRs

  • dagucloud/dagu#2290: Introduced related passive value-reference notice plumbing extended by this change.
  • dagucloud/dagu#2294: Modified the environment-reference notice reporting path extended with structured reasons here.
  • dagucloud/dagu#2302: Added related value-reference reason handling that this change further classifies.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly states the main change: separating validation defects from runtime-only values.
Description check ✅ Passed The description is comprehensive and covers the problem, approach, verification, and review notes, though it doesn't follow the exact template headings.
✨ 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 fix/validate-runtime-only-notices

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

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@specs/003-value-resolution.md`:
- Around line 364-371: Update the validation notice rules in
specs/003-value-resolution.md to resolve the contradiction: clarify that default
validation suppresses only runtime-only notices, while unresolved-reference
defects are reported by default, including their warning-level classification.
Preserve --show-unresolved reporting both notice types and the requirement that
rendered notices distinguish them.

In
`@ui/src/features/dags/components/value-reference-notices/ValueReferenceNoticesButton.tsx`:
- Around line 18-34: Update REASON_LABELS to include a readable label for
unknown_const_name, and add unknown_const_name to DEFECT_REASONS so fallback
classification matches the backend’s defect class. Keep the existing reasonLabel
and NoticeCard behavior unchanged.
🪄 Autofix (Beta)

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: CHILL

Plan: Pro Plus

Run ID: e5b4795b-18e4-44c8-a971-faf890f7abac

📥 Commits

Reviewing files that changed from the base of the PR and between 3568521 and a3e5c1a.

📒 Files selected for processing (18)
  • api/v1/api.gen.go
  • api/v1/api.yaml
  • conformance/spec007_value_resolution_steps/value_resolution_steps_test.go
  • conformance/spec017_built_in_run_context/built_in_run_context_test.go
  • internal/cmd/flags.go
  • internal/cmd/validate.go
  • internal/cmd/validate_test.go
  • internal/cmn/value/notices.go
  • internal/cmn/value/notices_test.go
  • internal/cmn/value/template.go
  • internal/core/value_fields.go
  • internal/core/value_notices.go
  • internal/service/frontend/api/v1/dags.go
  • specs/003-value-resolution.md
  • specs/006-value-resolution-env.md
  • specs/017-built-in-run-context.md
  • ui/src/api/v1/schema.ts
  • ui/src/features/dags/components/value-reference-notices/ValueReferenceNoticesButton.tsx

Comment on lines +364 to +371
Each notice must carry a class.
A notice is a defect when the reference names a step, output, context field, or
const the spec does not define, because no run can resolve it.
A notice is runtime-only when the reference is well formed and the inspecting
scope simply holds no value for it.
`dagu validate` must report defects by default and must keep runtime-only
notices out of its default output; `--show-unresolved` reports both.
Inspection surfaces that render notices must let a reader tell the two apart.

Copy link
Copy Markdown

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

Resolve the default-warning contradiction.

The preceding rule says passive notices must not be shown as normal validation warnings, while this rule requires defects in default validation output and internal/cmd/validate.go logs them at warning level. Clarify that only runtime-only notices are hidden by default.

🧰 Tools
🪛 LanguageTool

[grammar] ~367-~367: Use a hyphen to join words.
Context: ... runtime-only when the reference is well formed and the inspecting scope simply h...

(QB_NEW_EN_HYPHEN)


[grammar] ~371-~371: Ensure spelling is correct
Context: ...ow-unresolved` reports both. Inspection surfaces that render notices must let a reader t...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 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 `@specs/003-value-resolution.md` around lines 364 - 371, Update the validation
notice rules in specs/003-value-resolution.md to resolve the contradiction:
clarify that default validation suppresses only runtime-only notices, while
unresolved-reference defects are reported by default, including their
warning-level classification. Preserve --show-unresolved reporting both notice
types and the requirement that rendered notices distinguish them.

Comment on lines +18 to +34
const REASON_LABELS: Record<string, string> = {
unknown_step_id: 'Step id does not exist',
unknown_output_name: 'Output name is not declared',
missing_dependency: 'Producing step is not a dependency',
self_reference: 'Step references its own output',
unknown_context_field: 'Context field is not defined',
namespace_unavailable: 'Value is supplied by a run',
unknown_env_binding: 'Environment variable is supplied by a run',
};

const DEFECT_REASONS = new Set([
'unknown_step_id',
'unknown_output_name',
'missing_dependency',
'self_reference',
'unknown_context_field',
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Missing unknown_const_name entries in REASON_LABELS and DEFECT_REASONS.

This PR introduces the unknown_const_name reason (classified as defect by the backend's Class()), but neither REASON_LABELS nor the fallback DEFECT_REASONS set was updated for it. NoticeCard calls reasonLabel(notice.reason) unconditionally, so any unknown_const_name notice will display the raw enum string instead of a readable label, and the older-server fallback classification (isDefect) would also misclassify it as runtime-only if notice.class is ever absent.

🐛 Proposed fix
 const REASON_LABELS: Record<string, string> = {
   unknown_step_id: 'Step id does not exist',
   unknown_output_name: 'Output name is not declared',
   missing_dependency: 'Producing step is not a dependency',
   self_reference: 'Step references its own output',
   unknown_context_field: 'Context field is not defined',
+  unknown_const_name: 'Const name is not declared',
   namespace_unavailable: 'Value is supplied by a run',
   unknown_env_binding: 'Environment variable is supplied by a run',
 };

 const DEFECT_REASONS = new Set([
   'unknown_step_id',
   'unknown_output_name',
   'missing_dependency',
   'self_reference',
   'unknown_context_field',
+  'unknown_const_name',
 ]);
📝 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
const REASON_LABELS: Record<string, string> = {
unknown_step_id: 'Step id does not exist',
unknown_output_name: 'Output name is not declared',
missing_dependency: 'Producing step is not a dependency',
self_reference: 'Step references its own output',
unknown_context_field: 'Context field is not defined',
namespace_unavailable: 'Value is supplied by a run',
unknown_env_binding: 'Environment variable is supplied by a run',
};
const DEFECT_REASONS = new Set([
'unknown_step_id',
'unknown_output_name',
'missing_dependency',
'self_reference',
'unknown_context_field',
]);
const REASON_LABELS: Record<string, string> = {
unknown_step_id: 'Step id does not exist',
unknown_output_name: 'Output name is not declared',
missing_dependency: 'Producing step is not a dependency',
self_reference: 'Step references its own output',
unknown_context_field: 'Context field is not defined',
unknown_const_name: 'Const name is not declared',
namespace_unavailable: 'Value is supplied by a run',
unknown_env_binding: 'Environment variable is supplied by a run',
};
const DEFECT_REASONS = new Set([
'unknown_step_id',
'unknown_output_name',
'missing_dependency',
'self_reference',
'unknown_context_field',
'unknown_const_name',
]);
🤖 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
`@ui/src/features/dags/components/value-reference-notices/ValueReferenceNoticesButton.tsx`
around lines 18 - 34, Update REASON_LABELS to include a readable label for
unknown_const_name, and add unknown_const_name to DEFECT_REASONS so fallback
classification matches the backend’s defect class. Keep the existing reasonLabel
and NoticeCard behavior unchanged.

The notice loops in the consts, params, env, and step-reference specs assert
that validation prints references the run supplies: params a caller passes at
start time, environment variables the operator sets, and step outputs read
from a handler. Those are runtime-only, so they now need --show-unresolved.

The defect assertions in the same packages were left alone and still pass
against the default output, which is the split this change is for.
Copilot AI review requested due to automatic review settings July 27, 2026 06:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

The UI kept two tables mirroring the server's reason set and neither gained
unknown_const_name, so such a notice rendered its raw enum string and, on a
response predating the class field, fell back to runtime-only. Both tables now
carry it, and a test walks the generated reason enum so the next reason added
to the API fails here instead of degrading quietly.

The spec said a notice must not be shown as a normal validation warning, then
the new text required defects in the default output, which validate logs at
warning level. The rule now applies to runtime-only notices, which is what it
was protecting, and states that neither class changes the exit code.
Copilot AI review requested due to automatic review settings July 27, 2026 07:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Treat step-output references in fields without lookup scope as defects while keeping runtime context misses informational. Propagate step env scopes through foreach bodies and preserve the computed notice class in API responses.
Copilot AI review requested due to automatic review settings July 27, 2026 08:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@yohamta0
yohamta0 merged commit d86529b into main Jul 27, 2026
14 checks passed
@yohamta0
yohamta0 deleted the fix/validate-runtime-only-notices branch July 27, 2026 11:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants