Context
packages/loopover-engine/src/plan-export.ts and packages/loopover-engine/src/plan-step-readiness.ts both compute plan-dependency state from the same PlanDag shape, and disagree on what a dangling dependsOn id means.
orderByDependency (packages/loopover-engine/src/plan-export.ts:37) treats a dependsOn id that isn't present in plan.steps as already satisfied: !present.has(dep) || emitted.has(dep). This is locked in by test/unit/plan-export.test.ts:56-58 ("treats an unknown dependency id as already satisfied").
nextReadySteps (packages/loopover-engine/src/plan-step-readiness.ts:10) treats the identical case as never satisfied: statusById.get(dep) ?? "pending", and isDone() rejects "pending".
Neither this package nor packages/loopover-miner validates that every dependsOn id actually exists in the plan — that referential-integrity check (validatePlanDag) lives only in the app's own src/services/plan-dag.ts, outside this package, so a dangling id reaching these functions is a real, reachable case, not purely hypothetical.
The app's own canonical src/services/plan-dag.ts already resolved this exact question in the other direction: it rejects unknown deps via validatePlanDag, and its own nextReadySteps-equivalent treats them as unsatisfied — i.e. plan-step-readiness.ts already mirrors the app's real answer, and plan-export.ts is the outlier.
Requirements
orderByDependency in packages/loopover-engine/src/plan-export.ts must treat a dependsOn id absent from plan.steps the same way nextReadySteps does: not satisfied (i.e. remove the !present.has(dep) || short-circuit, or replace it with the equivalent "unsatisfied" branch).
- Do not change
plan-step-readiness.ts — it already has the correct semantics; this issue is about aligning plan-export.ts to match it.
- Update the existing test in
test/unit/plan-export.test.ts (currently asserting "treats an unknown dependency id as already satisfied") to assert the corrected behavior instead — don't leave the old assertion in place alongside the fix.
- Do not add plan-validation logic to this file (rejecting dangling ids outright); that's
validatePlanDag's job in the app layer and is out of scope here — this issue is only about making the two ordering/readiness functions agree on how they handle a dangling id if one reaches them.
Deliverables
Test Coverage Requirements
This package is under src/ via the packages/loopover-engine workspace — confirm this package's current coverage.include scoping before assuming the top-level 99% patch gate applies exactly as src/** does; match whatever this package's existing test file already does for its coverage target. The changed branch and the corrected assertion must both be covered — no regression in existing coverage.
Expected Outcome
orderByDependency and nextReadySteps produce consistent results for a plan containing a dependsOn id not present in plan.steps — a step with a dangling dependency is never shown as both "ready now" (via the plan-export renderer used by the MCP loopover_plan_status output) and "permanently blocked" (via resolvePlanOverallStatus/hasPlanReadySteps) for the same plan at the same time.
Links & Resources
packages/loopover-engine/src/plan-export.ts:25 (function start), :37 (the mismatched line)
packages/loopover-engine/src/plan-step-readiness.ts:8-10
test/unit/plan-export.test.ts:56-58
src/services/plan-dag.ts (the app's own canonical resolution of this question, for reference)
Context
packages/loopover-engine/src/plan-export.tsandpackages/loopover-engine/src/plan-step-readiness.tsboth compute plan-dependency state from the samePlanDagshape, and disagree on what a danglingdependsOnid means.orderByDependency(packages/loopover-engine/src/plan-export.ts:37) treats adependsOnid that isn't present inplan.stepsas already satisfied:!present.has(dep) || emitted.has(dep). This is locked in bytest/unit/plan-export.test.ts:56-58("treats an unknown dependency id as already satisfied").nextReadySteps(packages/loopover-engine/src/plan-step-readiness.ts:10) treats the identical case as never satisfied:statusById.get(dep) ?? "pending", andisDone()rejects"pending".Neither this package nor
packages/loopover-minervalidates that everydependsOnid actually exists in the plan — that referential-integrity check (validatePlanDag) lives only in the app's ownsrc/services/plan-dag.ts, outside this package, so a dangling id reaching these functions is a real, reachable case, not purely hypothetical.The app's own canonical
src/services/plan-dag.tsalready resolved this exact question in the other direction: it rejects unknown deps viavalidatePlanDag, and its ownnextReadySteps-equivalent treats them as unsatisfied — i.e.plan-step-readiness.tsalready mirrors the app's real answer, andplan-export.tsis the outlier.Requirements
orderByDependencyinpackages/loopover-engine/src/plan-export.tsmust treat adependsOnid absent fromplan.stepsthe same waynextReadyStepsdoes: not satisfied (i.e. remove the!present.has(dep) ||short-circuit, or replace it with the equivalent "unsatisfied" branch).plan-step-readiness.ts— it already has the correct semantics; this issue is about aligningplan-export.tsto match it.test/unit/plan-export.test.ts(currently asserting "treats an unknown dependency id as already satisfied") to assert the corrected behavior instead — don't leave the old assertion in place alongside the fix.validatePlanDag's job in the app layer and is out of scope here — this issue is only about making the two ordering/readiness functions agree on how they handle a dangling id if one reaches them.Deliverables
orderByDependency(packages/loopover-engine/src/plan-export.ts) updated so a danglingdependsOnid is treated as unsatisfied, matchingplan-step-readiness.ts's existing semantics.test/unit/plan-export.test.ts's dangling-dependency test updated to assert the corrected behavior.orderByDependencyandnextReadyStepsnow agree on the same dangling-dependency input (same plan, same dangling id, consistent readiness/order result from both).Test Coverage Requirements
This package is under
src/via thepackages/loopover-engineworkspace — confirm this package's currentcoverage.includescoping before assuming the top-level 99% patch gate applies exactly assrc/**does; match whatever this package's existing test file already does for its coverage target. The changed branch and the corrected assertion must both be covered — no regression in existing coverage.Expected Outcome
orderByDependencyandnextReadyStepsproduce consistent results for a plan containing adependsOnid not present inplan.steps— a step with a dangling dependency is never shown as both "ready now" (via the plan-export renderer used by the MCPloopover_plan_statusoutput) and "permanently blocked" (viaresolvePlanOverallStatus/hasPlanReadySteps) for the same plan at the same time.Links & Resources
packages/loopover-engine/src/plan-export.ts:25(function start),:37(the mismatched line)packages/loopover-engine/src/plan-step-readiness.ts:8-10test/unit/plan-export.test.ts:56-58src/services/plan-dag.ts(the app's own canonical resolution of this question, for reference)