Skip to content

feat: validate the dependency graph document-wide, not per target - #133

Merged
lesnik512 merged 2 commits into
mainfrom
feat/document-wide-dependency-graph
Sep 20, 2026
Merged

lesnik512 merged 2 commits into
mainfrom
feat/document-wide-dependency-graph

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Closes #87.

What changed

accepted(compose2pod) ⊆ accepted(docker) had one known breach, recorded in the issue as a deliberate ruling: depends_on existence and cycle checks lived in startup_order, which walks only the --target's closure. A service no target reaches could name an undefined service, or sit in a cycle, and the document passed the gate. docker compose config rejects both, whole-document.

graph.validate_graph walks from every service, through the same _walk that startup_order now calls with a single root. One walker, so the two callers cannot drift apart in what they refuse.

parsing._validate_depends_on calls it first. That also removes a guard: condition == "service_healthy" and dep in services and not has_healthcheck(...) silently skipped a service_healthy dependency on a service that does not exist. After the pass, dep is always a service, so the guard is gone rather than left as defensive code.

The price, deliberately

This is what kept the issue open, and it is not something the fix hides: a typo in one service now refuses the whole file, for every target in it. In a shared compose document with many targets, one broken service blocks them all.

That is Docker's own behaviour, and the hard rule leaves no other reading. The one thing that softens it is the message. unknown dependency 'ghost' was tolerable when only the closure could raise it; a document-wide refusal pointing at a service the user never asked to run has to say where the problem is, so _walk threads the declaring service through and the message is now service 'other': unknown dependency 'ghost'. Cycles already named a service.

Why the residual catalogue goes away

tests/conformance/corpus_residual/ existed to execute this breach rather than describe it, and to fail when the residual closed. Both close here, so both documents move into tests/conformance/corpus/ and are held there by assert_rule, which raises on exactly the docker-rejects-we-accept combination. test_residuals.py, the assert_residual fixture, the _RESIDUALS stash and the terminal-summary section go with them.

Keeping the files matters more than keeping the harness: they are the regression test for this PR. If the walk ever goes back to being closure-scoped, the generic corpus run fails on them.

Measurement

Verdicts re-measured against docker compose config v5.1.2 locally:

document before after
ghost depends_on outside the closure docker rejects, we accept both reject
cycle outside the closure docker rejects, we accept both reject

Gates

  • just lint clean.
  • just test-ci: 1448 passed, 100% coverage.
  • just test-conformance: 889 passed, 5 skipped. Over-rejections unchanged at 12; the residual section is gone because there are none.

Follow-on

#132 (links) lands on top of this. Docker runs links through the same dependency graph — a self-link is dependency cycle detected, a links ghost is depends on undefined service — so with this merged, links inherits both refusals from the pass above instead of needing a third residual document, which is what #132 currently says to file.

`depends_on` existence and cycle checks lived in the closure walk that
`--target` drives, so a service no target reaches could name an undefined
service or sit in a cycle and the document still passed. Docker rejects both.

`graph.validate_graph` walks from every service through the same `_walk`
`startup_order` uses, and `parsing._validate_depends_on` calls it first, which
also removes the membership guard that let a `service_healthy` dependency on a
missing service be skipped.

Both residual documents move into `tests/conformance/corpus/`, where
`assert_rule` raises on the combination they used to document, so the residual
catalogue and its fixture go with them.

Closes #87
@lesnik512
lesnik512 merged commit a8704f6 into main Sep 20, 2026
12 checks passed
@lesnik512
lesnik512 deleted the feat/document-wide-dependency-graph branch September 20, 2026 17:54
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.

Validate the depends_on graph outside the target's closure

1 participant