Skip to content

Resolve dependencies transitively across the full dependency graph - #20

Merged
neilverc merged 1 commit into
mainfrom
multi-hop-resolution
Aug 27, 2026
Merged

Resolve dependencies transitively across the full dependency graph#20
neilverc merged 1 commit into
mainfrom
multi-hop-resolution

Conversation

@neilverc

Copy link
Copy Markdown
Collaborator

Problem

render.go resolved dependencies strictly one-hop: applyDependency never recursed into a target's own Dependencies. A service depending on package/api-rate-limits, which itself depends on dynamo-table/rate-limit-exceeded, never triggered the dynamo-table's dependent hooks — the dependency never reached the service.

Resolution

veil's core now walks the full reachable dependency graph — breadth-first, cycle-safe, visit-once, the same shape graph.go's BFS already uses for veil graph — and applies qualifying dependent hooks along that walk, not just visualizes it.

  • applyDependency (single edge) → applyDependencies (BFS walk) + applyDependentHooks (per-edge application).
  • Each target is resolved (overlays + schema defaults) once and cached, even when reached via multiple paths; hooks still fire once per incoming edge (proven by a diamond-dependency test).
  • ctx.consumer at each hop is the immediate parent in the chain, not necessarily the render root — matches the existing per-(target-kind, consumer-kind) dependent-hook contract, just applied at every hop instead of only the first.
  • Global capability: every kind's dependencies resolve this way now, not scoped to any one kind.
  • SPEC.md updated (pipeline order, render-time execution, numbered pipeline steps) to describe the transitive walk.

Tests

New pkg/render/render_dependencies_test.go:

  • TestMultiHopDependencyAppliesTransitiveDependentHooks — reproduces the service → package → dynamo-table scenario exactly.
  • TestDependencyCycleAppliesEachEdgeOnceWithoutInfiniteLoop — mutual cycle terminates, each direction's hook fires once.
  • TestDiamondDependencyFiresTargetHookOncePerIncomingEdge — a target reached via two independent parents fires its dependent hook once per edge, not deduped by node.

Closes PLAT-8321.

veil's dependency resolution was one-hop: a resource's dependent hooks
only fired for its own directly-declared `dependencies`, never for the
dependencies of a target it depends on. A service depending on
package/api-rate-limits, which itself depends on
dynamo-table/rate-limit-exceeded, never triggered the dynamo-table's
dependent hooks.

applyDependency (single edge) is replaced with applyDependencies, a
breadth-first, cycle-safe, visit-once walk over the full transitive
dependency graph reachable from the render root — the same shape
graph.go's BFS already uses for `veil graph`. applyDependentHooks
applies each edge's qualifying dependent hooks against the one shared
bundle threaded through the whole walk. At every edge, ctx.consumer is
the immediate parent in the chain, not necessarily the render root.

This is a global capability change: every kind's dependencies resolve
this way now, not something scoped to any one kind.

PLAT-8321

@vercel-eddie vercel-eddie left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Stamp

@neilverc
neilverc merged commit 3de3ca6 into main Aug 27, 2026
4 checks passed
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